rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #61.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { validateAvailability } from "@capakraken/engine";
|
||||
import { validateAvailability } from "@nexus/engine";
|
||||
import type {
|
||||
AllocationConflictCheckResult,
|
||||
AllocationStatus,
|
||||
WeekdayAvailability,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { managerProcedure } from "../trpc.js";
|
||||
|
||||
@@ -8,13 +8,9 @@ import {
|
||||
loadAllocationEntry,
|
||||
updateAllocationEntry,
|
||||
updateAssignment,
|
||||
} from "@capakraken/application";
|
||||
import type { PrismaClient } from "@capakraken/db";
|
||||
import {
|
||||
AllocationStatus,
|
||||
CreateAllocationSchema,
|
||||
UpdateAllocationSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/application";
|
||||
import type { PrismaClient } from "@nexus/db";
|
||||
import { AllocationStatus, CreateAllocationSchema, UpdateAllocationSchema } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../../db/helpers.js";
|
||||
@@ -178,7 +174,7 @@ export async function updateAllocationWithAudit(
|
||||
changes: {
|
||||
before: existing.entry,
|
||||
after: updatedAllocation,
|
||||
} as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
} as unknown as import("@nexus/db").Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -205,7 +201,7 @@ export async function deleteAssignmentWithAudit(db: AllocationMutationDb, id: st
|
||||
entityType: "Assignment",
|
||||
entityId: id,
|
||||
action: "DELETE",
|
||||
changes: { before: existing } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
changes: { before: existing } as unknown as import("@nexus/db").Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -226,7 +222,7 @@ export async function deleteAllocationWithAudit(db: AllocationMutationDb, id: st
|
||||
action: "DELETE",
|
||||
changes: {
|
||||
before: existing.entry,
|
||||
} as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
} as unknown as import("@nexus/db").Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -253,7 +249,7 @@ export async function batchDeleteAllocationsWithAudit(db: AllocationMutationDb,
|
||||
id: allocation.entry.id,
|
||||
projectId: allocation.projectId,
|
||||
})),
|
||||
} as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
} as unknown as import("@nexus/db").Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAssignment, updateAssignment } from "@capakraken/application";
|
||||
import { createAssignment, updateAssignment } from "@nexus/application";
|
||||
import {
|
||||
AllocationStatus,
|
||||
CreateAllocationSchema,
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
PermissionKey,
|
||||
UpdateAllocationSchema,
|
||||
UpdateAssignmentSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import type {
|
||||
CreateAllocationInput,
|
||||
CreateAssignmentInput,
|
||||
UpdateAllocationInput,
|
||||
UpdateAssignmentInput,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../../db/helpers.js";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WeekdayAvailability } from "@capakraken/shared";
|
||||
import type { WeekdayAvailability } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
calculateEffectiveAvailableHours,
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
import { averagePerWorkingDay, round1, toIsoDate } from "./shared.js";
|
||||
|
||||
export async function buildResourceAvailabilityView(
|
||||
db: Pick<import("@capakraken/db").PrismaClient, "resource" | "assignment" | "vacation" | "holidayCalendar">,
|
||||
db: Pick<
|
||||
import("@nexus/db").PrismaClient,
|
||||
"resource" | "assignment" | "vacation" | "holidayCalendar"
|
||||
>,
|
||||
input: {
|
||||
resourceId: string;
|
||||
startDate: Date;
|
||||
@@ -21,7 +24,10 @@ export async function buildResourceAvailabilityView(
|
||||
const resource = await db.resource.findUnique({
|
||||
where: { id: input.resourceId },
|
||||
select: {
|
||||
id: true, displayName: true, eid: true, fte: true,
|
||||
id: true,
|
||||
displayName: true,
|
||||
eid: true,
|
||||
fte: true,
|
||||
availability: true,
|
||||
countryId: true,
|
||||
federalState: true,
|
||||
@@ -54,7 +60,11 @@ export async function buildResourceAvailabilityView(
|
||||
endDate: { gte: input.startDate },
|
||||
},
|
||||
select: {
|
||||
id: true, startDate: true, endDate: true, hoursPerDay: true, status: true,
|
||||
id: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
hoursPerDay: true,
|
||||
status: true,
|
||||
project: { select: { name: true, shortCode: true } },
|
||||
},
|
||||
orderBy: { startDate: "asc" },
|
||||
@@ -81,15 +91,17 @@ export async function buildResourceAvailabilityView(
|
||||
|
||||
const contexts = await loadResourceDailyAvailabilityContexts(
|
||||
db,
|
||||
[{
|
||||
id: resource.id,
|
||||
availability,
|
||||
countryId: resource.countryId,
|
||||
countryCode: resource.country?.code,
|
||||
federalState: resource.federalState,
|
||||
metroCityId: resource.metroCityId,
|
||||
metroCityName: resource.metroCity?.name,
|
||||
}],
|
||||
[
|
||||
{
|
||||
id: resource.id,
|
||||
availability,
|
||||
countryId: resource.countryId,
|
||||
countryCode: resource.country?.code,
|
||||
federalState: resource.federalState,
|
||||
metroCityId: resource.metroCityId,
|
||||
metroCityName: resource.metroCity?.name,
|
||||
},
|
||||
],
|
||||
input.startDate,
|
||||
input.endDate,
|
||||
);
|
||||
@@ -151,9 +163,7 @@ export async function buildResourceAvailabilityView(
|
||||
periodEnd: input.endDate,
|
||||
context,
|
||||
});
|
||||
const dailyCapacity = totalWorkingDays > 0
|
||||
? round1(totalPeriodCapacity / totalWorkingDays)
|
||||
: 0;
|
||||
const dailyCapacity = totalWorkingDays > 0 ? round1(totalPeriodCapacity / totalWorkingDays) : 0;
|
||||
|
||||
return {
|
||||
resource: { id: resource.id, name: resource.displayName, eid: resource.eid },
|
||||
@@ -164,9 +174,8 @@ export async function buildResourceAvailabilityView(
|
||||
conflictDays,
|
||||
totalAvailableHours: round1(totalAvailableHours),
|
||||
totalRequestedHours,
|
||||
coveragePercent: totalRequestedHours > 0
|
||||
? Math.round((totalAvailableHours / totalRequestedHours) * 100)
|
||||
: 0,
|
||||
coveragePercent:
|
||||
totalRequestedHours > 0 ? Math.round((totalAvailableHours / totalRequestedHours) * 100) : 0,
|
||||
existingAssignments: existingAssignments.map((assignment) => ({
|
||||
project: assignment.project.name,
|
||||
code: assignment.project.shortCode,
|
||||
@@ -191,9 +200,10 @@ export function buildResourceAvailabilitySummary(
|
||||
availability: Awaited<ReturnType<typeof buildResourceAvailabilityView>>,
|
||||
period: { startDate: Date; endDate: Date },
|
||||
) {
|
||||
const periodAvailableHours = availability.totalRequestedHours > 0
|
||||
? round1(availability.dailyCapacity * availability.totalWorkingDays)
|
||||
: 0;
|
||||
const periodAvailableHours =
|
||||
availability.totalRequestedHours > 0
|
||||
? round1(availability.dailyCapacity * availability.totalWorkingDays)
|
||||
: 0;
|
||||
const periodRemainingHours = round1(availability.totalAvailableHours);
|
||||
const periodBookedHours = round1(Math.max(0, periodAvailableHours - periodRemainingHours));
|
||||
|
||||
@@ -209,11 +219,16 @@ export function buildResourceAvailabilitySummary(
|
||||
currentBookedHoursPerDay: round1(
|
||||
Math.max(
|
||||
0,
|
||||
availability.dailyCapacity - availability.totalAvailableHours / Math.max(availability.totalWorkingDays, 1),
|
||||
availability.dailyCapacity -
|
||||
availability.totalAvailableHours / Math.max(availability.totalWorkingDays, 1),
|
||||
),
|
||||
),
|
||||
availableHoursPerDay: averagePerWorkingDay(availability.totalAvailableHours, availability.totalWorkingDays),
|
||||
isFullyAvailable: availability.existingAssignments.length === 0 && availability.vacations.length === 0,
|
||||
availableHoursPerDay: averagePerWorkingDay(
|
||||
availability.totalAvailableHours,
|
||||
availability.totalWorkingDays,
|
||||
),
|
||||
isFullyAvailable:
|
||||
availability.existingAssignments.length === 0 && availability.vacations.length === 0,
|
||||
existingAllocations: availability.existingAssignments.map((assignment) => ({
|
||||
project: `${assignment.project} (${assignment.code})`,
|
||||
hoursPerDay: assignment.hoursPerDay,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import {
|
||||
deleteDemandRequirement,
|
||||
fillOpenDemand,
|
||||
updateDemandRequirement,
|
||||
} from "@capakraken/application";
|
||||
} from "@nexus/application";
|
||||
import {
|
||||
BoundedJsonRecord,
|
||||
CreateDemandRequirementSchema,
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
FillOpenDemandByAllocationSchema,
|
||||
PermissionKey,
|
||||
UpdateDemandRequirementSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import type {
|
||||
CreateDemandRequirementInput,
|
||||
FillDemandRequirementInput,
|
||||
FillOpenDemandByAllocationInput,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../../db/helpers.js";
|
||||
import {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { PrismaClient } from "@capakraken/db";
|
||||
import { createDemandRequirement, fillDemandRequirement } from "@capakraken/application";
|
||||
import type {
|
||||
CreateDemandRequirementSchema,
|
||||
FillDemandRequirementSchema,
|
||||
} from "@capakraken/shared";
|
||||
import { buildTaskAction } from "@capakraken/shared";
|
||||
import type { PrismaClient } from "@nexus/db";
|
||||
import { createDemandRequirement, fillDemandRequirement } from "@nexus/application";
|
||||
import type { CreateDemandRequirementSchema, FillDemandRequirementSchema } from "@nexus/shared";
|
||||
import { buildTaskAction } from "@nexus/shared";
|
||||
import type { z } from "zod";
|
||||
import { checkBudgetThresholds } from "../../lib/budget-alerts.js";
|
||||
import { generateAutoSuggestions } from "../../lib/auto-staffing.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AllocationStatus } from "@capakraken/shared";
|
||||
import { AllocationStatus } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../../db/helpers.js";
|
||||
import { anonymizeResource, getAnonymizationDirectory } from "../../lib/anonymization.js";
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { AllocationStatus, UpdateAllocationSchema } from "@capakraken/shared";
|
||||
export { toIsoDate, round1, averagePerWorkingDay } from "@capakraken/shared";
|
||||
import { AllocationStatus, UpdateAllocationSchema } from "@nexus/shared";
|
||||
export { toIsoDate, round1, averagePerWorkingDay } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { PROJECT_BRIEF_SELECT, RESOURCE_BRIEF_SELECT, ROLE_BRIEF_SELECT } from "../../db/selects.js";
|
||||
import {
|
||||
PROJECT_BRIEF_SELECT,
|
||||
RESOURCE_BRIEF_SELECT,
|
||||
ROLE_BRIEF_SELECT,
|
||||
} from "../../db/selects.js";
|
||||
|
||||
export const DEMAND_INCLUDE = {
|
||||
project: { select: PROJECT_BRIEF_SELECT },
|
||||
@@ -64,4 +68,3 @@ export type CreateDemandDraftInput = {
|
||||
budgetCents?: number | undefined;
|
||||
metadata?: Record<string, unknown> | undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { buildSplitAllocationReadModel, loadAllocationEntry } from "@capakraken/application";
|
||||
import { AllocationStatus, CreateDemandRequirementSchema } from "@capakraken/shared";
|
||||
import { buildSplitAllocationReadModel, loadAllocationEntry } from "@nexus/application";
|
||||
import { AllocationStatus, CreateDemandRequirementSchema } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../../db/helpers.js";
|
||||
import { anonymizeResource, getAnonymizationDirectory } from "../../lib/anonymization.js";
|
||||
import { ASSIGNMENT_INCLUDE, type AllocationEntryUpdateInput, type AllocationListFilters, type AssignmentResolutionInput, type CreateDemandDraftInput, DEMAND_INCLUDE, toIsoDate } from "./shared.js";
|
||||
import {
|
||||
ASSIGNMENT_INCLUDE,
|
||||
type AllocationEntryUpdateInput,
|
||||
type AllocationListFilters,
|
||||
type AssignmentResolutionInput,
|
||||
type CreateDemandDraftInput,
|
||||
DEMAND_INCLUDE,
|
||||
toIsoDate,
|
||||
} from "./shared.js";
|
||||
|
||||
export function toDemandRequirementUpdateInput(input: AllocationEntryUpdateInput) {
|
||||
return {
|
||||
@@ -38,7 +46,10 @@ export function toAssignmentUpdateInput(input: AllocationEntryUpdateInput) {
|
||||
}
|
||||
|
||||
export async function loadAllocationReadModel(
|
||||
db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment" | "systemSettings" | "resource">,
|
||||
db: Pick<
|
||||
import("@nexus/db").PrismaClient,
|
||||
"demandRequirement" | "assignment" | "systemSettings" | "resource"
|
||||
>,
|
||||
input: AllocationListFilters,
|
||||
) {
|
||||
const [demandRequirements, assignments] = await Promise.all([
|
||||
@@ -64,12 +75,21 @@ export async function loadAllocationReadModel(
|
||||
]);
|
||||
|
||||
const readModel = buildSplitAllocationReadModel({ demandRequirements, assignments });
|
||||
const directory = await getAnonymizationDirectory(db as import("@capakraken/db").PrismaClient);
|
||||
const directory = await getAnonymizationDirectory(db as import("@nexus/db").PrismaClient);
|
||||
if (!directory) {
|
||||
return readModel;
|
||||
}
|
||||
|
||||
function anonymizeAllocation<T extends { resource?: { id: string; eid?: string | null; displayName?: string | null; email?: string | null } | null }>(allocation: T): T {
|
||||
function anonymizeAllocation<
|
||||
T extends {
|
||||
resource?: {
|
||||
id: string;
|
||||
eid?: string | null;
|
||||
displayName?: string | null;
|
||||
email?: string | null;
|
||||
} | null;
|
||||
},
|
||||
>(allocation: T): T {
|
||||
if (!allocation.resource) {
|
||||
return allocation;
|
||||
}
|
||||
@@ -85,7 +105,7 @@ export async function loadAllocationReadModel(
|
||||
}
|
||||
|
||||
export async function findAllocationEntryOrNull(
|
||||
db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment">,
|
||||
db: Pick<import("@nexus/db").PrismaClient, "demandRequirement" | "assignment">,
|
||||
id: string,
|
||||
) {
|
||||
try {
|
||||
@@ -99,7 +119,9 @@ export async function findAllocationEntryOrNull(
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCreateDemandRequirementInput(input: CreateDemandDraftInput): z.infer<typeof CreateDemandRequirementSchema> {
|
||||
export function buildCreateDemandRequirementInput(
|
||||
input: CreateDemandDraftInput,
|
||||
): z.infer<typeof CreateDemandRequirementSchema> {
|
||||
return {
|
||||
projectId: input.projectId,
|
||||
startDate: input.startDate,
|
||||
@@ -116,7 +138,7 @@ export function buildCreateDemandRequirementInput(input: CreateDemandDraftInput)
|
||||
}
|
||||
|
||||
export async function getDemandRequirementByIdOrThrow(
|
||||
db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement">,
|
||||
db: Pick<import("@nexus/db").PrismaClient, "demandRequirement">,
|
||||
id: string,
|
||||
) {
|
||||
return findUniqueOrThrow(
|
||||
@@ -129,7 +151,7 @@ export async function getDemandRequirementByIdOrThrow(
|
||||
}
|
||||
|
||||
export async function resolveAssignmentBySelection(
|
||||
db: Pick<import("@capakraken/db").PrismaClient, "assignment">,
|
||||
db: Pick<import("@nexus/db").PrismaClient, "assignment">,
|
||||
input: AssignmentResolutionInput,
|
||||
) {
|
||||
if (input.assignmentId) {
|
||||
@@ -159,16 +181,19 @@ export async function resolveAssignmentBySelection(
|
||||
orderBy: { startDate: "asc" },
|
||||
});
|
||||
|
||||
const matchingAssignment = assignments
|
||||
.filter((assignment) => {
|
||||
if (input.selectionMode === "WINDOW") {
|
||||
return (!input.startDate || assignment.startDate >= input.startDate)
|
||||
&& (!input.endDate || assignment.endDate <= input.endDate);
|
||||
}
|
||||
const matchingAssignment =
|
||||
assignments
|
||||
.filter((assignment) => {
|
||||
if (input.selectionMode === "WINDOW") {
|
||||
return (
|
||||
(!input.startDate || assignment.startDate >= input.startDate) &&
|
||||
(!input.endDate || assignment.endDate <= input.endDate)
|
||||
);
|
||||
}
|
||||
|
||||
return !input.startDate || toIsoDate(assignment.startDate) === toIsoDate(input.startDate);
|
||||
})
|
||||
.sort((left, right) => right.startDate.getTime() - left.startDate.getTime())[0] ?? null;
|
||||
return !input.startDate || toIsoDate(assignment.startDate) === toIsoDate(input.startDate);
|
||||
})
|
||||
.sort((left, right) => right.startDate.getTime() - left.startDate.getTime())[0] ?? null;
|
||||
|
||||
if (!matchingAssignment) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Assignment not found" });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssistantApprovalStatus, Prisma, type PrismaClient } from "@capakraken/db";
|
||||
import { AssistantApprovalStatus, Prisma, type PrismaClient } from "@nexus/db";
|
||||
import { logger } from "../lib/logger.js";
|
||||
import { buildApprovalSummary } from "./assistant-confirmation.js";
|
||||
|
||||
@@ -96,12 +96,8 @@ function isAssistantApprovalTableMissingError(error: unknown): boolean {
|
||||
const code = typeof candidate.code === "string" ? candidate.code : "";
|
||||
if (code !== "P2021") return false;
|
||||
|
||||
const message = typeof candidate.message === "string"
|
||||
? candidate.message
|
||||
: "";
|
||||
const metaTable = typeof candidate.meta?.table === "string"
|
||||
? candidate.meta.table
|
||||
: "";
|
||||
const message = typeof candidate.message === "string" ? candidate.message : "";
|
||||
const metaTable = typeof candidate.meta?.table === "string" ? candidate.meta.table : "";
|
||||
|
||||
return metaTable.includes("assistant_approvals") || message.includes("assistant_approvals");
|
||||
}
|
||||
@@ -141,29 +137,32 @@ export async function listPendingAssistantApprovals(
|
||||
db: AssistantApprovalStore,
|
||||
userId: string,
|
||||
): Promise<PendingAssistantApproval[]> {
|
||||
return withAssistantApprovalFallback(async () => {
|
||||
await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { lte: new Date() },
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.EXPIRED,
|
||||
},
|
||||
});
|
||||
return withAssistantApprovalFallback(
|
||||
async () => {
|
||||
await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { lte: new Date() },
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.EXPIRED,
|
||||
},
|
||||
});
|
||||
|
||||
const approvals = await db.assistantApproval.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { gt: new Date() },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
const approvals = await db.assistantApproval.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { gt: new Date() },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
return approvals.map(mapPendingApproval);
|
||||
}, () => []);
|
||||
return approvals.map(mapPendingApproval);
|
||||
},
|
||||
() => [],
|
||||
);
|
||||
}
|
||||
|
||||
export async function clearPendingAssistantApproval(
|
||||
@@ -171,19 +170,22 @@ export async function clearPendingAssistantApproval(
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
): Promise<void> {
|
||||
await withAssistantApprovalFallback(async () => {
|
||||
await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.CANCELLED,
|
||||
cancelledAt: new Date(),
|
||||
},
|
||||
});
|
||||
}, () => undefined);
|
||||
await withAssistantApprovalFallback(
|
||||
async () => {
|
||||
await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.CANCELLED,
|
||||
cancelledAt: new Date(),
|
||||
},
|
||||
});
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export async function peekPendingAssistantApproval(
|
||||
@@ -191,30 +193,33 @@ export async function peekPendingAssistantApproval(
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
): Promise<PendingAssistantApproval | null> {
|
||||
return withAssistantApprovalFallback(async () => {
|
||||
await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { lte: new Date() },
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.EXPIRED,
|
||||
},
|
||||
});
|
||||
return withAssistantApprovalFallback(
|
||||
async () => {
|
||||
await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { lte: new Date() },
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.EXPIRED,
|
||||
},
|
||||
});
|
||||
|
||||
const pending = await db.assistantApproval.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
if (!pending) return null;
|
||||
return mapPendingApproval(pending);
|
||||
}, () => null);
|
||||
const pending = await db.assistantApproval.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
if (!pending) return null;
|
||||
return mapPendingApproval(pending);
|
||||
},
|
||||
() => null,
|
||||
);
|
||||
}
|
||||
|
||||
export async function consumePendingAssistantApproval(
|
||||
|
||||
@@ -8,10 +8,7 @@ import {
|
||||
createPendingAssistantApproval,
|
||||
toApprovalPayload,
|
||||
} from "./assistant-approvals.js";
|
||||
import {
|
||||
ASSISTANT_CONFIRMATION_PREFIX,
|
||||
parseToolArguments,
|
||||
} from "./assistant-confirmation.js";
|
||||
import { ASSISTANT_CONFIRMATION_PREFIX, parseToolArguments } from "./assistant-confirmation.js";
|
||||
import {
|
||||
buildAssistantChatResponse,
|
||||
mergeInsights,
|
||||
@@ -216,7 +213,8 @@ async function handleAssistantToolCalls(input: {
|
||||
|
||||
return {
|
||||
response: buildAssistantChatResponse({
|
||||
content: "Schreibende Assistant-Aktionen sind gerade nicht verfuegbar, weil der Bestaetigungsspeicher in der Datenbank fehlt. Bitte die CapaKraken-DB-Migration anwenden und dann erneut versuchen.",
|
||||
content:
|
||||
"Schreibende Assistant-Aktionen sind gerade nicht verfuegbar, weil der Bestaetigungsspeicher in der Datenbank fehlt. Bitte die Nexus-DB-Migration anwenden und dann erneut versuchen.",
|
||||
insights: collectedInsights,
|
||||
actions: collectedActions,
|
||||
}),
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
resolvePermissions,
|
||||
type PermissionOverrides,
|
||||
SystemRole,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ASSISTANT_CONFIRMATION_PREFIX } from "./assistant-confirmation.js";
|
||||
|
||||
export const ASSISTANT_SYSTEM_PROMPT = `Du bist der CapaKraken-Assistent — ein hilfreicher AI-Assistent für Ressourcenplanung und Projektmanagement in einer 3D-Produktionsumgebung.
|
||||
export const ASSISTANT_SYSTEM_PROMPT = `Du bist der Nexus-Assistent — ein hilfreicher AI-Assistent für Ressourcenplanung und Projektmanagement in einer 3D-Produktionsumgebung.
|
||||
|
||||
Deine Fähigkeiten:
|
||||
- Fragen über Ressourcen, Projekte, Allokationen, Budget, Urlaub, Estimates, Org-Struktur, Rollen, Blueprints, Rate Cards beantworten
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type PermissionKey } from "@capakraken/shared";
|
||||
import { type PermissionKey } from "@nexus/shared";
|
||||
|
||||
import { getAvailableAssistantToolsForContext } from "./assistant-tools.js";
|
||||
import type { ToolDef } from "./assistant-tools/shared.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ToolManifest } from "@capakraken/shared";
|
||||
import type { ToolManifest } from "@nexus/shared";
|
||||
|
||||
/**
|
||||
* Rich metadata registry for AI assistant tools.
|
||||
@@ -67,7 +67,8 @@ export const TOOL_REGISTRY: ToolManifest[] = [
|
||||
category: "staffing",
|
||||
isMutation: false,
|
||||
requiresAdvanced: true,
|
||||
intent: "Get ranked resource suggestions for an open demand, scored by skill match, availability, and cost",
|
||||
intent:
|
||||
"Get ranked resource suggestions for an open demand, scored by skill match, availability, and cost",
|
||||
examples: [
|
||||
"Who should I assign to fill the senior designer role on Project Z?",
|
||||
"Find the best available backend engineers for a 3-month engagement starting in June",
|
||||
@@ -99,10 +100,7 @@ export const TOOL_REGISTRY: ToolManifest[] = [
|
||||
category: "vacation",
|
||||
isMutation: true,
|
||||
intent: "Approve a pending vacation request",
|
||||
examples: [
|
||||
"Approve Alice's vacation request",
|
||||
"Accept the leave request with ID abc123",
|
||||
],
|
||||
examples: ["Approve Alice's vacation request", "Accept the leave request with ID abc123"],
|
||||
},
|
||||
// ─── Reporting ─────────────────────────────────────────────────────────────
|
||||
{
|
||||
@@ -110,7 +108,8 @@ export const TOOL_REGISTRY: ToolManifest[] = [
|
||||
category: "reporting",
|
||||
isMutation: false,
|
||||
requiresAdvanced: true,
|
||||
intent: "Generate a chargeability report showing billable vs. non-billable utilization across resources or chapters",
|
||||
intent:
|
||||
"Generate a chargeability report showing billable vs. non-billable utilization across resources or chapters",
|
||||
examples: [
|
||||
"Show me chargeability for the engineering chapter in Q1",
|
||||
"What is the billable rate for the team last month?",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - ./assistant-tools/access-control.ts (role/permission gating logic)
|
||||
*/
|
||||
|
||||
import { PermissionKey } from "@capakraken/shared";
|
||||
import { PermissionKey } from "@nexus/shared";
|
||||
import { createReadOnlyProxy } from "../lib/read-only-prisma.js";
|
||||
import { logger } from "../lib/logger.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { PermissionKey, SystemRole } from "@nexus/shared";
|
||||
import type { ToolAccessRequirements, ToolContext, ToolDef } from "./shared.js";
|
||||
import { AssistantVisibleError } from "./helpers.js";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { AllocationStatus, PermissionKey, UpdateAssignmentSchema } from "@capakraken/shared";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { AllocationStatus, PermissionKey, UpdateAssignmentSchema } from "@nexus/shared";
|
||||
import { SystemRole } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { fmtEur } from "../../lib/format-utils.js";
|
||||
@@ -104,132 +104,168 @@ type AllocationPlanningDeps = {
|
||||
toAssistantAllocationNotFoundError: (error: unknown) => unknown;
|
||||
};
|
||||
|
||||
export const allocationPlanningReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_allocations",
|
||||
description: "List assignments/allocations, optionally filtered by resource or project. Shows who is assigned where, hours/day, dates, and cost.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Filter by resource ID" },
|
||||
projectId: { type: "string", description: "Filter by project ID" },
|
||||
resourceName: { type: "string", description: "Filter by resource name (partial match)" },
|
||||
projectCode: { type: "string", description: "Filter by project short code (partial match)" },
|
||||
status: { type: "string", description: "Filter by status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
|
||||
limit: { type: "integer", description: "Max results. Default: 30" },
|
||||
export const allocationPlanningReadToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_allocations",
|
||||
description:
|
||||
"List assignments/allocations, optionally filtered by resource or project. Shows who is assigned where, hours/day, dates, and cost.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Filter by resource ID" },
|
||||
projectId: { type: "string", description: "Filter by project ID" },
|
||||
resourceName: {
|
||||
type: "string",
|
||||
description: "Filter by resource name (partial match)",
|
||||
},
|
||||
projectCode: {
|
||||
type: "string",
|
||||
description: "Filter by project short code (partial match)",
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Filter by status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED",
|
||||
},
|
||||
limit: { type: "integer", description: "Max results. Default: 30" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_budget_status",
|
||||
description: "Get the budget status of a project: total budget, confirmed/proposed costs, remaining, utilization percentage.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_budget_status",
|
||||
description:
|
||||
"Get the budget status of a project: total budget, confirmed/proposed costs, remaining, utilization percentage.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
list_allocations: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
get_budget_status: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresCostView: true,
|
||||
},
|
||||
},
|
||||
], {
|
||||
list_allocations: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
get_budget_status: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresCostView: true,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export const allocationPlanningMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_allocation",
|
||||
description: "Create a new allocation/booking for a resource on a project. Requires manageAllocations permission. Always confirm with the user before calling this. Created with PROPOSED status.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID" },
|
||||
projectId: { type: "string", description: "Project ID" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
hoursPerDay: { type: "number", description: "Hours per day (e.g. 8)" },
|
||||
role: { type: "string", description: "Optional role name" },
|
||||
},
|
||||
required: ["resourceId", "projectId", "startDate", "endDate", "hoursPerDay"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "cancel_allocation",
|
||||
description: "Cancel an existing allocation. Can find by allocation ID, or by resource name + project code + date range. Requires manageAllocations permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allocationId: { type: "string", description: "Allocation ID (if known)" },
|
||||
resourceName: { type: "string", description: "Resource name (partial match)" },
|
||||
projectCode: { type: "string", description: "Project short code (partial match)" },
|
||||
startDate: { type: "string", description: "Filter by start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "Filter by end date YYYY-MM-DD" },
|
||||
export const allocationPlanningMutationToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_allocation",
|
||||
description:
|
||||
"Create a new allocation/booking for a resource on a project. Requires manageAllocations permission. Always confirm with the user before calling this. Created with PROPOSED status.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID" },
|
||||
projectId: { type: "string", description: "Project ID" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
hoursPerDay: { type: "number", description: "Hours per day (e.g. 8)" },
|
||||
role: { type: "string", description: "Optional role name" },
|
||||
},
|
||||
required: ["resourceId", "projectId", "startDate", "endDate", "hoursPerDay"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_allocation_status",
|
||||
description: "Change the status of an existing allocation. Can reactivate cancelled allocations, confirm proposed ones, etc. Requires manageAllocations permission. Always confirm with the user before calling.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allocationId: { type: "string", description: "Allocation ID" },
|
||||
resourceName: { type: "string", description: "Resource name (partial match, used if no allocationId)" },
|
||||
projectCode: { type: "string", description: "Project short code (partial match, used if no allocationId)" },
|
||||
startDate: { type: "string", description: "Start date filter YYYY-MM-DD (used if no allocationId)" },
|
||||
newStatus: { type: "string", description: "New status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "cancel_allocation",
|
||||
description:
|
||||
"Cancel an existing allocation. Can find by allocation ID, or by resource name + project code + date range. Requires manageAllocations permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allocationId: { type: "string", description: "Allocation ID (if known)" },
|
||||
resourceName: { type: "string", description: "Resource name (partial match)" },
|
||||
projectCode: { type: "string", description: "Project short code (partial match)" },
|
||||
startDate: { type: "string", description: "Filter by start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "Filter by end date YYYY-MM-DD" },
|
||||
},
|
||||
},
|
||||
required: ["newStatus"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_allocation_status",
|
||||
description:
|
||||
"Change the status of an existing allocation. Can reactivate cancelled allocations, confirm proposed ones, etc. Requires manageAllocations permission. Always confirm with the user before calling.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allocationId: { type: "string", description: "Allocation ID" },
|
||||
resourceName: {
|
||||
type: "string",
|
||||
description: "Resource name (partial match, used if no allocationId)",
|
||||
},
|
||||
projectCode: {
|
||||
type: "string",
|
||||
description: "Project short code (partial match, used if no allocationId)",
|
||||
},
|
||||
startDate: {
|
||||
type: "string",
|
||||
description: "Start date filter YYYY-MM-DD (used if no allocationId)",
|
||||
},
|
||||
newStatus: {
|
||||
type: "string",
|
||||
description: "New status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED",
|
||||
},
|
||||
},
|
||||
required: ["newStatus"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
create_allocation: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
cancel_allocation: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
update_allocation_status: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
},
|
||||
], {
|
||||
create_allocation: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
cancel_allocation: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
update_allocation_status: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createAllocationPlanningExecutors(
|
||||
deps: AllocationPlanningDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async list_allocations(params: {
|
||||
resourceId?: string;
|
||||
projectId?: string;
|
||||
resourceName?: string;
|
||||
projectCode?: string;
|
||||
status?: string;
|
||||
limit?: number;
|
||||
}, ctx: ToolContext) {
|
||||
async list_allocations(
|
||||
params: {
|
||||
resourceId?: string;
|
||||
projectId?: string;
|
||||
resourceName?: string;
|
||||
projectCode?: string;
|
||||
status?: string;
|
||||
limit?: number;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
|
||||
const status = params.status && Object.values(AllocationStatus).includes(params.status as AllocationStatus)
|
||||
? params.status as AllocationStatus
|
||||
: undefined;
|
||||
const status =
|
||||
params.status && Object.values(AllocationStatus).includes(params.status as AllocationStatus)
|
||||
? (params.status as AllocationStatus)
|
||||
: undefined;
|
||||
const readModel = await caller.listView({
|
||||
...(params.resourceId ? { resourceId: params.resourceId } : {}),
|
||||
...(params.projectId ? { projectId: params.projectId } : {}),
|
||||
@@ -243,14 +279,14 @@ export function createAllocationPlanningExecutors(
|
||||
return readModel.assignments
|
||||
.filter((assignment) => {
|
||||
if (
|
||||
resourceNameQuery
|
||||
&& !assignment.resource?.displayName?.toLowerCase().includes(resourceNameQuery)
|
||||
resourceNameQuery &&
|
||||
!assignment.resource?.displayName?.toLowerCase().includes(resourceNameQuery)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
projectCodeQuery
|
||||
&& !assignment.project?.shortCode?.toLowerCase().includes(projectCodeQuery)
|
||||
projectCodeQuery &&
|
||||
!assignment.project?.shortCode?.toLowerCase().includes(projectCodeQuery)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -304,14 +340,17 @@ export function createAllocationPlanningExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async create_allocation(params: {
|
||||
resourceId: string;
|
||||
projectId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
hoursPerDay: number;
|
||||
role?: string;
|
||||
}, ctx: ToolContext) {
|
||||
async create_allocation(
|
||||
params: {
|
||||
resourceId: string;
|
||||
projectId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
hoursPerDay: number;
|
||||
role?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
const [resource, project] = await Promise.all([
|
||||
deps.resolveResourceIdentifier(ctx, params.resourceId),
|
||||
@@ -345,19 +384,25 @@ export function createAllocationPlanningExecutors(
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError && error.code === "CONFLICT") {
|
||||
return { error: "Allocation already exists for this resource/project/dates. No new allocation created." };
|
||||
return {
|
||||
error:
|
||||
"Allocation already exists for this resource/project/dates. No new allocation created.",
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async cancel_allocation(params: {
|
||||
allocationId?: string;
|
||||
resourceName?: string;
|
||||
projectCode?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}, ctx: ToolContext) {
|
||||
async cancel_allocation(
|
||||
params: {
|
||||
allocationId?: string;
|
||||
resourceName?: string;
|
||||
projectCode?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
|
||||
const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
|
||||
@@ -420,13 +465,16 @@ export function createAllocationPlanningExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async update_allocation_status(params: {
|
||||
allocationId?: string;
|
||||
resourceName?: string;
|
||||
projectCode?: string;
|
||||
startDate?: string;
|
||||
newStatus: string;
|
||||
}, ctx: ToolContext) {
|
||||
async update_allocation_status(
|
||||
params: {
|
||||
allocationId?: string;
|
||||
resourceName?: string;
|
||||
projectCode?: string;
|
||||
startDate?: string;
|
||||
newStatus: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
|
||||
const validStatuses = ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -45,53 +45,66 @@ type AuditHistoryDeps = {
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
};
|
||||
|
||||
export const auditHistoryToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "query_change_history",
|
||||
description: "Search the audit history for changes to projects, resources, allocations, vacations, or any entity. Reuses the real audit log list API. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", description: "Filter by entity type (e.g. 'Project', 'Resource', 'Allocation', 'Vacation', 'Role', 'Estimate')" },
|
||||
search: { type: "string", description: "Search in entity name or summary text" },
|
||||
userId: { type: "string", description: "Filter by user ID who made the change" },
|
||||
daysBack: { type: "integer", description: "How many days back to search. Default: 7" },
|
||||
action: { type: "string", description: "Filter by action type: CREATE, UPDATE, DELETE, SHIFT, IMPORT" },
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
export const auditHistoryToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "query_change_history",
|
||||
description:
|
||||
"Search the audit history for changes to projects, resources, allocations, vacations, or any entity. Reuses the real audit log list API. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: {
|
||||
type: "string",
|
||||
description:
|
||||
"Filter by entity type (e.g. 'Project', 'Resource', 'Allocation', 'Vacation', 'Role', 'Estimate')",
|
||||
},
|
||||
search: { type: "string", description: "Search in entity name or summary text" },
|
||||
userId: { type: "string", description: "Filter by user ID who made the change" },
|
||||
daysBack: { type: "integer", description: "How many days back to search. Default: 7" },
|
||||
action: {
|
||||
type: "string",
|
||||
description: "Filter by action type: CREATE, UPDATE, DELETE, SHIFT, IMPORT",
|
||||
},
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_entity_timeline",
|
||||
description: "Get the audit history for a specific entity (project, resource, etc.) via the real audit API. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", description: "Entity type (e.g. 'Project', 'Resource', 'Allocation')" },
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
limit: { type: "integer", description: "Max results. Default: 50" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_entity_timeline",
|
||||
description:
|
||||
"Get the audit history for a specific entity (project, resource, etc.) via the real audit API. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: {
|
||||
type: "string",
|
||||
description: "Entity type (e.g. 'Project', 'Resource', 'Allocation')",
|
||||
},
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
limit: { type: "integer", description: "Max results. Default: 50" },
|
||||
},
|
||||
required: ["entityType", "entityId"],
|
||||
},
|
||||
required: ["entityType", "entityId"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
query_change_history: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_entity_timeline: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
},
|
||||
], {
|
||||
query_change_history: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_entity_timeline: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createAuditHistoryExecutors(
|
||||
deps: AuditHistoryDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
export function createAuditHistoryExecutors(deps: AuditHistoryDeps): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async query_change_history(
|
||||
params: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -17,24 +17,25 @@ type ResolvedResource = {
|
||||
|
||||
type BlueprintsRateCardsDeps = {
|
||||
createBlueprintCaller: (ctx: TRPCContext) => {
|
||||
listSummaries: () => Promise<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
_count: { projects: number };
|
||||
}>>;
|
||||
listSummaries: () => Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
_count: { projects: number };
|
||||
}>
|
||||
>;
|
||||
getByIdentifier: (params: { identifier: string }) => Promise<BlueprintRecord>;
|
||||
};
|
||||
createRateCardCaller: (ctx: TRPCContext) => {
|
||||
list: (params: {
|
||||
isActive: boolean;
|
||||
search?: string;
|
||||
}) => Promise<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
effectiveFrom: Date | null;
|
||||
effectiveTo: Date | null;
|
||||
_count: { lines: number };
|
||||
}>>;
|
||||
list: (params: { isActive: boolean; search?: string }) => Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
effectiveFrom: Date | null;
|
||||
effectiveTo: Date | null;
|
||||
_count: { lines: number };
|
||||
}>
|
||||
>;
|
||||
resolveBestRate: (params: {
|
||||
resourceId?: string;
|
||||
roleName?: string;
|
||||
@@ -50,81 +51,85 @@ type BlueprintsRateCardsDeps = {
|
||||
resolve: () => Promise<T>,
|
||||
notFoundMessage: string,
|
||||
) => Promise<T | AssistantToolErrorResult>;
|
||||
parseOptionalIsoDate: (
|
||||
value: string | undefined,
|
||||
fieldName: string,
|
||||
) => Date | undefined;
|
||||
parseOptionalIsoDate: (value: string | undefined, fieldName: string) => Date | undefined;
|
||||
fmtDate: (value: Date | null | undefined) => string | null;
|
||||
};
|
||||
|
||||
export const blueprintsRateCardsToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_blueprints",
|
||||
description: "List available project blueprints with their field definitions.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_blueprint",
|
||||
description: "Get detailed blueprint with all field definitions and role presets.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Blueprint ID or name (partial match)" },
|
||||
},
|
||||
required: ["identifier"],
|
||||
export const blueprintsRateCardsToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_blueprints",
|
||||
description: "List available project blueprints with their field definitions.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_rate_cards",
|
||||
description: "List rate cards with their effective dates and line items.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search by name" },
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_blueprint",
|
||||
description: "Get detailed blueprint with all field definitions and role presets.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Blueprint ID or name (partial match)" },
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "resolve_rate",
|
||||
description: "Look up the applicable rate for a resource, role, or management level from rate cards.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID or name" },
|
||||
roleName: { type: "string", description: "Role name" },
|
||||
date: { type: "string", description: "Date to check rate for (YYYY-MM-DD). Default: today" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_rate_cards",
|
||||
description: "List rate cards with their effective dates and line items.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search by name" },
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "resolve_rate",
|
||||
description:
|
||||
"Look up the applicable rate for a resource, role, or management level from rate cards.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID or name" },
|
||||
roleName: { type: "string", description: "Role name" },
|
||||
date: {
|
||||
type: "string",
|
||||
description: "Date to check rate for (YYYY-MM-DD). Default: today",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
list_blueprints: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
get_blueprint: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
list_rate_cards: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresCostView: true,
|
||||
},
|
||||
resolve_rate: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresCostView: true,
|
||||
},
|
||||
},
|
||||
], {
|
||||
list_blueprints: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
get_blueprint: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
list_rate_cards: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresCostView: true,
|
||||
},
|
||||
resolve_rate: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresCostView: true,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createBlueprintsRateCardsExecutors(
|
||||
deps: BlueprintsRateCardsDeps,
|
||||
@@ -158,10 +163,7 @@ export function createBlueprintsRateCardsExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async list_rate_cards(
|
||||
params: { query?: string; limit?: number },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async list_rate_cards(params: { query?: string; limit?: number }, ctx: ToolContext) {
|
||||
const caller = deps.createRateCardCaller(deps.createScopedCallerContext(ctx));
|
||||
const cards = await caller.list({
|
||||
isActive: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { PermissionKey, SystemRole } from "@nexus/shared";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
@@ -67,93 +67,129 @@ export type ChargeabilityComputationDeps = {
|
||||
) => Promise<ResolvedProject | AssistantToolErrorResult>;
|
||||
};
|
||||
|
||||
export const chargeabilityComputationReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_chargeability_report",
|
||||
description: "Get the detailed chargeability report readmodel for a month range, including group totals and per-resource month series. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startMonth: { type: "string", description: "Start month in YYYY-MM format." },
|
||||
endMonth: { type: "string", description: "End month in YYYY-MM format." },
|
||||
orgUnitId: { type: "string", description: "Optional org unit filter." },
|
||||
managementLevelGroupId: { type: "string", description: "Optional management level group filter." },
|
||||
countryId: { type: "string", description: "Optional country filter." },
|
||||
includeProposed: { type: "boolean", description: "Whether proposed bookings should count towards chargeability. Default: false." },
|
||||
resourceQuery: { type: "string", description: "Optional resource filter by name or eid after loading the report." },
|
||||
resourceLimit: { type: "integer", description: "Maximum number of resources returned. Default: 25, max 100." },
|
||||
export const chargeabilityComputationReadToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_chargeability_report",
|
||||
description:
|
||||
"Get the detailed chargeability report readmodel for a month range, including group totals and per-resource month series. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startMonth: { type: "string", description: "Start month in YYYY-MM format." },
|
||||
endMonth: { type: "string", description: "End month in YYYY-MM format." },
|
||||
orgUnitId: { type: "string", description: "Optional org unit filter." },
|
||||
managementLevelGroupId: {
|
||||
type: "string",
|
||||
description: "Optional management level group filter.",
|
||||
},
|
||||
countryId: { type: "string", description: "Optional country filter." },
|
||||
includeProposed: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Whether proposed bookings should count towards chargeability. Default: false.",
|
||||
},
|
||||
resourceQuery: {
|
||||
type: "string",
|
||||
description: "Optional resource filter by name or eid after loading the report.",
|
||||
},
|
||||
resourceLimit: {
|
||||
type: "integer",
|
||||
description: "Maximum number of resources returned. Default: 25, max 100.",
|
||||
},
|
||||
},
|
||||
required: ["startMonth", "endMonth"],
|
||||
},
|
||||
required: ["startMonth", "endMonth"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_resource_computation_graph",
|
||||
description: "Get the resource computation graph with transparent SAH, holiday, absence, allocation, chargeability, and budget derivation factors. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, eid, or display name." },
|
||||
month: { type: "string", description: "Month in YYYY-MM format." },
|
||||
domain: { type: "string", enum: ["INPUT", "SAH", "ALLOCATION", "RULES", "CHARGEABILITY", "BUDGET"], description: "Optional domain filter for graph nodes." },
|
||||
includeLinks: { type: "boolean", description: "Include graph links for the selected nodes. Default: false." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_resource_computation_graph",
|
||||
description:
|
||||
"Get the resource computation graph with transparent SAH, holiday, absence, allocation, chargeability, and budget derivation factors. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, eid, or display name." },
|
||||
month: { type: "string", description: "Month in YYYY-MM format." },
|
||||
domain: {
|
||||
type: "string",
|
||||
enum: ["INPUT", "SAH", "ALLOCATION", "RULES", "CHARGEABILITY", "BUDGET"],
|
||||
description: "Optional domain filter for graph nodes.",
|
||||
},
|
||||
includeLinks: {
|
||||
type: "boolean",
|
||||
description: "Include graph links for the selected nodes. Default: false.",
|
||||
},
|
||||
},
|
||||
required: ["resourceId", "month"],
|
||||
},
|
||||
required: ["resourceId", "month"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_project_computation_graph",
|
||||
description: "Get the project computation graph with estimate, commercial, effort, experience, spread, and budget derivation factors. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID, short code, or project name." },
|
||||
domain: { type: "string", enum: ["INPUT", "ESTIMATE", "COMMERCIAL", "EXPERIENCE", "EFFORT", "SPREAD", "BUDGET"], description: "Optional domain filter for graph nodes." },
|
||||
includeLinks: { type: "boolean", description: "Include graph links for the selected nodes. Default: false." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_project_computation_graph",
|
||||
description:
|
||||
"Get the project computation graph with estimate, commercial, effort, experience, spread, and budget derivation factors. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID, short code, or project name." },
|
||||
domain: {
|
||||
type: "string",
|
||||
enum: ["INPUT", "ESTIMATE", "COMMERCIAL", "EXPERIENCE", "EFFORT", "SPREAD", "BUDGET"],
|
||||
description: "Optional domain filter for graph nodes.",
|
||||
},
|
||||
includeLinks: {
|
||||
type: "boolean",
|
||||
description: "Include graph links for the selected nodes. Default: false.",
|
||||
},
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
get_chargeability_report: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
get_resource_computation_graph: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
get_project_computation_graph: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
},
|
||||
], {
|
||||
get_chargeability_report: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
get_resource_computation_graph: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
get_project_computation_graph: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createChargeabilityComputationExecutors(
|
||||
deps: ChargeabilityComputationDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async get_chargeability_report(params: {
|
||||
startMonth: string;
|
||||
endMonth: string;
|
||||
orgUnitId?: string;
|
||||
managementLevelGroupId?: string;
|
||||
countryId?: string;
|
||||
includeProposed?: boolean;
|
||||
resourceQuery?: string;
|
||||
resourceLimit?: number;
|
||||
}, ctx: ToolContext) {
|
||||
async get_chargeability_report(
|
||||
params: {
|
||||
startMonth: string;
|
||||
endMonth: string;
|
||||
orgUnitId?: string;
|
||||
managementLevelGroupId?: string;
|
||||
countryId?: string;
|
||||
includeProposed?: boolean;
|
||||
resourceQuery?: string;
|
||||
resourceLimit?: number;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertPermission(ctx, PermissionKey.VIEW_COSTS);
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
@@ -162,7 +198,9 @@ export function createChargeabilityComputationExecutors(
|
||||
startMonth: params.startMonth,
|
||||
endMonth: params.endMonth,
|
||||
...(params.orgUnitId ? { orgUnitId: params.orgUnitId } : {}),
|
||||
...(params.managementLevelGroupId ? { managementLevelGroupId: params.managementLevelGroupId } : {}),
|
||||
...(params.managementLevelGroupId
|
||||
? { managementLevelGroupId: params.managementLevelGroupId }
|
||||
: {}),
|
||||
...(params.countryId ? { countryId: params.countryId } : {}),
|
||||
includeProposed: params.includeProposed ?? false,
|
||||
...(params.resourceQuery ? { resourceQuery: params.resourceQuery } : {}),
|
||||
@@ -170,12 +208,15 @@ export function createChargeabilityComputationExecutors(
|
||||
});
|
||||
},
|
||||
|
||||
async get_resource_computation_graph(params: {
|
||||
resourceId: string;
|
||||
month: string;
|
||||
domain?: ResourceComputationDomain;
|
||||
includeLinks?: boolean;
|
||||
}, ctx: ToolContext) {
|
||||
async get_resource_computation_graph(
|
||||
params: {
|
||||
resourceId: string;
|
||||
month: string;
|
||||
domain?: ResourceComputationDomain;
|
||||
includeLinks?: boolean;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertPermission(ctx, PermissionKey.VIEW_COSTS);
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
@@ -193,11 +234,14 @@ export function createChargeabilityComputationExecutors(
|
||||
});
|
||||
},
|
||||
|
||||
async get_project_computation_graph(params: {
|
||||
projectId: string;
|
||||
domain?: ProjectComputationDomain;
|
||||
includeLinks?: boolean;
|
||||
}, ctx: ToolContext) {
|
||||
async get_project_computation_graph(
|
||||
params: {
|
||||
projectId: string;
|
||||
domain?: ProjectComputationDomain;
|
||||
includeLinks?: boolean;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertPermission(ctx, PermissionKey.VIEW_COSTS);
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SystemRole,
|
||||
UpdateClientSchema,
|
||||
UpdateOrgUnitSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -17,10 +17,7 @@ type ClientsOrgUnitsDeps = {
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
update: (params: {
|
||||
id: string;
|
||||
data: z.input<typeof UpdateClientSchema>;
|
||||
}) => Promise<{
|
||||
update: (params: { id: string; data: z.input<typeof UpdateClientSchema> }) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
@@ -32,10 +29,7 @@ type ClientsOrgUnitsDeps = {
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
update: (params: {
|
||||
id: string;
|
||||
data: z.input<typeof UpdateOrgUnitSchema>;
|
||||
}) => Promise<{
|
||||
update: (params: { id: string; data: z.input<typeof UpdateOrgUnitSchema> }) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
@@ -45,134 +39,145 @@ type ClientsOrgUnitsDeps = {
|
||||
error: unknown,
|
||||
action?: "create" | "update" | "delete",
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantOrgUnitMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantOrgUnitMutationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const clientMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_client",
|
||||
description: "Create a new client. Requires manager or admin role. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Client name" },
|
||||
code: { type: "string", description: "Client code" },
|
||||
parentId: { type: "string", description: "Optional parent client ID" },
|
||||
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
|
||||
tags: { type: "array", items: { type: "string" }, description: "Optional client tags" },
|
||||
export const clientMutationToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_client",
|
||||
description: "Create a new client. Requires manager or admin role. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Client name" },
|
||||
code: { type: "string", description: "Client code" },
|
||||
parentId: { type: "string", description: "Optional parent client ID" },
|
||||
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
|
||||
tags: { type: "array", items: { type: "string" }, description: "Optional client tags" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_client",
|
||||
description: "Update a client. Requires manager or admin role. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Client ID" },
|
||||
name: { type: "string", description: "New name" },
|
||||
code: { type: "string", description: "New code" },
|
||||
sortOrder: { type: "integer", description: "New sort order" },
|
||||
isActive: { type: "boolean", description: "Set active state" },
|
||||
parentId: { type: "string", description: "Parent client ID; use null to clear" },
|
||||
tags: { type: "array", items: { type: "string" }, description: "Replacement client tags" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_client",
|
||||
description: "Update a client. Requires manager or admin role. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Client ID" },
|
||||
name: { type: "string", description: "New name" },
|
||||
code: { type: "string", description: "New code" },
|
||||
sortOrder: { type: "integer", description: "New sort order" },
|
||||
isActive: { type: "boolean", description: "Set active state" },
|
||||
parentId: { type: "string", description: "Parent client ID; use null to clear" },
|
||||
tags: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Replacement client tags",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_client",
|
||||
description: "Delete a client. Requires admin role. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Client ID" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_client",
|
||||
description: "Delete a client. Requires admin role. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Client ID" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
create_client: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
update_client: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
delete_client: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
},
|
||||
], {
|
||||
create_client: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
update_client: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
delete_client: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export const orgUnitMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_org_unit",
|
||||
description: "Create a new organizational unit. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Org unit name" },
|
||||
shortName: { type: "string", description: "Short name/code" },
|
||||
level: { type: "integer", description: "Level (5, 6, or 7)" },
|
||||
parentId: { type: "string", description: "Parent org unit ID (optional)" },
|
||||
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
|
||||
export const orgUnitMutationToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_org_unit",
|
||||
description: "Create a new organizational unit. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Org unit name" },
|
||||
shortName: { type: "string", description: "Short name/code" },
|
||||
level: { type: "integer", description: "Level (5, 6, or 7)" },
|
||||
parentId: { type: "string", description: "Parent org unit ID (optional)" },
|
||||
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
|
||||
},
|
||||
required: ["name", "level"],
|
||||
},
|
||||
required: ["name", "level"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_org_unit",
|
||||
description: "Update an organizational unit. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Org unit ID" },
|
||||
name: { type: "string", description: "New name" },
|
||||
shortName: { type: "string", description: "New short name" },
|
||||
sortOrder: { type: "integer", description: "New sort order" },
|
||||
isActive: { type: "boolean", description: "Set active state" },
|
||||
parentId: { type: "string", description: "Parent org unit ID; use null to clear" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_org_unit",
|
||||
description: "Update an organizational unit. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Org unit ID" },
|
||||
name: { type: "string", description: "New name" },
|
||||
shortName: { type: "string", description: "New short name" },
|
||||
sortOrder: { type: "integer", description: "New sort order" },
|
||||
isActive: { type: "boolean", description: "Set active state" },
|
||||
parentId: { type: "string", description: "Parent org unit ID; use null to clear" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
create_org_unit: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_org_unit: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
},
|
||||
], {
|
||||
create_org_unit: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_org_unit: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createClientsOrgUnitsExecutors(
|
||||
deps: ClientsOrgUnitsDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async create_client(params: {
|
||||
name: string;
|
||||
code?: string;
|
||||
parentId?: string;
|
||||
sortOrder?: number;
|
||||
tags?: string[];
|
||||
}, ctx: ToolContext) {
|
||||
async create_client(
|
||||
params: {
|
||||
name: string;
|
||||
code?: string;
|
||||
parentId?: string;
|
||||
sortOrder?: number;
|
||||
tags?: string[];
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createClientCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
let client;
|
||||
@@ -196,15 +201,18 @@ export function createClientsOrgUnitsExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async update_client(params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
code?: string | null;
|
||||
sortOrder?: number;
|
||||
isActive?: boolean;
|
||||
parentId?: string | null;
|
||||
tags?: string[];
|
||||
}, ctx: ToolContext) {
|
||||
async update_client(
|
||||
params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
code?: string | null;
|
||||
sortOrder?: number;
|
||||
isActive?: boolean;
|
||||
parentId?: string | null;
|
||||
tags?: string[];
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createClientCaller(deps.createScopedCallerContext(ctx));
|
||||
const data = UpdateClientSchema.parse({
|
||||
...(params.name !== undefined ? { name: params.name } : {}),
|
||||
@@ -262,13 +270,16 @@ export function createClientsOrgUnitsExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async create_org_unit(params: {
|
||||
name: string;
|
||||
shortName?: string;
|
||||
level: number;
|
||||
parentId?: string;
|
||||
sortOrder?: number;
|
||||
}, ctx: ToolContext) {
|
||||
async create_org_unit(
|
||||
params: {
|
||||
name: string;
|
||||
shortName?: string;
|
||||
level: number;
|
||||
parentId?: string;
|
||||
sortOrder?: number;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createOrgUnitCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
let orgUnit;
|
||||
@@ -292,14 +303,17 @@ export function createClientsOrgUnitsExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async update_org_unit(params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
shortName?: string | null;
|
||||
sortOrder?: number;
|
||||
isActive?: boolean;
|
||||
parentId?: string | null;
|
||||
}, ctx: ToolContext) {
|
||||
async update_org_unit(
|
||||
params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
shortName?: string | null;
|
||||
sortOrder?: number;
|
||||
isActive?: boolean;
|
||||
parentId?: string | null;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createOrgUnitCaller(deps.createScopedCallerContext(ctx));
|
||||
const data = UpdateOrgUnitSchema.parse({
|
||||
...(params.name !== undefined ? { name: params.name } : {}),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { COMMENT_ENTITY_TYPE_VALUES, type CommentEntityType } from "@capakraken/shared";
|
||||
import { COMMENT_ENTITY_TYPE_VALUES, type CommentEntityType } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../../lib/comment-entity-registry.js";
|
||||
import {
|
||||
getCommentToolEntityDescription,
|
||||
getCommentToolScopeSentence,
|
||||
} from "../../lib/comment-entity-registry.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
@@ -31,75 +34,86 @@ type CommentsDeps = {
|
||||
entityId: string;
|
||||
body: string;
|
||||
}) => Promise<CommentRecord>;
|
||||
resolve: (params: {
|
||||
id: string;
|
||||
resolved: boolean;
|
||||
}) => Promise<CommentRecord>;
|
||||
resolve: (params: { id: string; resolved: boolean }) => Promise<CommentRecord>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
toAssistantCommentCreationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantCommentResolveError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantCommentCreationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantCommentResolveError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const commentReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_comments",
|
||||
description: `List comments (with replies) for a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
export const commentReadToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_comments",
|
||||
description: `List comments (with replies) for a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: {
|
||||
type: "string",
|
||||
enum: [...COMMENT_ENTITY_TYPE_VALUES],
|
||||
description: getCommentToolEntityDescription(),
|
||||
},
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
},
|
||||
required: ["entityType", "entityId"],
|
||||
},
|
||||
required: ["entityType", "entityId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {});
|
||||
],
|
||||
{},
|
||||
);
|
||||
|
||||
export const commentMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_comment",
|
||||
description: `Add a comment to a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required. Supports @mentions. Always confirm with the user first.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
body: { type: "string", description: "Comment body text. Use @[Name](userId) for mentions." },
|
||||
export const commentMutationToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_comment",
|
||||
description: `Add a comment to a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required. Supports @mentions. Always confirm with the user first.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: {
|
||||
type: "string",
|
||||
enum: [...COMMENT_ENTITY_TYPE_VALUES],
|
||||
description: getCommentToolEntityDescription(),
|
||||
},
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
body: {
|
||||
type: "string",
|
||||
description: "Comment body text. Use @[Name](userId) for mentions.",
|
||||
},
|
||||
},
|
||||
required: ["entityType", "entityId", "body"],
|
||||
},
|
||||
required: ["entityType", "entityId", "body"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "resolve_comment",
|
||||
description: `Mark a comment as resolved (or unresolve it) on a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required, and only the comment author or an admin can change resolution.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
commentId: { type: "string", description: "Comment ID to resolve" },
|
||||
resolved: { type: "boolean", description: "Set to true to resolve, false to unresolve. Default: true" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "resolve_comment",
|
||||
description: `Mark a comment as resolved (or unresolve it) on a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required, and only the comment author or an admin can change resolution.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
commentId: { type: "string", description: "Comment ID to resolve" },
|
||||
resolved: {
|
||||
type: "boolean",
|
||||
description: "Set to true to resolve, false to unresolve. Default: true",
|
||||
},
|
||||
},
|
||||
required: ["commentId"],
|
||||
},
|
||||
required: ["commentId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {});
|
||||
],
|
||||
{},
|
||||
);
|
||||
|
||||
export function createCommentExecutors(
|
||||
deps: CommentsDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
export function createCommentExecutors(deps: CommentsDeps): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async list_comments(
|
||||
params: { entityType: CommentEntityType; entityId: string },
|
||||
@@ -166,10 +180,7 @@ export function createCommentExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async resolve_comment(
|
||||
params: { commentId: string; resolved?: boolean },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async resolve_comment(params: { commentId: string; resolved?: boolean }, ctx: ToolContext) {
|
||||
const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx));
|
||||
let updated;
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import { SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import {
|
||||
CreateCountrySchema,
|
||||
CreateMetroCitySchema,
|
||||
UpdateCountrySchema,
|
||||
UpdateMetroCitySchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -45,136 +45,143 @@ type CountryMetroAdminDeps = {
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
assertAdminRole: (ctx: ToolContext) => void;
|
||||
formatCountry: (country: CountryRecord) => unknown;
|
||||
toAssistantCountryMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantMetroCityMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantCountryMutationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantMetroCityMutationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const countryMetroAdminToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_country",
|
||||
description: "Create a country with daily working hours and optional schedule rules. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: { type: "string", description: "ISO country code such as DE or ES." },
|
||||
name: { type: "string", description: "Country name." },
|
||||
dailyWorkingHours: { type: "number", description: "Standard daily working hours." },
|
||||
scheduleRules: {
|
||||
type: "object",
|
||||
description: "Optional schedule rule object such as the Spain reduced-hours configuration.",
|
||||
},
|
||||
},
|
||||
required: ["code", "name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_country",
|
||||
description: "Update a country including working hours, schedule rules, or active state. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Country ID." },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: { type: "string" },
|
||||
name: { type: "string" },
|
||||
dailyWorkingHours: { type: "number" },
|
||||
scheduleRules: { type: ["object", "null"] },
|
||||
isActive: { type: "boolean" },
|
||||
export const countryMetroAdminToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_country",
|
||||
description:
|
||||
"Create a country with daily working hours and optional schedule rules. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: { type: "string", description: "ISO country code such as DE or ES." },
|
||||
name: { type: "string", description: "Country name." },
|
||||
dailyWorkingHours: { type: "number", description: "Standard daily working hours." },
|
||||
scheduleRules: {
|
||||
type: "object",
|
||||
description:
|
||||
"Optional schedule rule object such as the Spain reduced-hours configuration.",
|
||||
},
|
||||
},
|
||||
required: ["code", "name"],
|
||||
},
|
||||
required: ["id", "data"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_metro_city",
|
||||
description: "Create a metro city for a country. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
countryId: { type: "string", description: "Country ID." },
|
||||
name: { type: "string", description: "Metro city name." },
|
||||
},
|
||||
required: ["countryId", "name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_metro_city",
|
||||
description: "Rename a metro city. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Metro city ID." },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_country",
|
||||
description:
|
||||
"Update a country including working hours, schedule rules, or active state. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Country ID." },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: { type: "string" },
|
||||
name: { type: "string" },
|
||||
dailyWorkingHours: { type: "number" },
|
||||
scheduleRules: { type: ["object", "null"] },
|
||||
isActive: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["id", "data"],
|
||||
},
|
||||
required: ["id", "data"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_metro_city",
|
||||
description:
|
||||
"Create a metro city for a country. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
countryId: { type: "string", description: "Country ID." },
|
||||
name: { type: "string", description: "Metro city name." },
|
||||
},
|
||||
required: ["countryId", "name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_metro_city",
|
||||
description: "Rename a metro city. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Metro city ID." },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["id", "data"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_metro_city",
|
||||
description:
|
||||
"Delete a metro city when no resource is assigned to it. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Metro city ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_metro_city",
|
||||
description: "Delete a metro city when no resource is assigned to it. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Metro city ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
create_country: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_country: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
create_metro_city: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_metro_city: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
delete_metro_city: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
},
|
||||
], {
|
||||
create_country: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_country: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
create_metro_city: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_metro_city: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
delete_metro_city: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createCountryMetroAdminExecutors(
|
||||
deps: CountryMetroAdminDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async create_country(params: {
|
||||
code: string;
|
||||
name: string;
|
||||
dailyWorkingHours?: number;
|
||||
scheduleRules?: Prisma.JsonValue | null;
|
||||
}, ctx: ToolContext) {
|
||||
async create_country(
|
||||
params: {
|
||||
code: string;
|
||||
name: string;
|
||||
dailyWorkingHours?: number;
|
||||
scheduleRules?: Prisma.JsonValue | null;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertAdminRole(ctx);
|
||||
const caller = deps.createCountryCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
@@ -198,16 +205,19 @@ export function createCountryMetroAdminExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async update_country(params: {
|
||||
id: string;
|
||||
data: {
|
||||
code?: string;
|
||||
name?: string;
|
||||
dailyWorkingHours?: number;
|
||||
scheduleRules?: Prisma.JsonValue | null;
|
||||
isActive?: boolean;
|
||||
};
|
||||
}, ctx: ToolContext) {
|
||||
async update_country(
|
||||
params: {
|
||||
id: string;
|
||||
data: {
|
||||
code?: string;
|
||||
name?: string;
|
||||
dailyWorkingHours?: number;
|
||||
scheduleRules?: Prisma.JsonValue | null;
|
||||
isActive?: boolean;
|
||||
};
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertAdminRole(ctx);
|
||||
const caller = deps.createCountryCaller(deps.createScopedCallerContext(ctx));
|
||||
const input = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -22,65 +22,66 @@ type CountryReadmodelsDeps = {
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
formatCountry: (country: CountryRecord) => unknown;
|
||||
toAssistantCountryNotFoundError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantCountryNotFoundError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const countryReadmodelToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_countries",
|
||||
description: "List countries including working hours, schedule rules, active flag, and metro cities.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
includeInactive: { type: "boolean", description: "Include inactive countries. Default: false." },
|
||||
search: { type: "string", description: "Optional country code or name search." },
|
||||
export const countryReadmodelToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_countries",
|
||||
description:
|
||||
"List countries including working hours, schedule rules, active flag, and metro cities.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
includeInactive: {
|
||||
type: "boolean",
|
||||
description: "Include inactive countries. Default: false.",
|
||||
},
|
||||
search: { type: "string", description: "Optional country code or name search." },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_country",
|
||||
description: "Get one country with schedule rules, active flag, metro cities, and resource count. Accepts ID, code, or name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Country ID, code, or name." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_country",
|
||||
description:
|
||||
"Get one country with schedule rules, active flag, metro cities, and resource count. Accepts ID, code, or name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Country ID, code, or name." },
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
get_country: {
|
||||
requiresResourceOverview: true,
|
||||
},
|
||||
},
|
||||
], {
|
||||
get_country: {
|
||||
requiresResourceOverview: true,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createCountryReadmodelExecutors(
|
||||
deps: CountryReadmodelsDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async list_countries(
|
||||
params: { includeInactive?: boolean; search?: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async list_countries(params: { includeInactive?: boolean; search?: string }, ctx: ToolContext) {
|
||||
const caller = deps.createCountryCaller(deps.createScopedCallerContext(ctx));
|
||||
const countries = await caller.list(
|
||||
params.includeInactive
|
||||
? undefined
|
||||
: { isActive: true },
|
||||
);
|
||||
const countries = await caller.list(params.includeInactive ? undefined : { isActive: true });
|
||||
const normalizedSearch = params.search?.trim().toLowerCase();
|
||||
const filteredCountries = normalizedSearch
|
||||
? countries.filter((country) =>
|
||||
country.code.toLowerCase().includes(normalizedSearch)
|
||||
|| country.name.toLowerCase().includes(normalizedSearch))
|
||||
? countries.filter(
|
||||
(country) =>
|
||||
country.code.toLowerCase().includes(normalizedSearch) ||
|
||||
country.name.toLowerCase().includes(normalizedSearch),
|
||||
)
|
||||
: countries;
|
||||
|
||||
return {
|
||||
@@ -89,10 +90,7 @@ export function createCountryReadmodelExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async get_country(
|
||||
params: { identifier: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async get_country(params: { identifier: string }, ctx: ToolContext) {
|
||||
const caller = deps.createCountryCaller(deps.createScopedCallerContext(ctx));
|
||||
let country;
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { PermissionKey, SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -49,141 +49,156 @@ type DashboardInsightsReportsDeps = {
|
||||
|
||||
const REPORT_ENTITIES: ReportEntity[] = ["resource", "project", "assignment", "resource_month"];
|
||||
|
||||
export const dashboardInsightsReportsToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_dashboard_detail",
|
||||
description: "Get detailed dashboard data: peak allocation times, top-value resources, demand pipeline, chargeability overview, project health risks, and skill gap staffing pressure.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
section: {
|
||||
type: "string",
|
||||
description: "Which section: peak_times, top_resources, demand_pipeline, chargeability_overview, project_health, skill_gaps, or all",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "detect_anomalies",
|
||||
description: "Detect anomalies across all active projects: budget burn rate issues, staffing gaps, utilization outliers, and timeline overruns.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_skill_gaps",
|
||||
description: "Analyze skill supply vs demand across all active projects. Returns which skills are in short supply relative to demand requirements.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_project_health",
|
||||
description: "Get health scores for all active projects based on budget utilization, staffing completeness, and timeline status.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_budget_forecast",
|
||||
description: "Get budget utilization and burn rate per active project. Shows total budget, spent, remaining, and whether burn is ahead or behind schedule.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_insights_summary",
|
||||
description: "Get a summary of anomaly counts by category (budget, staffing, timeline, utilization) plus critical count.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "run_report",
|
||||
description: "Run a dynamic report query on resources, projects, assignments, or resource-month rows with flexible column selection and filtering.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entity: {
|
||||
type: "string",
|
||||
enum: ["resource", "project", "assignment", "resource_month"],
|
||||
description: "Entity type to query",
|
||||
},
|
||||
columns: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Column keys to include (e.g. 'displayName', 'chapter', 'country.name')",
|
||||
},
|
||||
filters: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
field: { type: "string", description: "Field to filter on" },
|
||||
op: { type: "string", enum: ["eq", "neq", "gt", "lt", "gte", "lte", "contains", "in"], description: "Filter operator" },
|
||||
value: { type: "string", description: "Filter value (string)" },
|
||||
},
|
||||
required: ["field", "op", "value"],
|
||||
export const dashboardInsightsReportsToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_dashboard_detail",
|
||||
description:
|
||||
"Get detailed dashboard data: peak allocation times, top-value resources, demand pipeline, chargeability overview, project health risks, and skill gap staffing pressure.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
section: {
|
||||
type: "string",
|
||||
description:
|
||||
"Which section: peak_times, top_resources, demand_pipeline, chargeability_overview, project_health, skill_gaps, or all",
|
||||
},
|
||||
description: "Filters to apply",
|
||||
},
|
||||
periodMonth: {
|
||||
type: "string",
|
||||
description: "Required for resource_month reports. Format: YYYY-MM",
|
||||
},
|
||||
groupBy: {
|
||||
type: "string",
|
||||
description: "Optional scalar field used to group result rows into labeled sections.",
|
||||
},
|
||||
sortBy: {
|
||||
type: "string",
|
||||
description: "Optional scalar field used to sort rows within the grouped result.",
|
||||
},
|
||||
sortDir: {
|
||||
type: "string",
|
||||
enum: ["asc", "desc"],
|
||||
description: "Sort direction for sortBy. Default: asc",
|
||||
},
|
||||
limit: { type: "integer", description: "Max results. Default: 50" },
|
||||
},
|
||||
required: ["entity", "columns"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "detect_anomalies",
|
||||
description:
|
||||
"Detect anomalies across all active projects: budget burn rate issues, staffing gaps, utilization outliers, and timeline overruns.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_skill_gaps",
|
||||
description:
|
||||
"Analyze skill supply vs demand across all active projects. Returns which skills are in short supply relative to demand requirements.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_project_health",
|
||||
description:
|
||||
"Get health scores for all active projects based on budget utilization, staffing completeness, and timeline status.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_budget_forecast",
|
||||
description:
|
||||
"Get budget utilization and burn rate per active project. Shows total budget, spent, remaining, and whether burn is ahead or behind schedule.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_insights_summary",
|
||||
description:
|
||||
"Get a summary of anomaly counts by category (budget, staffing, timeline, utilization) plus critical count.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "run_report",
|
||||
description:
|
||||
"Run a dynamic report query on resources, projects, assignments, or resource-month rows with flexible column selection and filtering.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entity: {
|
||||
type: "string",
|
||||
enum: ["resource", "project", "assignment", "resource_month"],
|
||||
description: "Entity type to query",
|
||||
},
|
||||
columns: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Column keys to include (e.g. 'displayName', 'chapter', 'country.name')",
|
||||
},
|
||||
filters: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
field: { type: "string", description: "Field to filter on" },
|
||||
op: {
|
||||
type: "string",
|
||||
enum: ["eq", "neq", "gt", "lt", "gte", "lte", "contains", "in"],
|
||||
description: "Filter operator",
|
||||
},
|
||||
value: { type: "string", description: "Filter value (string)" },
|
||||
},
|
||||
required: ["field", "op", "value"],
|
||||
},
|
||||
description: "Filters to apply",
|
||||
},
|
||||
periodMonth: {
|
||||
type: "string",
|
||||
description: "Required for resource_month reports. Format: YYYY-MM",
|
||||
},
|
||||
groupBy: {
|
||||
type: "string",
|
||||
description: "Optional scalar field used to group result rows into labeled sections.",
|
||||
},
|
||||
sortBy: {
|
||||
type: "string",
|
||||
description: "Optional scalar field used to sort rows within the grouped result.",
|
||||
},
|
||||
sortDir: {
|
||||
type: "string",
|
||||
enum: ["asc", "desc"],
|
||||
description: "Sort direction for sortBy. Default: asc",
|
||||
},
|
||||
limit: { type: "integer", description: "Max results. Default: 50" },
|
||||
},
|
||||
required: ["entity", "columns"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
get_dashboard_detail: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
detect_anomalies: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_skill_gaps: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_project_health: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_budget_forecast: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
},
|
||||
get_insights_summary: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
run_report: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
},
|
||||
], {
|
||||
get_dashboard_detail: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
detect_anomalies: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_skill_gaps: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_project_health: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_budget_forecast: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
},
|
||||
get_insights_summary: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
run_report: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createDashboardInsightsReportsExecutors(
|
||||
deps: DashboardInsightsReportsDeps,
|
||||
@@ -220,21 +235,23 @@ export function createDashboardInsightsReportsExecutors(
|
||||
return caller.getInsightsSummary();
|
||||
},
|
||||
|
||||
async run_report(params: {
|
||||
entity: string;
|
||||
columns: string[];
|
||||
filters?: ReportFilter[];
|
||||
periodMonth?: string;
|
||||
groupBy?: string;
|
||||
sortBy?: string;
|
||||
sortDir?: "asc" | "desc";
|
||||
limit?: number;
|
||||
}, ctx: ToolContext) {
|
||||
async run_report(
|
||||
params: {
|
||||
entity: string;
|
||||
columns: string[];
|
||||
filters?: ReportFilter[];
|
||||
periodMonth?: string;
|
||||
groupBy?: string;
|
||||
sortBy?: string;
|
||||
sortDir?: "asc" | "desc";
|
||||
limit?: number;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const entity = params.entity as ReportEntity;
|
||||
if (!REPORT_ENTITIES.includes(entity)) {
|
||||
return {
|
||||
error:
|
||||
`Unknown entity: ${params.entity}. Use resource, project, assignment, or resource_month.`,
|
||||
error: `Unknown entity: ${params.entity}. Use resource, project, assignment, or resource_month.`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { Prisma, VacationType } from "@capakraken/db";
|
||||
import { AllocationStatus, PermissionKey, SystemRole, toIsoDateOrNull } from "@capakraken/shared";
|
||||
import type { WeekdayAvailability } from "@capakraken/shared";
|
||||
import { Prisma, VacationType } from "@nexus/db";
|
||||
import { AllocationStatus, PermissionKey, SystemRole, toIsoDateOrNull } from "@nexus/shared";
|
||||
import type { WeekdayAvailability } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { ZodError } from "zod";
|
||||
export { fmtEur } from "../../lib/format-utils.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DispoStagedRecordType, ImportBatchStatus, StagedRecordStatus } from "@capakraken/db";
|
||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { DispoStagedRecordType, ImportBatchStatus, StagedRecordStatus } from "@nexus/db";
|
||||
import { PermissionKey, SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -86,305 +86,383 @@ type ImportExportDispoDeps = {
|
||||
toAssistantDispoImportBatchNotFoundError: (error: unknown) => { error: string } | null;
|
||||
};
|
||||
|
||||
const CONTROLLER_ASSISTANT_ROLES = [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER] as const;
|
||||
const CONTROLLER_ASSISTANT_ROLES = [
|
||||
SystemRole.ADMIN,
|
||||
SystemRole.MANAGER,
|
||||
SystemRole.CONTROLLER,
|
||||
] as const;
|
||||
const MANAGER_ASSISTANT_ROLES = [SystemRole.ADMIN, SystemRole.MANAGER] as const;
|
||||
const ADMIN_ASSISTANT_ROLES = [SystemRole.ADMIN] as const;
|
||||
|
||||
export const importExportDispoToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "export_resources_csv",
|
||||
description: "Export the current active resource list as CSV via the real import/export router. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
export const importExportDispoToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "export_resources_csv",
|
||||
description:
|
||||
"Export the current active resource list as CSV via the real import/export router. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "export_projects_csv",
|
||||
description: "Export the current project list as CSV via the real import/export router. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "export_projects_csv",
|
||||
description:
|
||||
"Export the current project list as CSV via the real import/export router. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "import_csv_data",
|
||||
description: "Import CSV-style row data for resources, projects, or allocations via the real import/export router. Requires manager/admin, importData permission, and defaults to dry-run.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", enum: ["resources", "projects", "allocations"], description: "Import target entity type." },
|
||||
rows: {
|
||||
type: "array",
|
||||
description: "CSV rows already parsed to key/value objects.",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "import_csv_data",
|
||||
description:
|
||||
"Import CSV-style row data for resources, projects, or allocations via the real import/export router. Requires manager/admin, importData permission, and defaults to dry-run.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: {
|
||||
type: "string",
|
||||
enum: ["resources", "projects", "allocations"],
|
||||
description: "Import target entity type.",
|
||||
},
|
||||
rows: {
|
||||
type: "array",
|
||||
description: "CSV rows already parsed to key/value objects.",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string" },
|
||||
},
|
||||
},
|
||||
dryRun: {
|
||||
type: "boolean",
|
||||
description: "Validate only without persisting changes. Default: true.",
|
||||
},
|
||||
},
|
||||
dryRun: { type: "boolean", description: "Validate only without persisting changes. Default: true." },
|
||||
required: ["entityType", "rows"],
|
||||
},
|
||||
required: ["entityType", "rows"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_import_batches",
|
||||
description:
|
||||
"List Dispo import batches with pagination and optional status filter via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: "string", description: "Optional batch status filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_dispo_import_batch",
|
||||
description:
|
||||
"Get one Dispo import batch including staged record counters via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Import batch ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "stage_dispo_import_batch",
|
||||
description:
|
||||
"Stage a Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
planningWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Filesystem path to the planning workbook.",
|
||||
},
|
||||
referenceWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Filesystem path to the reference workbook.",
|
||||
},
|
||||
chargeabilityWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Filesystem path to the chargeability workbook.",
|
||||
},
|
||||
costWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Optional filesystem path to the cost workbook.",
|
||||
},
|
||||
rosterWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Optional filesystem path to the roster workbook.",
|
||||
},
|
||||
notes: { type: "string", description: "Optional import notes." },
|
||||
},
|
||||
required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "validate_dispo_import_batch",
|
||||
description:
|
||||
"Validate a Dispo import batch readiness check via the real dispo router without committing anything. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
planningWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Filesystem path to the planning workbook.",
|
||||
},
|
||||
referenceWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Filesystem path to the reference workbook.",
|
||||
},
|
||||
chargeabilityWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Filesystem path to the chargeability workbook.",
|
||||
},
|
||||
costWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Optional filesystem path to the cost workbook.",
|
||||
},
|
||||
rosterWorkbookPath: {
|
||||
type: "string",
|
||||
description: "Optional filesystem path to the roster workbook.",
|
||||
},
|
||||
importBatchId: {
|
||||
type: "string",
|
||||
description: "Optional existing staged import batch ID.",
|
||||
},
|
||||
notes: { type: "string", description: "Optional import notes." },
|
||||
},
|
||||
required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "cancel_dispo_import_batch",
|
||||
description:
|
||||
"Cancel a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Import batch ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_resources",
|
||||
description:
|
||||
"List staged Dispo resources for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
status: { type: "string", description: "Optional staged record status filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_projects",
|
||||
description:
|
||||
"List staged Dispo projects for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
status: { type: "string", description: "Optional staged record status filter." },
|
||||
isTbd: { type: "boolean", description: "Optional TBD-project filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_assignments",
|
||||
description:
|
||||
"List staged Dispo assignments for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
status: { type: "string", description: "Optional staged record status filter." },
|
||||
resourceExternalId: {
|
||||
type: "string",
|
||||
description: "Optional resource external ID filter.",
|
||||
},
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_vacations",
|
||||
description:
|
||||
"List staged Dispo vacations for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
resourceExternalId: {
|
||||
type: "string",
|
||||
description: "Optional resource external ID filter.",
|
||||
},
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_unresolved_records",
|
||||
description:
|
||||
"List staged unresolved Dispo records for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
recordType: { type: "string", description: "Optional unresolved record type filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "resolve_dispo_staged_record",
|
||||
description:
|
||||
"Resolve one staged Dispo record via the real dispo router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Staged record ID." },
|
||||
recordType: { type: "string", description: "Staged record type." },
|
||||
action: {
|
||||
type: "string",
|
||||
enum: ["APPROVE", "REJECT", "SKIP"],
|
||||
description: "Resolution action.",
|
||||
},
|
||||
},
|
||||
required: ["id", "recordType", "action"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "commit_dispo_import_batch",
|
||||
description:
|
||||
"Commit a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
allowTbdUnresolved: {
|
||||
type: "boolean",
|
||||
description: "Allow unresolved TBD projects during commit.",
|
||||
},
|
||||
importTbdProjects: {
|
||||
type: "boolean",
|
||||
description: "Whether TBD projects should be imported.",
|
||||
},
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_import_batches",
|
||||
description: "List Dispo import batches with pagination and optional status filter via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: "string", description: "Optional batch status filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
},
|
||||
export_resources_csv: {
|
||||
allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES],
|
||||
},
|
||||
export_projects_csv: {
|
||||
allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES],
|
||||
},
|
||||
import_csv_data: {
|
||||
requiredPermissions: [PermissionKey.IMPORT_DATA],
|
||||
allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_import_batches: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
get_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
stage_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
validate_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
cancel_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_resources: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_projects: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_assignments: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_vacations: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_unresolved_records: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
resolve_dispo_staged_record: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
commit_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_dispo_import_batch",
|
||||
description: "Get one Dispo import batch including staged record counters via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Import batch ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "stage_dispo_import_batch",
|
||||
description: "Stage a Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
planningWorkbookPath: { type: "string", description: "Filesystem path to the planning workbook." },
|
||||
referenceWorkbookPath: { type: "string", description: "Filesystem path to the reference workbook." },
|
||||
chargeabilityWorkbookPath: { type: "string", description: "Filesystem path to the chargeability workbook." },
|
||||
costWorkbookPath: { type: "string", description: "Optional filesystem path to the cost workbook." },
|
||||
rosterWorkbookPath: { type: "string", description: "Optional filesystem path to the roster workbook." },
|
||||
notes: { type: "string", description: "Optional import notes." },
|
||||
},
|
||||
required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "validate_dispo_import_batch",
|
||||
description: "Validate a Dispo import batch readiness check via the real dispo router without committing anything. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
planningWorkbookPath: { type: "string", description: "Filesystem path to the planning workbook." },
|
||||
referenceWorkbookPath: { type: "string", description: "Filesystem path to the reference workbook." },
|
||||
chargeabilityWorkbookPath: { type: "string", description: "Filesystem path to the chargeability workbook." },
|
||||
costWorkbookPath: { type: "string", description: "Optional filesystem path to the cost workbook." },
|
||||
rosterWorkbookPath: { type: "string", description: "Optional filesystem path to the roster workbook." },
|
||||
importBatchId: { type: "string", description: "Optional existing staged import batch ID." },
|
||||
notes: { type: "string", description: "Optional import notes." },
|
||||
},
|
||||
required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "cancel_dispo_import_batch",
|
||||
description: "Cancel a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Import batch ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_resources",
|
||||
description: "List staged Dispo resources for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
status: { type: "string", description: "Optional staged record status filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_projects",
|
||||
description: "List staged Dispo projects for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
status: { type: "string", description: "Optional staged record status filter." },
|
||||
isTbd: { type: "boolean", description: "Optional TBD-project filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_assignments",
|
||||
description: "List staged Dispo assignments for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
status: { type: "string", description: "Optional staged record status filter." },
|
||||
resourceExternalId: { type: "string", description: "Optional resource external ID filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_vacations",
|
||||
description: "List staged Dispo vacations for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
resourceExternalId: { type: "string", description: "Optional resource external ID filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_dispo_staged_unresolved_records",
|
||||
description: "List staged unresolved Dispo records for one import batch via the real dispo router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
recordType: { type: "string", description: "Optional unresolved record type filter." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "resolve_dispo_staged_record",
|
||||
description: "Resolve one staged Dispo record via the real dispo router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Staged record ID." },
|
||||
recordType: { type: "string", description: "Staged record type." },
|
||||
action: { type: "string", enum: ["APPROVE", "REJECT", "SKIP"], description: "Resolution action." },
|
||||
},
|
||||
required: ["id", "recordType", "action"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "commit_dispo_import_batch",
|
||||
description: "Commit a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importBatchId: { type: "string", description: "Import batch ID." },
|
||||
allowTbdUnresolved: { type: "boolean", description: "Allow unresolved TBD projects during commit." },
|
||||
importTbdProjects: { type: "boolean", description: "Whether TBD projects should be imported." },
|
||||
},
|
||||
required: ["importBatchId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {
|
||||
export_resources_csv: {
|
||||
allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES],
|
||||
},
|
||||
export_projects_csv: {
|
||||
allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES],
|
||||
},
|
||||
import_csv_data: {
|
||||
requiredPermissions: [PermissionKey.IMPORT_DATA],
|
||||
allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_import_batches: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
get_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
stage_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
validate_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
cancel_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_resources: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_projects: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_assignments: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_vacations: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
list_dispo_staged_unresolved_records: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
resolve_dispo_staged_record: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
commit_dispo_import_batch: {
|
||||
allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
function clampLimit(limit: number | undefined): number | undefined {
|
||||
return limit !== undefined ? Math.min(Math.max(limit, 1), 200) : undefined;
|
||||
@@ -444,10 +522,7 @@ export function createImportExportDispoExecutors(
|
||||
});
|
||||
},
|
||||
|
||||
async get_dispo_import_batch(
|
||||
params: { id: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async get_dispo_import_batch(params: { id: string }, ctx: ToolContext) {
|
||||
const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx));
|
||||
try {
|
||||
return await caller.getImportBatch({ id: params.id });
|
||||
@@ -476,9 +551,13 @@ export function createImportExportDispoExecutors(
|
||||
chargeabilityWorkbookPath: params.chargeabilityWorkbookPath,
|
||||
planningWorkbookPath: params.planningWorkbookPath,
|
||||
referenceWorkbookPath: params.referenceWorkbookPath,
|
||||
...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}),
|
||||
...(params.costWorkbookPath !== undefined
|
||||
? { costWorkbookPath: params.costWorkbookPath }
|
||||
: {}),
|
||||
...(params.notes !== undefined ? { notes: params.notes } : {}),
|
||||
...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}),
|
||||
...(params.rosterWorkbookPath !== undefined
|
||||
? { rosterWorkbookPath: params.rosterWorkbookPath }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -499,23 +578,29 @@ export function createImportExportDispoExecutors(
|
||||
chargeabilityWorkbookPath: params.chargeabilityWorkbookPath,
|
||||
planningWorkbookPath: params.planningWorkbookPath,
|
||||
referenceWorkbookPath: params.referenceWorkbookPath,
|
||||
...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}),
|
||||
...(params.costWorkbookPath !== undefined
|
||||
? { costWorkbookPath: params.costWorkbookPath }
|
||||
: {}),
|
||||
...(params.importBatchId !== undefined ? { importBatchId: params.importBatchId } : {}),
|
||||
...(params.notes !== undefined ? { notes: params.notes } : {}),
|
||||
...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}),
|
||||
...(params.rosterWorkbookPath !== undefined
|
||||
? { rosterWorkbookPath: params.rosterWorkbookPath }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
|
||||
async cancel_dispo_import_batch(
|
||||
params: { id: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async cancel_dispo_import_batch(params: { id: string }, ctx: ToolContext) {
|
||||
const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.cancelImportBatch({ id: params.id });
|
||||
},
|
||||
|
||||
async list_dispo_staged_resources(
|
||||
params: { importBatchId: string; status?: StagedRecordStatus; limit?: number; cursor?: string },
|
||||
params: {
|
||||
importBatchId: string;
|
||||
status?: StagedRecordStatus;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const limit = clampLimit(params.limit);
|
||||
@@ -529,7 +614,13 @@ export function createImportExportDispoExecutors(
|
||||
},
|
||||
|
||||
async list_dispo_staged_projects(
|
||||
params: { importBatchId: string; status?: StagedRecordStatus; isTbd?: boolean; limit?: number; cursor?: string },
|
||||
params: {
|
||||
importBatchId: string;
|
||||
status?: StagedRecordStatus;
|
||||
isTbd?: boolean;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const limit = clampLimit(params.limit);
|
||||
@@ -544,7 +635,13 @@ export function createImportExportDispoExecutors(
|
||||
},
|
||||
|
||||
async list_dispo_staged_assignments(
|
||||
params: { importBatchId: string; status?: StagedRecordStatus; resourceExternalId?: string; limit?: number; cursor?: string },
|
||||
params: {
|
||||
importBatchId: string;
|
||||
status?: StagedRecordStatus;
|
||||
resourceExternalId?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const limit = clampLimit(params.limit);
|
||||
@@ -552,28 +649,42 @@ export function createImportExportDispoExecutors(
|
||||
return caller.listStagedAssignments({
|
||||
importBatchId: params.importBatchId,
|
||||
...(params.status !== undefined ? { status: params.status } : {}),
|
||||
...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}),
|
||||
...(params.resourceExternalId !== undefined
|
||||
? { resourceExternalId: params.resourceExternalId }
|
||||
: {}),
|
||||
...(params.cursor ? { cursor: params.cursor } : {}),
|
||||
...(limit !== undefined ? { limit } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async list_dispo_staged_vacations(
|
||||
params: { importBatchId: string; resourceExternalId?: string; limit?: number; cursor?: string },
|
||||
params: {
|
||||
importBatchId: string;
|
||||
resourceExternalId?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const limit = clampLimit(params.limit);
|
||||
const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.listStagedVacations({
|
||||
importBatchId: params.importBatchId,
|
||||
...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}),
|
||||
...(params.resourceExternalId !== undefined
|
||||
? { resourceExternalId: params.resourceExternalId }
|
||||
: {}),
|
||||
...(params.cursor ? { cursor: params.cursor } : {}),
|
||||
...(limit !== undefined ? { limit } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async list_dispo_staged_unresolved_records(
|
||||
params: { importBatchId: string; recordType?: DispoStagedRecordType; limit?: number; cursor?: string },
|
||||
params: {
|
||||
importBatchId: string;
|
||||
recordType?: DispoStagedRecordType;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const limit = clampLimit(params.limit);
|
||||
@@ -587,7 +698,11 @@ export function createImportExportDispoExecutors(
|
||||
},
|
||||
|
||||
async resolve_dispo_staged_record(
|
||||
params: { action: "APPROVE" | "REJECT" | "SKIP"; id: string; recordType: DispoStagedRecordType },
|
||||
params: {
|
||||
action: "APPROVE" | "REJECT" | "SKIP";
|
||||
id: string;
|
||||
recordType: DispoStagedRecordType;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx));
|
||||
@@ -605,8 +720,12 @@ export function createImportExportDispoExecutors(
|
||||
const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.commitImportBatch({
|
||||
importBatchId: params.importBatchId,
|
||||
...(params.allowTbdUnresolved !== undefined ? { allowTbdUnresolved: params.allowTbdUnresolved } : {}),
|
||||
...(params.importTbdProjects !== undefined ? { importTbdProjects: params.importTbdProjects } : {}),
|
||||
...(params.allowTbdUnresolved !== undefined
|
||||
? { allowTbdUnresolved: params.allowTbdUnresolved }
|
||||
: {}),
|
||||
...(params.importTbdProjects !== undefined
|
||||
? { importTbdProjects: params.importTbdProjects }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { EstimateStatus, SystemRole } from "@capakraken/shared";
|
||||
import { EstimateStatus, SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -17,21 +17,17 @@ type PlanningNavigationDeps = {
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
createClientCaller: (ctx: TRPCContext) => {
|
||||
list: (params: {
|
||||
isActive: boolean;
|
||||
search?: string;
|
||||
}) => Promise<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
code: string | null;
|
||||
_count: { projects: number };
|
||||
}>>;
|
||||
list: (params: { isActive: boolean; search?: string }) => Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
code: string | null;
|
||||
_count: { projects: number };
|
||||
}>
|
||||
>;
|
||||
};
|
||||
createOrgUnitCaller: (ctx: TRPCContext) => {
|
||||
list: (params: {
|
||||
isActive: boolean;
|
||||
level?: number;
|
||||
}) => Promise<Array<{ id: string }>>;
|
||||
list: (params: { isActive: boolean; level?: number }) => Promise<Array<{ id: string }>>;
|
||||
getById: (params: { id: string }) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -71,127 +67,204 @@ type PlanningNavigationDeps = {
|
||||
parseIsoDate: (value: string, fieldName: string) => Date;
|
||||
};
|
||||
|
||||
export const planningNavigationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "search_estimates",
|
||||
description: "Search for estimates (cost/effort estimates) by project or name. Returns estimate name, status, version count.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectCode: { type: "string", description: "Project short code to filter by" },
|
||||
query: { type: "string", description: "Search term (matches estimate name)" },
|
||||
status: { type: "string", description: "Filter by status: DRAFT, IN_REVIEW, APPROVED, ARCHIVED" },
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_clients",
|
||||
description: "List clients/customers. Can search by name or code.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search term (matches name or code)" },
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_org_units",
|
||||
description: "List organizational units (departments, teams) with their hierarchy.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
level: { type: "integer", description: "Filter by org level (5, 6, or 7)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_my_timeline_entries_view",
|
||||
description: "Get the caller's own self-service timeline entries view for a date range using the real timeline self-service endpoint. Returns only data for the caller's linked resource.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
|
||||
resourceIds: { type: "array", items: { type: "string" }, description: "Optional filters are accepted but will be scoped to the caller's own linked resource." },
|
||||
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to narrow the caller's own timeline view." },
|
||||
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to narrow the caller's own timeline view." },
|
||||
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters. Self-service scoping still applies." },
|
||||
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs. Self-service scoping still applies." },
|
||||
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes. Self-service scoping still applies." },
|
||||
},
|
||||
required: ["startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_my_timeline_holiday_overlays",
|
||||
description: "Get the caller's own self-service holiday overlays for a date range using the real timeline self-service endpoint. Returns only holiday overlays for the caller's linked resource.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
|
||||
resourceIds: { type: "array", items: { type: "string" }, description: "Optional filters are accepted but will be scoped to the caller's own linked resource." },
|
||||
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to narrow the caller's own holiday overlay view." },
|
||||
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to narrow the caller's own holiday overlay view." },
|
||||
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters. Self-service scoping still applies." },
|
||||
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs. Self-service scoping still applies." },
|
||||
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes. Self-service scoping still applies." },
|
||||
},
|
||||
required: ["startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "navigate_to_page",
|
||||
description: "Navigate the user to a specific page in CapaKraken, optionally with filters. Use this when the user wants to see data on a specific page (e.g. 'show me on the timeline', 'open the resources page').",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
page: {
|
||||
type: "string",
|
||||
description: "Page name: timeline, dashboard, resources, projects, allocations, staffing, estimates, vacations, my-vacations, roles, skills-analytics, chargeability, computation-graph",
|
||||
export const planningNavigationToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "search_estimates",
|
||||
description:
|
||||
"Search for estimates (cost/effort estimates) by project or name. Returns estimate name, status, version count.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectCode: { type: "string", description: "Project short code to filter by" },
|
||||
query: { type: "string", description: "Search term (matches estimate name)" },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Filter by status: DRAFT, IN_REVIEW, APPROVED, ARCHIVED",
|
||||
},
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
eids: { type: "string", description: "Comma-separated employee IDs to filter (for timeline)" },
|
||||
chapters: { type: "string", description: "Comma-separated chapters to filter (for timeline)" },
|
||||
projectIds: { type: "string", description: "Comma-separated project IDs to filter (for timeline)" },
|
||||
clientIds: { type: "string", description: "Comma-separated client IDs to filter (for timeline)" },
|
||||
countryCodes: { type: "string", description: "Comma-separated country codes to filter (e.g. 'ES,DE' for Spain and Germany, for timeline)" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD (for timeline)" },
|
||||
days: { type: "integer", description: "Number of days to show (for timeline)" },
|
||||
},
|
||||
required: ["page"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_clients",
|
||||
description: "List clients/customers. Can search by name or code.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search term (matches name or code)" },
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_org_units",
|
||||
description: "List organizational units (departments, teams) with their hierarchy.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
level: { type: "integer", description: "Filter by org level (5, 6, or 7)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_my_timeline_entries_view",
|
||||
description:
|
||||
"Get the caller's own self-service timeline entries view for a date range using the real timeline self-service endpoint. Returns only data for the caller's linked resource.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
|
||||
resourceIds: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description:
|
||||
"Optional filters are accepted but will be scoped to the caller's own linked resource.",
|
||||
},
|
||||
projectIds: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional project IDs to narrow the caller's own timeline view.",
|
||||
},
|
||||
clientIds: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional client IDs to narrow the caller's own timeline view.",
|
||||
},
|
||||
chapters: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional chapter filters. Self-service scoping still applies.",
|
||||
},
|
||||
eids: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional employee IDs. Self-service scoping still applies.",
|
||||
},
|
||||
countryCodes: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional country codes. Self-service scoping still applies.",
|
||||
},
|
||||
},
|
||||
required: ["startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_my_timeline_holiday_overlays",
|
||||
description:
|
||||
"Get the caller's own self-service holiday overlays for a date range using the real timeline self-service endpoint. Returns only holiday overlays for the caller's linked resource.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
|
||||
resourceIds: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description:
|
||||
"Optional filters are accepted but will be scoped to the caller's own linked resource.",
|
||||
},
|
||||
projectIds: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional project IDs to narrow the caller's own holiday overlay view.",
|
||||
},
|
||||
clientIds: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional client IDs to narrow the caller's own holiday overlay view.",
|
||||
},
|
||||
chapters: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional chapter filters. Self-service scoping still applies.",
|
||||
},
|
||||
eids: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional employee IDs. Self-service scoping still applies.",
|
||||
},
|
||||
countryCodes: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional country codes. Self-service scoping still applies.",
|
||||
},
|
||||
},
|
||||
required: ["startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "navigate_to_page",
|
||||
description:
|
||||
"Navigate the user to a specific page in Nexus, optionally with filters. Use this when the user wants to see data on a specific page (e.g. 'show me on the timeline', 'open the resources page').",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
page: {
|
||||
type: "string",
|
||||
description:
|
||||
"Page name: timeline, dashboard, resources, projects, allocations, staffing, estimates, vacations, my-vacations, roles, skills-analytics, chargeability, computation-graph",
|
||||
},
|
||||
eids: {
|
||||
type: "string",
|
||||
description: "Comma-separated employee IDs to filter (for timeline)",
|
||||
},
|
||||
chapters: {
|
||||
type: "string",
|
||||
description: "Comma-separated chapters to filter (for timeline)",
|
||||
},
|
||||
projectIds: {
|
||||
type: "string",
|
||||
description: "Comma-separated project IDs to filter (for timeline)",
|
||||
},
|
||||
clientIds: {
|
||||
type: "string",
|
||||
description: "Comma-separated client IDs to filter (for timeline)",
|
||||
},
|
||||
countryCodes: {
|
||||
type: "string",
|
||||
description:
|
||||
"Comma-separated country codes to filter (e.g. 'ES,DE' for Spain and Germany, for timeline)",
|
||||
},
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD (for timeline)" },
|
||||
days: { type: "integer", description: "Number of days to show (for timeline)" },
|
||||
},
|
||||
required: ["page"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
search_estimates: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
list_clients: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
list_org_units: {
|
||||
requiresResourceOverview: true,
|
||||
},
|
||||
},
|
||||
], {
|
||||
search_estimates: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
list_clients: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
list_org_units: {
|
||||
requiresResourceOverview: true,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createPlanningNavigationExecutors(
|
||||
deps: PlanningNavigationDeps,
|
||||
@@ -223,10 +296,7 @@ export function createPlanningNavigationExecutors(
|
||||
});
|
||||
},
|
||||
|
||||
async list_clients(
|
||||
params: { query?: string; limit?: number },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async list_clients(params: { query?: string; limit?: number }, ctx: ToolContext) {
|
||||
const limit = Math.min(params.limit ?? 20, 50);
|
||||
const caller = deps.createClientCaller(deps.createScopedCallerContext(ctx));
|
||||
const clients = await caller.list({
|
||||
@@ -242,10 +312,7 @@ export function createPlanningNavigationExecutors(
|
||||
}));
|
||||
},
|
||||
|
||||
async list_org_units(
|
||||
params: { level?: number },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async list_org_units(params: { level?: number }, ctx: ToolContext) {
|
||||
const caller = deps.createOrgUnitCaller(deps.createScopedCallerContext(ctx));
|
||||
const units = await caller.list({
|
||||
isActive: true,
|
||||
@@ -345,7 +412,9 @@ export function createPlanningNavigationExecutors(
|
||||
};
|
||||
const path = pageMap[params.page];
|
||||
if (!path) {
|
||||
return { error: `Unknown page: ${params.page}. Available: ${Object.keys(pageMap).join(", ")}` };
|
||||
return {
|
||||
error: `Unknown page: ${params.page}. Available: ${Object.keys(pageMap).join(", ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
const queryParts: string[] = [];
|
||||
@@ -353,7 +422,8 @@ export function createPlanningNavigationExecutors(
|
||||
if (params.chapters) queryParts.push(`chapters=${encodeURIComponent(params.chapters)}`);
|
||||
if (params.projectIds) queryParts.push(`projectIds=${encodeURIComponent(params.projectIds)}`);
|
||||
if (params.clientIds) queryParts.push(`clientIds=${encodeURIComponent(params.clientIds)}`);
|
||||
if (params.countryCodes) queryParts.push(`countryCodes=${encodeURIComponent(params.countryCodes)}`);
|
||||
if (params.countryCodes)
|
||||
queryParts.push(`countryCodes=${encodeURIComponent(params.countryCodes)}`);
|
||||
if (params.startDate) queryParts.push(`startDate=${encodeURIComponent(params.startDate)}`);
|
||||
if (params.days) queryParts.push(`days=${params.days}`);
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { CreateProjectSchema, PermissionKey, type ProjectStatus, UpdateProjectSchema } from "@capakraken/shared";
|
||||
import {
|
||||
CreateProjectSchema,
|
||||
PermissionKey,
|
||||
type ProjectStatus,
|
||||
UpdateProjectSchema,
|
||||
} from "@nexus/shared";
|
||||
import { fmtEur } from "../../lib/format-utils.js";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
@@ -50,14 +55,10 @@ type ProjectToolsDeps = {
|
||||
removeCover: (params: { projectId: string }) => Promise<unknown>;
|
||||
};
|
||||
createBlueprintCaller: (ctx: TRPCContext) => {
|
||||
resolveByIdentifier: (params: {
|
||||
identifier: string;
|
||||
}) => Promise<ResolvedReference>;
|
||||
resolveByIdentifier: (params: { identifier: string }) => Promise<ResolvedReference>;
|
||||
};
|
||||
createClientCaller: (ctx: TRPCContext) => {
|
||||
resolveByIdentifier: (params: {
|
||||
identifier: string;
|
||||
}) => Promise<ResolvedReference>;
|
||||
resolveByIdentifier: (params: { identifier: string }) => Promise<ResolvedReference>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
resolveProjectIdentifier: (
|
||||
@@ -72,13 +73,8 @@ type ProjectToolsDeps = {
|
||||
resolve: () => Promise<T>,
|
||||
notFoundMessage: string,
|
||||
) => Promise<T | AssistantToolErrorResult>;
|
||||
toAssistantNotFoundError: (
|
||||
error: unknown,
|
||||
message: string,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantProjectMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantNotFoundError: (error: unknown, message: string) => AssistantToolErrorResult | null;
|
||||
toAssistantProjectMutationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantProjectCreationError: (
|
||||
error: unknown,
|
||||
shortCode: string,
|
||||
@@ -99,10 +95,7 @@ async function resolveOptionalReferenceId(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resolved = await deps.resolveEntityOrAssistantError(
|
||||
resolveReference,
|
||||
notFoundMessage,
|
||||
);
|
||||
const resolved = await deps.resolveEntityOrAssistantError(resolveReference, notFoundMessage);
|
||||
if ("error" in resolved) {
|
||||
return resolved;
|
||||
}
|
||||
@@ -119,7 +112,10 @@ export const projectReadToolDefinitions: ToolDef[] = [
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search term (matches name, shortCode)" },
|
||||
status: { type: "string", description: "Filter by status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED" },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Filter by status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED",
|
||||
},
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
},
|
||||
@@ -129,7 +125,8 @@ export const projectReadToolDefinitions: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_project",
|
||||
description: "Get detailed information about a single project by ID or short code, including top allocations.",
|
||||
description:
|
||||
"Get detailed information about a single project by ID or short code, including top allocations.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -146,16 +143,27 @@ export const projectMutationToolDefinitions: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_project",
|
||||
description: "Update a project's details. Requires manageProjects permission. Always confirm with the user before calling this.",
|
||||
description:
|
||||
"Update a project's details. Requires manageProjects permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Project ID, short code, or project name" },
|
||||
name: { type: "string", description: "New project name" },
|
||||
budgetCents: { type: "integer", description: "New budget in cents (e.g. 10000000 = 100,000 EUR)" },
|
||||
budgetCents: {
|
||||
type: "integer",
|
||||
description: "New budget in cents (e.g. 10000000 = 100,000 EUR)",
|
||||
},
|
||||
winProbability: { type: "integer", description: "Win probability 0-100" },
|
||||
status: { type: "string", description: "New status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED" },
|
||||
responsiblePerson: { type: "string", description: "Name of the responsible person. Must match an existing resource's display name (case-insensitive search)." },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "New status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED",
|
||||
},
|
||||
responsiblePerson: {
|
||||
type: "string",
|
||||
description:
|
||||
"Name of the responsible person. Must match an existing resource's display name (case-insensitive search).",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
@@ -165,25 +173,60 @@ export const projectMutationToolDefinitions: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_project",
|
||||
description: "Create a new project. Requires manageProjects permission. Always confirm with the user before calling this. The project is created in DRAFT status by default.",
|
||||
description:
|
||||
"Create a new project. Requires manageProjects permission. Always confirm with the user before calling this. The project is created in DRAFT status by default.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
shortCode: { type: "string", description: "Unique project code, uppercase alphanumeric with hyphens/underscores (e.g. 'PROJ-001')" },
|
||||
shortCode: {
|
||||
type: "string",
|
||||
description:
|
||||
"Unique project code, uppercase alphanumeric with hyphens/underscores (e.g. 'PROJ-001')",
|
||||
},
|
||||
name: { type: "string", description: "Project name" },
|
||||
orderType: { type: "string", description: "Order type: BD, CHARGEABLE, INTERNAL, OVERHEAD" },
|
||||
allocationType: { type: "string", description: "Allocation type: INT or EXT. Default: INT" },
|
||||
budgetCents: { type: "integer", description: "Budget in cents (e.g. 10000000 = 100,000 EUR)" },
|
||||
orderType: {
|
||||
type: "string",
|
||||
description: "Order type: BD, CHARGEABLE, INTERNAL, OVERHEAD",
|
||||
},
|
||||
allocationType: {
|
||||
type: "string",
|
||||
description: "Allocation type: INT or EXT. Default: INT",
|
||||
},
|
||||
budgetCents: {
|
||||
type: "integer",
|
||||
description: "Budget in cents (e.g. 10000000 = 100,000 EUR)",
|
||||
},
|
||||
startDate: { type: "string", description: "Start date (YYYY-MM-DD)" },
|
||||
endDate: { type: "string", description: "End date (YYYY-MM-DD)" },
|
||||
winProbability: { type: "integer", description: "Win probability 0-100. Default: 100" },
|
||||
status: { type: "string", description: "Initial status: DRAFT, ACTIVE, ON_HOLD. Default: DRAFT" },
|
||||
responsiblePerson: { type: "string", description: "Name of the responsible person. Must match an existing resource's display name (case-insensitive search)." },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Initial status: DRAFT, ACTIVE, ON_HOLD. Default: DRAFT",
|
||||
},
|
||||
responsiblePerson: {
|
||||
type: "string",
|
||||
description:
|
||||
"Name of the responsible person. Must match an existing resource's display name (case-insensitive search).",
|
||||
},
|
||||
color: { type: "string", description: "Hex color (e.g. '#3b82f6')" },
|
||||
blueprintName: { type: "string", description: "Blueprint name to look up and attach (partial match)" },
|
||||
clientName: { type: "string", description: "Client name to look up and attach (partial match)" },
|
||||
blueprintName: {
|
||||
type: "string",
|
||||
description: "Blueprint name to look up and attach (partial match)",
|
||||
},
|
||||
clientName: {
|
||||
type: "string",
|
||||
description: "Client name to look up and attach (partial match)",
|
||||
},
|
||||
},
|
||||
required: ["shortCode", "name", "orderType", "budgetCents", "startDate", "endDate", "responsiblePerson"],
|
||||
required: [
|
||||
"shortCode",
|
||||
"name",
|
||||
"orderType",
|
||||
"budgetCents",
|
||||
"startDate",
|
||||
"endDate",
|
||||
"responsiblePerson",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -191,7 +234,8 @@ export const projectMutationToolDefinitions: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_project",
|
||||
description: "Delete a project. Only DRAFT projects can be deleted. Requires manageProjects permission. Always confirm first.",
|
||||
description:
|
||||
"Delete a project. Only DRAFT projects can be deleted. Requires manageProjects permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -205,12 +249,17 @@ export const projectMutationToolDefinitions: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "generate_project_cover",
|
||||
description: "Generate an AI cover art image for a project. Uses the configured image provider (DALL-E or Google Gemini). The image will be stored as the project's cover. Requires manageProjects permission.",
|
||||
description:
|
||||
"Generate an AI cover art image for a project. Uses the configured image provider (DALL-E or Google Gemini). The image will be stored as the project's cover. Requires manageProjects permission.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID" },
|
||||
prompt: { type: "string", description: "Optional custom prompt for the AI image generation (e.g. 'futuristic car in neon cityscape'). If not provided, a default automotive/CGI prompt is used based on the project name." },
|
||||
prompt: {
|
||||
type: "string",
|
||||
description:
|
||||
"Optional custom prompt for the AI image generation (e.g. 'futuristic car in neon cityscape'). If not provided, a default automotive/CGI prompt is used based on the project name.",
|
||||
},
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
@@ -232,9 +281,7 @@ export const projectMutationToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export function createProjectExecutors(
|
||||
deps: ProjectToolsDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
export function createProjectExecutors(deps: ProjectToolsDeps): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async search_projects(
|
||||
params: { query?: string; status?: string; limit?: number },
|
||||
@@ -440,10 +487,7 @@ export function createProjectExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async generate_project_cover(
|
||||
params: { projectId: string; prompt?: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async generate_project_cover(params: { projectId: string; prompt?: string }, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
const caller = deps.createProjectCaller(deps.createScopedCallerContext(ctx));
|
||||
const project = await deps.resolveProjectIdentifier(ctx, params.projectId);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateResourceSchema, PermissionKey, UpdateResourceSchema } from "@capakraken/shared";
|
||||
import { CreateResourceSchema, PermissionKey, UpdateResourceSchema } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -37,10 +37,7 @@ type ResourceToolsDeps = {
|
||||
}) => Promise<unknown>;
|
||||
getByIdentifierDetail: (params: { identifier: string }) => Promise<unknown>;
|
||||
create: (params: ParsedCreateResourceInput) => Promise<ResourceRecord>;
|
||||
update: (params: {
|
||||
id: string;
|
||||
data: ParsedUpdateResourceInput;
|
||||
}) => Promise<ResourceRecord>;
|
||||
update: (params: { id: string; data: ParsedUpdateResourceInput }) => Promise<ResourceRecord>;
|
||||
deactivate: (params: { id: string }) => Promise<unknown>;
|
||||
};
|
||||
createRoleCaller: (ctx: TRPCContext) => {
|
||||
@@ -61,128 +58,167 @@ type ResourceToolsDeps = {
|
||||
resolve: () => Promise<T>,
|
||||
notFoundMessage: string,
|
||||
) => Promise<T | AssistantToolErrorResult>;
|
||||
toAssistantResourceMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantResourceCreationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantResourceMutationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantResourceCreationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const resourceReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "search_resources",
|
||||
description: "Search for resources (employees) by name, employee ID, chapter, country, metro city, org unit, or role. Resource overview access required. Returns a list of matching resources with key details.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search term (matches displayName, eid, chapter)" },
|
||||
country: { type: "string", description: "Filter by country name or code (e.g. 'Spain', 'ES', 'Deutschland', 'DE')" },
|
||||
metroCity: { type: "string", description: "Filter by metro city name (e.g. 'Madrid', 'München')" },
|
||||
orgUnit: { type: "string", description: "Filter by org unit name (partial match)" },
|
||||
roleName: { type: "string", description: "Filter by role name (partial match)" },
|
||||
isActive: { type: "boolean", description: "Filter by active status. Default: true" },
|
||||
limit: { type: "integer", description: "Max results. Default: 50" },
|
||||
export const resourceReadToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "search_resources",
|
||||
description:
|
||||
"Search for resources (employees) by name, employee ID, chapter, country, metro city, org unit, or role. Resource overview access required. Returns a list of matching resources with key details.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search term (matches displayName, eid, chapter)",
|
||||
},
|
||||
country: {
|
||||
type: "string",
|
||||
description:
|
||||
"Filter by country name or code (e.g. 'Spain', 'ES', 'Deutschland', 'DE')",
|
||||
},
|
||||
metroCity: {
|
||||
type: "string",
|
||||
description: "Filter by metro city name (e.g. 'Madrid', 'München')",
|
||||
},
|
||||
orgUnit: { type: "string", description: "Filter by org unit name (partial match)" },
|
||||
roleName: { type: "string", description: "Filter by role name (partial match)" },
|
||||
isActive: { type: "boolean", description: "Filter by active status. Default: true" },
|
||||
limit: { type: "integer", description: "Max results. Default: 50" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_resource",
|
||||
description: "Get detailed information about a single resource by ID, employee ID (eid), or name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Resource ID, employee ID (eid like EMP-001), or display name" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_resource",
|
||||
description:
|
||||
"Get detailed information about a single resource by ID, employee ID (eid), or name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: {
|
||||
type: "string",
|
||||
description: "Resource ID, employee ID (eid like EMP-001), or display name",
|
||||
},
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
search_resources: {
|
||||
requiresResourceOverview: true,
|
||||
},
|
||||
},
|
||||
], {
|
||||
search_resources: {
|
||||
requiresResourceOverview: true,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export const resourceMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_resource",
|
||||
description: "Update a resource's details. Requires manageResources permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
displayName: { type: "string", description: "New display name" },
|
||||
fte: { type: "number", description: "New FTE (0.0-1.0)" },
|
||||
lcrCents: { type: "integer", description: "New LCR in cents (e.g. 8500 = 85.00 EUR/h)" },
|
||||
chapter: { type: "string", description: "New chapter" },
|
||||
chargeabilityTarget: { type: "number", description: "New chargeability target (0-100)" },
|
||||
export const resourceMutationToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_resource",
|
||||
description:
|
||||
"Update a resource's details. Requires manageResources permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
displayName: { type: "string", description: "New display name" },
|
||||
fte: { type: "number", description: "New FTE (0.0-1.0)" },
|
||||
lcrCents: {
|
||||
type: "integer",
|
||||
description: "New LCR in cents (e.g. 8500 = 85.00 EUR/h)",
|
||||
},
|
||||
chapter: { type: "string", description: "New chapter" },
|
||||
chargeabilityTarget: {
|
||||
type: "number",
|
||||
description: "New chargeability target (0-100)",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_resource",
|
||||
description: "Create a new resource/employee. Requires manageResources permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
eid: { type: "string", description: "Employee ID (e.g. EMP-001)" },
|
||||
displayName: { type: "string", description: "Full name" },
|
||||
email: { type: "string", description: "Email address" },
|
||||
fte: { type: "number", description: "Full-time equivalent 0.0-1.0. Default: 1.0" },
|
||||
lcrCents: { type: "integer", description: "Labor cost rate in cents/hour (e.g. 8500 = 85.00 EUR/h)" },
|
||||
ucrCents: { type: "integer", description: "Utilization cost rate in cents/hour (optional; defaults to 70% of LCR)" },
|
||||
chapter: { type: "string", description: "Chapter/department" },
|
||||
chargeabilityTarget: { type: "number", description: "Target utilization percentage 0-100. Default: 80" },
|
||||
roleName: { type: "string", description: "Role name to assign (e.g. 'Designer')" },
|
||||
countryCode: { type: "string", description: "Country code or name (e.g. 'DE', 'Germany')" },
|
||||
orgUnitName: { type: "string", description: "Organizational unit name" },
|
||||
postalCode: { type: "string", description: "Postal code. If provided without federalState, state may be inferred." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_resource",
|
||||
description:
|
||||
"Create a new resource/employee. Requires manageResources permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
eid: { type: "string", description: "Employee ID (e.g. EMP-001)" },
|
||||
displayName: { type: "string", description: "Full name" },
|
||||
email: { type: "string", description: "Email address" },
|
||||
fte: { type: "number", description: "Full-time equivalent 0.0-1.0. Default: 1.0" },
|
||||
lcrCents: {
|
||||
type: "integer",
|
||||
description: "Labor cost rate in cents/hour (e.g. 8500 = 85.00 EUR/h)",
|
||||
},
|
||||
ucrCents: {
|
||||
type: "integer",
|
||||
description: "Utilization cost rate in cents/hour (optional; defaults to 70% of LCR)",
|
||||
},
|
||||
chapter: { type: "string", description: "Chapter/department" },
|
||||
chargeabilityTarget: {
|
||||
type: "number",
|
||||
description: "Target utilization percentage 0-100. Default: 80",
|
||||
},
|
||||
roleName: { type: "string", description: "Role name to assign (e.g. 'Designer')" },
|
||||
countryCode: {
|
||||
type: "string",
|
||||
description: "Country code or name (e.g. 'DE', 'Germany')",
|
||||
},
|
||||
orgUnitName: { type: "string", description: "Organizational unit name" },
|
||||
postalCode: {
|
||||
type: "string",
|
||||
description: "Postal code. If provided without federalState, state may be inferred.",
|
||||
},
|
||||
},
|
||||
required: ["eid", "displayName", "lcrCents"],
|
||||
},
|
||||
required: ["eid", "displayName", "lcrCents"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "deactivate_resource",
|
||||
description: "Deactivate a resource (soft delete). Requires manageResources permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "deactivate_resource",
|
||||
description:
|
||||
"Deactivate a resource (soft delete). Requires manageResources permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
update_resource: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_RESOURCES],
|
||||
},
|
||||
create_resource: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_RESOURCES],
|
||||
},
|
||||
deactivate_resource: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_RESOURCES],
|
||||
},
|
||||
},
|
||||
], {
|
||||
update_resource: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_RESOURCES],
|
||||
},
|
||||
create_resource: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_RESOURCES],
|
||||
},
|
||||
deactivate_resource: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_RESOURCES],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createResourceExecutors(
|
||||
deps: ResourceToolsDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
export function createResourceExecutors(deps: ResourceToolsDeps): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async search_resources(
|
||||
params: {
|
||||
@@ -373,10 +409,7 @@ export function createResourceExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async deactivate_resource(
|
||||
params: { identifier: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async deactivate_resource(params: { identifier: string }, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
||||
const resource = await deps.resolveResourceIdentifier(ctx, params.identifier);
|
||||
if ("error" in resource) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { CreateRoleSchema, PermissionKey, SystemRole, UpdateRoleSchema } from "@capakraken/shared";
|
||||
import { CreateRoleSchema, PermissionKey, SystemRole, UpdateRoleSchema } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -11,19 +11,18 @@ type ResolvedResource = {
|
||||
|
||||
type RolesAnalyticsDeps = {
|
||||
createRoleCaller: (ctx: TRPCContext) => {
|
||||
list: (params: Record<string, never>) => Promise<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
color?: string | null;
|
||||
}>>;
|
||||
list: (params: Record<string, never>) => Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
color?: string | null;
|
||||
}>
|
||||
>;
|
||||
create: (params: z.input<typeof CreateRoleSchema>) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
update: (params: {
|
||||
id: string;
|
||||
data: z.input<typeof UpdateRoleSchema>;
|
||||
}) => Promise<{
|
||||
update: (params: { id: string; data: z.input<typeof UpdateRoleSchema> }) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
@@ -34,20 +33,19 @@ type RolesAnalyticsDeps = {
|
||||
searchBySkills: (params: {
|
||||
rules: Array<{ skill: string; minProficiency: number }>;
|
||||
operator: "OR";
|
||||
}) => Promise<Array<{
|
||||
id: string;
|
||||
eid: string;
|
||||
displayName: string;
|
||||
chapter?: string | null;
|
||||
matchedSkills: Array<{
|
||||
skill: string;
|
||||
proficiency: number;
|
||||
}>;
|
||||
}>>;
|
||||
getChargeabilitySummary: (params: {
|
||||
resourceId: string;
|
||||
month: string;
|
||||
}) => Promise<unknown>;
|
||||
}) => Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
eid: string;
|
||||
displayName: string;
|
||||
chapter?: string | null;
|
||||
matchedSkills: Array<{
|
||||
skill: string;
|
||||
proficiency: number;
|
||||
}>;
|
||||
}>
|
||||
>;
|
||||
getChargeabilitySummary: (params: { resourceId: string; month: string }) => Promise<unknown>;
|
||||
};
|
||||
createDashboardCaller: (ctx: TRPCContext) => {
|
||||
getStatisticsDetail: () => Promise<unknown>;
|
||||
@@ -63,130 +61,145 @@ type RolesAnalyticsDeps = {
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const rolesAnalyticsReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_roles",
|
||||
description: "List all available roles with their colors.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "search_by_skill",
|
||||
description: "Find resources that have a specific skill. Controller/manager/admin access required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
skill: { type: "string", description: "Skill name to search for" },
|
||||
},
|
||||
required: ["skill"],
|
||||
export const rolesAnalyticsReadToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_roles",
|
||||
description: "List all available roles with their colors.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_statistics",
|
||||
description: "Get overview statistics: total resources, projects, active allocations, budget summary, projects by status, chapter breakdown.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_chargeability",
|
||||
description: "Get chargeability data for a resource in a given month: hours booked vs available, chargeability %, target comparison.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, eid, or name" },
|
||||
month: { type: "string", description: "Month in YYYY-MM format, e.g. 2026-03. Default: current month" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "search_by_skill",
|
||||
description:
|
||||
"Find resources that have a specific skill. Controller/manager/admin access required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
skill: { type: "string", description: "Skill name to search for" },
|
||||
},
|
||||
required: ["skill"],
|
||||
},
|
||||
required: ["resourceId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_statistics",
|
||||
description:
|
||||
"Get overview statistics: total resources, projects, active allocations, budget summary, projects by status, chapter breakdown.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_chargeability",
|
||||
description:
|
||||
"Get chargeability data for a resource in a given month: hours booked vs available, chargeability %, target comparison.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, eid, or name" },
|
||||
month: {
|
||||
type: "string",
|
||||
description: "Month in YYYY-MM format, e.g. 2026-03. Default: current month",
|
||||
},
|
||||
},
|
||||
required: ["resourceId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
list_roles: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
search_by_skill: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_statistics: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_chargeability: {
|
||||
requiresCostView: true,
|
||||
},
|
||||
},
|
||||
], {
|
||||
list_roles: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
search_by_skill: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_statistics: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_chargeability: {
|
||||
requiresCostView: true,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export const rolesAnalyticsMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_role",
|
||||
description: "Create a new role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Role name" },
|
||||
description: { type: "string", description: "Optional role description" },
|
||||
color: { type: "string", description: "Hex color (e.g. #3b82f6). Default: #6b7280" },
|
||||
export const rolesAnalyticsMutationToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_role",
|
||||
description:
|
||||
"Create a new role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Role name" },
|
||||
description: { type: "string", description: "Optional role description" },
|
||||
color: { type: "string", description: "Hex color (e.g. #3b82f6). Default: #6b7280" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_role",
|
||||
description: "Update a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Role ID" },
|
||||
name: { type: "string", description: "New name" },
|
||||
description: { type: "string", description: "New description" },
|
||||
color: { type: "string", description: "New hex color" },
|
||||
isActive: { type: "boolean", description: "Set active state" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_role",
|
||||
description:
|
||||
"Update a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Role ID" },
|
||||
name: { type: "string", description: "New name" },
|
||||
description: { type: "string", description: "New description" },
|
||||
color: { type: "string", description: "New hex color" },
|
||||
isActive: { type: "boolean", description: "Set active state" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_role",
|
||||
description: "Delete a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Role ID" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_role",
|
||||
description:
|
||||
"Delete a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Role ID" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
create_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ROLES],
|
||||
},
|
||||
update_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ROLES],
|
||||
},
|
||||
delete_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ROLES],
|
||||
},
|
||||
},
|
||||
], {
|
||||
create_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ROLES],
|
||||
},
|
||||
update_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ROLES],
|
||||
},
|
||||
delete_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ROLES],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createRolesAnalyticsExecutors(
|
||||
deps: RolesAnalyticsDeps,
|
||||
@@ -226,7 +239,8 @@ export function createRolesAnalyticsExecutors(
|
||||
|
||||
async get_chargeability(params: { resourceId: string; month?: string }, ctx: ToolContext) {
|
||||
const now = new Date();
|
||||
const month = params.month ?? `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
const month =
|
||||
params.month ?? `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
@@ -239,11 +253,14 @@ export function createRolesAnalyticsExecutors(
|
||||
});
|
||||
},
|
||||
|
||||
async create_role(params: {
|
||||
name: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
}, ctx: ToolContext) {
|
||||
async create_role(
|
||||
params: {
|
||||
name: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
let role;
|
||||
@@ -267,13 +284,16 @@ export function createRolesAnalyticsExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async update_role(params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
isActive?: boolean;
|
||||
}, ctx: ToolContext) {
|
||||
async update_role(
|
||||
params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
isActive?: boolean;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx));
|
||||
const data = UpdateRoleSchema.parse({
|
||||
...(params.name !== undefined ? { name: params.name } : {}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { PermissionKey, SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -76,81 +76,93 @@ type ScenarioRateAnalysisDeps = {
|
||||
fmtEur: (value: number) => string;
|
||||
};
|
||||
|
||||
export const scenarioRateAnalysisToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "lookup_rate",
|
||||
description: "Find the best matching rate card line for given criteria (client, chapter, management level, role, seniority).",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
clientId: { type: "string", description: "Client ID to find rate card for" },
|
||||
chapter: { type: "string", description: "Chapter to match" },
|
||||
managementLevelId: { type: "string", description: "Management level ID to match" },
|
||||
roleName: { type: "string", description: "Role name to match" },
|
||||
seniority: { type: "string", description: "Seniority level to match" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "simulate_scenario",
|
||||
description: "Run a read-only what-if staffing simulation for a project. Shows cost/hours/utilization impact of adding, removing, or changing resource assignments without persisting changes.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID" },
|
||||
changes: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
assignmentId: { type: "string", description: "Existing assignment ID to modify (omit for new)" },
|
||||
resourceId: { type: "string", description: "Resource ID" },
|
||||
roleId: { type: "string", description: "Role ID" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
hoursPerDay: { type: "number", description: "Hours per day" },
|
||||
remove: { type: "boolean", description: "Set true to remove an existing assignment" },
|
||||
},
|
||||
required: ["startDate", "endDate", "hoursPerDay"],
|
||||
},
|
||||
description: "Array of staffing changes to simulate",
|
||||
export const scenarioRateAnalysisToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "lookup_rate",
|
||||
description:
|
||||
"Find the best matching rate card line for given criteria (client, chapter, management level, role, seniority).",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
clientId: { type: "string", description: "Client ID to find rate card for" },
|
||||
chapter: { type: "string", description: "Chapter to match" },
|
||||
managementLevelId: { type: "string", description: "Management level ID to match" },
|
||||
roleName: { type: "string", description: "Role name to match" },
|
||||
seniority: { type: "string", description: "Seniority level to match" },
|
||||
},
|
||||
},
|
||||
required: ["projectId", "changes"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "generate_project_narrative",
|
||||
description: "Generate an AI-powered executive narrative for a project covering budget, staffing, timeline risk, and action items. Requires AI to be configured.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "simulate_scenario",
|
||||
description:
|
||||
"Run a read-only what-if staffing simulation for a project. Shows cost/hours/utilization impact of adding, removing, or changing resource assignments without persisting changes.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID" },
|
||||
changes: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
assignmentId: {
|
||||
type: "string",
|
||||
description: "Existing assignment ID to modify (omit for new)",
|
||||
},
|
||||
resourceId: { type: "string", description: "Resource ID" },
|
||||
roleId: { type: "string", description: "Role ID" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
hoursPerDay: { type: "number", description: "Hours per day" },
|
||||
remove: {
|
||||
type: "boolean",
|
||||
description: "Set true to remove an existing assignment",
|
||||
},
|
||||
},
|
||||
required: ["startDate", "endDate", "hoursPerDay"],
|
||||
},
|
||||
description: "Array of staffing changes to simulate",
|
||||
},
|
||||
},
|
||||
required: ["projectId", "changes"],
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "generate_project_narrative",
|
||||
description:
|
||||
"Generate an AI-powered executive narrative for a project covering budget, staffing, timeline risk, and action items. Requires AI to be configured.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID" },
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
lookup_rate: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
},
|
||||
simulate_scenario: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
generate_project_narrative: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
},
|
||||
], {
|
||||
lookup_rate: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
},
|
||||
simulate_scenario: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
generate_project_narrative: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createScenarioRateAnalysisExecutors(
|
||||
deps: ScenarioRateAnalysisDeps,
|
||||
@@ -176,7 +188,9 @@ export function createScenarioRateAnalysisExecutors(
|
||||
? {
|
||||
...result.bestMatch,
|
||||
costRate: deps.fmtEur(result.bestMatch.costRateCents),
|
||||
billRate: result.bestMatch.billRateCents ? deps.fmtEur(result.bestMatch.billRateCents) : null,
|
||||
billRate: result.bestMatch.billRateCents
|
||||
? deps.fmtEur(result.bestMatch.billRateCents)
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
alternatives: result.alternatives.map((alternative) => ({
|
||||
@@ -232,10 +246,7 @@ export function createScenarioRateAnalysisExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async generate_project_narrative(
|
||||
params: { projectId: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async generate_project_narrative(params: { projectId: string }, ctx: ToolContext) {
|
||||
const caller = deps.createInsightsCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.generateProjectNarrative({ projectId: params.projectId });
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import type { prisma } from "@capakraken/db";
|
||||
import type { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import type { prisma } from "@nexus/db";
|
||||
import type { PermissionKey, SystemRole } from "@nexus/shared";
|
||||
import type { z } from "zod";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AllocationStatus, PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { AllocationStatus, PermissionKey, SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -112,144 +112,157 @@ type StaffingDemandDeps = {
|
||||
parseIsoDate: (value: string, fieldName: string) => Date;
|
||||
parseOptionalIsoDate: (value: string | undefined, fieldName: string) => Date | undefined;
|
||||
fmtDate: (value: Date | null | undefined) => string | null;
|
||||
toAssistantDemandCreationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantDemandFillError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantDemandCreationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantDemandFillError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const staffingDemandReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_demands",
|
||||
description: "List staffing demand requirements for projects. Shows open positions that need to be filled.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Filter by project ID or short code" },
|
||||
status: { type: "string", description: "Filter by status: OPEN, PARTIALLY_FILLED, FILLED, CANCELLED" },
|
||||
limit: { type: "integer", description: "Max results. Default: 30" },
|
||||
export const staffingDemandReadToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_demands",
|
||||
description:
|
||||
"List staffing demand requirements for projects. Shows open positions that need to be filled.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Filter by project ID or short code" },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Filter by status: OPEN, PARTIALLY_FILLED, FILLED, CANCELLED",
|
||||
},
|
||||
limit: { type: "integer", description: "Max results. Default: 30" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "check_resource_availability",
|
||||
description: "Check if a resource is available in a given date range (no conflicting allocations or vacations).",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, eid, or name" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "check_resource_availability",
|
||||
description:
|
||||
"Check if a resource is available in a given date range (no conflicting allocations or vacations).",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, eid, or name" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
},
|
||||
required: ["resourceId", "startDate", "endDate"],
|
||||
},
|
||||
required: ["resourceId", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_staffing_suggestions",
|
||||
description: "Get AI-powered staffing suggestions for a project based on required skills, availability, and cost.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
roleName: { type: "string", description: "Role to find candidates for" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
limit: { type: "integer", description: "Max suggestions. Default: 5" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_staffing_suggestions",
|
||||
description:
|
||||
"Get AI-powered staffing suggestions for a project based on required skills, availability, and cost.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
roleName: { type: "string", description: "Role to find candidates for" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
limit: { type: "integer", description: "Max suggestions. Default: 5" },
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "find_capacity",
|
||||
description: "Find resources with available capacity in a date range.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
minHoursPerDay: { type: "number", description: "Minimum available hours/day. Default: 4" },
|
||||
roleName: { type: "string", description: "Filter by role name" },
|
||||
chapter: { type: "string", description: "Filter by chapter" },
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "find_capacity",
|
||||
description: "Find resources with available capacity in a date range.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
minHoursPerDay: {
|
||||
type: "number",
|
||||
description: "Minimum available hours/day. Default: 4",
|
||||
},
|
||||
roleName: { type: "string", description: "Filter by role name" },
|
||||
chapter: { type: "string", description: "Filter by chapter" },
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
required: ["startDate", "endDate"],
|
||||
},
|
||||
required: ["startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
list_demands: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
check_resource_availability: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
get_staffing_suggestions: {
|
||||
requiresPlanningRead: true,
|
||||
requiresCostView: true,
|
||||
},
|
||||
find_capacity: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
},
|
||||
], {
|
||||
list_demands: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
check_resource_availability: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
get_staffing_suggestions: {
|
||||
requiresPlanningRead: true,
|
||||
requiresCostView: true,
|
||||
},
|
||||
find_capacity: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export const staffingDemandMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_demand",
|
||||
description: "Create a staffing demand requirement on a project. Requires manageAllocations permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
roleName: { type: "string", description: "Role name for the demand" },
|
||||
headcount: { type: "integer", description: "Number of people needed. Default: 1" },
|
||||
hoursPerDay: { type: "number", description: "Hours per day required" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
export const staffingDemandMutationToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_demand",
|
||||
description:
|
||||
"Create a staffing demand requirement on a project. Requires manageAllocations permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
roleName: { type: "string", description: "Role name for the demand" },
|
||||
headcount: { type: "integer", description: "Number of people needed. Default: 1" },
|
||||
hoursPerDay: { type: "number", description: "Hours per day required" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
},
|
||||
required: ["projectId", "roleName", "hoursPerDay", "startDate", "endDate"],
|
||||
},
|
||||
required: ["projectId", "roleName", "hoursPerDay", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "fill_demand",
|
||||
description: "Fill/assign a resource to an open demand requirement. Requires manageAllocations permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
demandId: { type: "string", description: "Demand requirement ID" },
|
||||
resourceId: { type: "string", description: "Resource ID or name to assign" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "fill_demand",
|
||||
description:
|
||||
"Fill/assign a resource to an open demand requirement. Requires manageAllocations permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
demandId: { type: "string", description: "Demand requirement ID" },
|
||||
resourceId: { type: "string", description: "Resource ID or name to assign" },
|
||||
},
|
||||
required: ["demandId", "resourceId"],
|
||||
},
|
||||
required: ["demandId", "resourceId"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
create_demand: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
fill_demand: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
},
|
||||
], {
|
||||
create_demand: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
fill_demand: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createStaffingDemandExecutors(
|
||||
deps: StaffingDemandDeps,
|
||||
@@ -273,19 +286,21 @@ export function createStaffingDemandExecutors(
|
||||
...(params.status ? { status: params.status as AllocationStatus } : {}),
|
||||
});
|
||||
|
||||
return demands.map((demand) => ({
|
||||
id: demand.id,
|
||||
project: demand.project.name,
|
||||
projectCode: demand.project.shortCode,
|
||||
role: demand.roleEntity?.name ?? demand.role ?? "Unspecified",
|
||||
status: demand.status,
|
||||
headcount: demand.headcount,
|
||||
filled: demand.assignments.length,
|
||||
remaining: demand.headcount - demand.assignments.length,
|
||||
hoursPerDay: demand.hoursPerDay,
|
||||
start: deps.fmtDate(demand.startDate),
|
||||
end: deps.fmtDate(demand.endDate),
|
||||
})).slice(0, limit);
|
||||
return demands
|
||||
.map((demand) => ({
|
||||
id: demand.id,
|
||||
project: demand.project.name,
|
||||
projectCode: demand.project.shortCode,
|
||||
role: demand.roleEntity?.name ?? demand.role ?? "Unspecified",
|
||||
status: demand.status,
|
||||
headcount: demand.headcount,
|
||||
filled: demand.assignments.length,
|
||||
remaining: demand.headcount - demand.assignments.length,
|
||||
hoursPerDay: demand.hoursPerDay,
|
||||
start: deps.fmtDate(demand.startDate),
|
||||
end: deps.fmtDate(demand.endDate),
|
||||
}))
|
||||
.slice(0, limit);
|
||||
},
|
||||
|
||||
async create_demand(
|
||||
@@ -346,10 +361,7 @@ export function createStaffingDemandExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async fill_demand(
|
||||
params: { demandId: string; resourceId: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async fill_demand(params: { demandId: string; resourceId: string }, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
const allocationCaller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
|
||||
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
|
||||
@@ -371,9 +383,8 @@ export function createStaffingDemandExecutors(
|
||||
throw error;
|
||||
}
|
||||
|
||||
const roleName = result.demandRequirement.roleEntity?.name
|
||||
?? result.demandRequirement.role
|
||||
?? null;
|
||||
const roleName =
|
||||
result.demandRequirement.roleEntity?.name ?? result.demandRequirement.role ?? null;
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -22,9 +22,10 @@ type UserAdminDeps = {
|
||||
setPassword: (params: { userId: string; password: string }) => Promise<Record<string, unknown>>;
|
||||
updateRole: (params: { id: string; systemRole: SystemRole }) => Promise<UserRecord>;
|
||||
updateName: (params: { id: string; name: string }) => Promise<UserRecord>;
|
||||
linkResource: (
|
||||
params: { userId: string; resourceId: string | null },
|
||||
) => Promise<Record<string, unknown>>;
|
||||
linkResource: (params: {
|
||||
userId: string;
|
||||
resourceId: string | null;
|
||||
}) => Promise<Record<string, unknown>>;
|
||||
autoLinkAllByEmail: () => Promise<Record<string, unknown> & { linked: number }>;
|
||||
setPermissions: (params: {
|
||||
userId: string;
|
||||
@@ -43,214 +44,228 @@ type UserAdminDeps = {
|
||||
error: unknown,
|
||||
mode?: "create" | "password",
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantUserResourceLinkError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantUserResourceLinkError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const userAdminToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_users",
|
||||
description: "List all system users via the admin user router, including role and MFA state. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: "integer", description: "Max results. Default: 50" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_user",
|
||||
description: "Create a new system user and auto-link a matching resource by email when possible. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
email: { type: "string", description: "User email address." },
|
||||
name: { type: "string", description: "Display name." },
|
||||
systemRole: { type: "string", enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"], description: "Initial system role." },
|
||||
password: { type: "string", description: "Initial password, minimum 8 characters." },
|
||||
},
|
||||
required: ["email", "name", "password"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_user_password",
|
||||
description: "Reset a user's password. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
password: { type: "string", description: "New password, minimum 8 characters." },
|
||||
},
|
||||
required: ["userId", "password"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_user_role",
|
||||
description: "Change a user's system role. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "User ID." },
|
||||
systemRole: { type: "string", enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
|
||||
},
|
||||
required: ["id", "systemRole"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_user_name",
|
||||
description: "Rename a user. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "User ID." },
|
||||
name: { type: "string", description: "New display name." },
|
||||
},
|
||||
required: ["id", "name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "link_user_resource",
|
||||
description: "Link or unlink a user to a resource. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
resourceId: { type: ["string", "null"], description: "Resource ID or null to unlink." },
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "auto_link_users_by_email",
|
||||
description: "Auto-link all users without a resource to matching resources by email. Admin role required. Always confirm first.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_user_permissions",
|
||||
description: "Set explicit permission overrides for a user. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
overrides: {
|
||||
type: ["object", "null"],
|
||||
properties: {
|
||||
granted: { type: "array", items: { type: "string" } },
|
||||
denied: { type: "array", items: { type: "string" } },
|
||||
chapterIds: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
description: "Permission override object or null to clear.",
|
||||
export const userAdminToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_users",
|
||||
description:
|
||||
"List all system users via the admin user router, including role and MFA state. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: "integer", description: "Max results. Default: 50" },
|
||||
},
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "reset_user_permissions",
|
||||
description: "Reset a user's permission overrides back to role defaults. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_user",
|
||||
description:
|
||||
"Create a new system user and auto-link a matching resource by email when possible. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
email: { type: "string", description: "User email address." },
|
||||
name: { type: "string", description: "Display name." },
|
||||
systemRole: {
|
||||
type: "string",
|
||||
enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"],
|
||||
description: "Initial system role.",
|
||||
},
|
||||
password: { type: "string", description: "Initial password, minimum 8 characters." },
|
||||
},
|
||||
required: ["email", "name", "password"],
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_effective_user_permissions",
|
||||
description: "Get a user's resolved permissions, role, and explicit overrides. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_user_password",
|
||||
description: "Reset a user's password. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
password: { type: "string", description: "New password, minimum 8 characters." },
|
||||
},
|
||||
required: ["userId", "password"],
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "disable_user_totp",
|
||||
description: "Disable MFA TOTP for a user as an admin override. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_user_role",
|
||||
description: "Change a user's system role. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "User ID." },
|
||||
systemRole: {
|
||||
type: "string",
|
||||
enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"],
|
||||
},
|
||||
},
|
||||
required: ["id", "systemRole"],
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_user_name",
|
||||
description: "Rename a user. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "User ID." },
|
||||
name: { type: "string", description: "New display name." },
|
||||
},
|
||||
required: ["id", "name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "link_user_resource",
|
||||
description:
|
||||
"Link or unlink a user to a resource. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
resourceId: { type: ["string", "null"], description: "Resource ID or null to unlink." },
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "auto_link_users_by_email",
|
||||
description:
|
||||
"Auto-link all users without a resource to matching resources by email. Admin role required. Always confirm first.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_user_permissions",
|
||||
description:
|
||||
"Set explicit permission overrides for a user. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
overrides: {
|
||||
type: ["object", "null"],
|
||||
properties: {
|
||||
granted: { type: "array", items: { type: "string" } },
|
||||
denied: { type: "array", items: { type: "string" } },
|
||||
chapterIds: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
description: "Permission override object or null to clear.",
|
||||
},
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "reset_user_permissions",
|
||||
description:
|
||||
"Reset a user's permission overrides back to role defaults. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_effective_user_permissions",
|
||||
description:
|
||||
"Get a user's resolved permissions, role, and explicit overrides. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "disable_user_totp",
|
||||
description:
|
||||
"Disable MFA TOTP for a user as an admin override. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", description: "User ID." },
|
||||
},
|
||||
required: ["userId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
list_users: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
create_user: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
set_user_password: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_user_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_user_name: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
link_user_resource: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
auto_link_users_by_email: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
set_user_permissions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
reset_user_permissions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
get_effective_user_permissions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
disable_user_totp: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
},
|
||||
], {
|
||||
list_users: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
create_user: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
set_user_password: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_user_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_user_name: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
link_user_resource: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
auto_link_users_by_email: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
set_user_permissions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
reset_user_permissions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
get_effective_user_permissions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
disable_user_totp: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createUserAdminExecutors(
|
||||
deps: UserAdminDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
export function createUserAdminExecutors(deps: UserAdminDeps): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async list_users(params: { limit?: number }, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
@@ -258,12 +273,15 @@ export function createUserAdminExecutors(
|
||||
return users.slice(0, Math.min(params.limit ?? 50, 100));
|
||||
},
|
||||
|
||||
async create_user(params: {
|
||||
email: string;
|
||||
name: string;
|
||||
systemRole?: SystemRole;
|
||||
password: string;
|
||||
}, ctx: ToolContext) {
|
||||
async create_user(
|
||||
params: {
|
||||
email: string;
|
||||
name: string;
|
||||
systemRole?: SystemRole;
|
||||
password: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
let user;
|
||||
try {
|
||||
@@ -354,7 +372,10 @@ export function createUserAdminExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async link_user_resource(params: { userId: string; resourceId?: string | null }, ctx: ToolContext) {
|
||||
async link_user_resource(
|
||||
params: { userId: string; resourceId?: string | null },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
let result;
|
||||
try {
|
||||
@@ -389,14 +410,17 @@ export function createUserAdminExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async set_user_permissions(params: {
|
||||
userId: string;
|
||||
overrides?: {
|
||||
granted?: string[];
|
||||
denied?: string[];
|
||||
chapterIds?: string[];
|
||||
} | null;
|
||||
}, ctx: ToolContext) {
|
||||
async set_user_permissions(
|
||||
params: {
|
||||
userId: string;
|
||||
overrides?: {
|
||||
granted?: string[];
|
||||
denied?: string[];
|
||||
chapterIds?: string[];
|
||||
} | null;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
let user;
|
||||
try {
|
||||
@@ -417,7 +441,9 @@ export function createUserAdminExecutors(
|
||||
success: true,
|
||||
user,
|
||||
userId: user.id,
|
||||
message: params.overrides ? "Updated user permission overrides." : "Cleared user permission overrides.",
|
||||
message: params.overrides
|
||||
? "Updated user permission overrides."
|
||||
: "Cleared user permission overrides.",
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -20,9 +20,9 @@ type UserSelfServiceDeps = {
|
||||
getDashboardLayout: () => Promise<unknown>;
|
||||
saveDashboardLayout: (params: { layout: unknown[] }) => Promise<Record<string, unknown>>;
|
||||
getFavoriteProjectIds: () => Promise<unknown>;
|
||||
toggleFavoriteProject: (
|
||||
params: { projectId: string },
|
||||
) => Promise<Record<string, unknown> & { added: boolean }>;
|
||||
toggleFavoriteProject: (params: {
|
||||
projectId: string;
|
||||
}) => Promise<Record<string, unknown> & { added: boolean }>;
|
||||
getColumnPreferences: () => Promise<unknown>;
|
||||
setColumnPreferences: (params: {
|
||||
view: ColumnPreferenceView;
|
||||
@@ -36,169 +36,184 @@ type UserSelfServiceDeps = {
|
||||
activeCount: () => Promise<unknown>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
toAssistantCurrentUserError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantTotpEnableError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantCurrentUserError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantTotpEnableError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const userSelfServiceToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_assignable_users",
|
||||
description: "List lightweight users available for assignment workflows. Manager or admin role required.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_current_user",
|
||||
description: "Get the authenticated user's own profile, role, and permission overrides.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_dashboard_layout",
|
||||
description: "Get the authenticated user's saved dashboard widget layout and last update timestamp.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "save_dashboard_layout",
|
||||
description: "Save the authenticated user's dashboard layout. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
layout: {
|
||||
type: "array",
|
||||
description: "Dashboard layout items as stored by the user router.",
|
||||
items: { type: "object" },
|
||||
},
|
||||
},
|
||||
required: ["layout"],
|
||||
export const userSelfServiceToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_assignable_users",
|
||||
description:
|
||||
"List lightweight users available for assignment workflows. Manager or admin role required.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_favorite_project_ids",
|
||||
description: "Get the authenticated user's favorite project IDs.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "toggle_favorite_project",
|
||||
description: "Add or remove a project from the authenticated user's favorites. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID." },
|
||||
},
|
||||
required: ["projectId"],
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_current_user",
|
||||
description: "Get the authenticated user's own profile, role, and permission overrides.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_column_preferences",
|
||||
description: "Get the authenticated user's saved table column preferences for all supported views.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_dashboard_layout",
|
||||
description:
|
||||
"Get the authenticated user's saved dashboard widget layout and last update timestamp.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_column_preferences",
|
||||
description: "Update the authenticated user's table column preferences for one view. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
view: {
|
||||
type: "string",
|
||||
enum: ["resources", "projects", "allocations", "vacations", "roles", "users", "blueprints"],
|
||||
description: "View key to update.",
|
||||
},
|
||||
visible: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Visible column IDs.",
|
||||
},
|
||||
sort: {
|
||||
type: ["object", "null"],
|
||||
properties: {
|
||||
field: { type: "string" },
|
||||
dir: { type: "string", enum: ["asc", "desc"] },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "save_dashboard_layout",
|
||||
description: "Save the authenticated user's dashboard layout. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
layout: {
|
||||
type: "array",
|
||||
description: "Dashboard layout items as stored by the user router.",
|
||||
items: { type: "object" },
|
||||
},
|
||||
description: "Sort state. Use null to clear it.",
|
||||
},
|
||||
rowOrder: {
|
||||
type: ["array", "null"],
|
||||
items: { type: "string" },
|
||||
description: "Optional row order. Use null to clear it.",
|
||||
},
|
||||
required: ["layout"],
|
||||
},
|
||||
required: ["view"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "generate_totp_secret",
|
||||
description: "Generate a new MFA TOTP secret and provisioning URI for the authenticated user. Always confirm first. The secret is sensitive.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "verify_and_enable_totp",
|
||||
description: "Verify a 6-digit MFA TOTP token and enable MFA for the authenticated user. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
token: { type: "string", description: "6-digit TOTP token." },
|
||||
},
|
||||
required: ["token"],
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_favorite_project_ids",
|
||||
description: "Get the authenticated user's favorite project IDs.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "toggle_favorite_project",
|
||||
description:
|
||||
"Add or remove a project from the authenticated user's favorites. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID." },
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_column_preferences",
|
||||
description:
|
||||
"Get the authenticated user's saved table column preferences for all supported views.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_column_preferences",
|
||||
description:
|
||||
"Update the authenticated user's table column preferences for one view. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
view: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"resources",
|
||||
"projects",
|
||||
"allocations",
|
||||
"vacations",
|
||||
"roles",
|
||||
"users",
|
||||
"blueprints",
|
||||
],
|
||||
description: "View key to update.",
|
||||
},
|
||||
visible: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Visible column IDs.",
|
||||
},
|
||||
sort: {
|
||||
type: ["object", "null"],
|
||||
properties: {
|
||||
field: { type: "string" },
|
||||
dir: { type: "string", enum: ["asc", "desc"] },
|
||||
},
|
||||
description: "Sort state. Use null to clear it.",
|
||||
},
|
||||
rowOrder: {
|
||||
type: ["array", "null"],
|
||||
items: { type: "string" },
|
||||
description: "Optional row order. Use null to clear it.",
|
||||
},
|
||||
},
|
||||
required: ["view"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "generate_totp_secret",
|
||||
description:
|
||||
"Generate a new MFA TOTP secret and provisioning URI for the authenticated user. Always confirm first. The secret is sensitive.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "verify_and_enable_totp",
|
||||
description:
|
||||
"Verify a 6-digit MFA TOTP token and enable MFA for the authenticated user. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
token: { type: "string", description: "6-digit TOTP token." },
|
||||
},
|
||||
required: ["token"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_mfa_status",
|
||||
description: "Get the authenticated user's MFA status.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_active_user_count",
|
||||
description:
|
||||
"Get the number of users active in the last five minutes. Admin role required.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_mfa_status",
|
||||
description: "Get the authenticated user's MFA status.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
list_assignable_users: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_active_user_count: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_active_user_count",
|
||||
description: "Get the number of users active in the last five minutes. Admin role required.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
], {
|
||||
list_assignable_users: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_active_user_count: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createUserSelfServiceExecutors(
|
||||
deps: UserSelfServiceDeps,
|
||||
@@ -233,8 +248,8 @@ export function createUserSelfServiceExecutors(
|
||||
|
||||
async save_dashboard_layout(params: { layout: unknown[] }, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
const result = await withCurrentUserErrorMapping(
|
||||
() => caller.saveDashboardLayout({ layout: params.layout }),
|
||||
const result = await withCurrentUserErrorMapping(() =>
|
||||
caller.saveDashboardLayout({ layout: params.layout }),
|
||||
);
|
||||
if ("error" in result) {
|
||||
return result;
|
||||
@@ -255,8 +270,8 @@ export function createUserSelfServiceExecutors(
|
||||
|
||||
async toggle_favorite_project(params: { projectId: string }, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
const result = await withCurrentUserErrorMapping(
|
||||
() => caller.toggleFavoriteProject({ projectId: params.projectId }),
|
||||
const result = await withCurrentUserErrorMapping(() =>
|
||||
caller.toggleFavoriteProject({ projectId: params.projectId }),
|
||||
);
|
||||
if ("error" in result) {
|
||||
return result;
|
||||
@@ -275,19 +290,24 @@ export function createUserSelfServiceExecutors(
|
||||
return withCurrentUserErrorMapping(() => caller.getColumnPreferences());
|
||||
},
|
||||
|
||||
async set_column_preferences(params: {
|
||||
view: ColumnPreferenceView;
|
||||
visible?: string[];
|
||||
sort?: { field: string; dir: "asc" | "desc" } | null;
|
||||
rowOrder?: string[] | null;
|
||||
}, ctx: ToolContext) {
|
||||
async set_column_preferences(
|
||||
params: {
|
||||
view: ColumnPreferenceView;
|
||||
visible?: string[];
|
||||
sort?: { field: string; dir: "asc" | "desc" } | null;
|
||||
rowOrder?: string[] | null;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
const result = await withCurrentUserErrorMapping(() => caller.setColumnPreferences({
|
||||
view: params.view,
|
||||
...(params.visible !== undefined ? { visible: params.visible } : {}),
|
||||
...(params.sort !== undefined ? { sort: params.sort } : {}),
|
||||
...(params.rowOrder !== undefined ? { rowOrder: params.rowOrder } : {}),
|
||||
}));
|
||||
const result = await withCurrentUserErrorMapping(() =>
|
||||
caller.setColumnPreferences({
|
||||
view: params.view,
|
||||
...(params.visible !== undefined ? { visible: params.visible } : {}),
|
||||
...(params.sort !== undefined ? { sort: params.sort } : {}),
|
||||
...(params.rowOrder !== undefined ? { rowOrder: params.rowOrder } : {}),
|
||||
}),
|
||||
);
|
||||
if ("error" in result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VacationType } from "@capakraken/db";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { VacationType } from "@nexus/db";
|
||||
import { SystemRole } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -56,15 +56,8 @@ type VacationEntitlementDeps = {
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
createEntitlementCaller: (ctx: TRPCContext) => {
|
||||
getYearSummaryDetail: (params: {
|
||||
year: number;
|
||||
resourceName?: string;
|
||||
}) => Promise<unknown>;
|
||||
set: (params: {
|
||||
resourceId: string;
|
||||
year: number;
|
||||
entitledDays: number;
|
||||
}) => Promise<unknown>;
|
||||
getYearSummaryDetail: (params: { year: number; resourceName?: string }) => Promise<unknown>;
|
||||
set: (params: { resourceId: string; year: number; entitledDays: number }) => Promise<unknown>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
resolveResourceIdentifier: (
|
||||
@@ -74,162 +67,168 @@ type VacationEntitlementDeps = {
|
||||
parseIsoDate: (value: string, fieldName: string) => Date;
|
||||
fmtDate: (value: Date | null | undefined) => string | null;
|
||||
parseAssistantVacationRequestType: (input: string) => VacationType;
|
||||
toAssistantVacationCreationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantVacationCreationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantVacationMutationError: (
|
||||
error: unknown,
|
||||
action: "approve" | "reject" | "cancel",
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantEntitlementMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantEntitlementMutationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const vacationEntitlementToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_vacation",
|
||||
description: "Create a vacation/leave request through the real vacation workflow. Any authenticated user can request leave for their own resource; manager/admin can create requests for others. Always confirm with the user.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
type: {
|
||||
type: "string",
|
||||
enum: ["ANNUAL", "SICK", "OTHER"],
|
||||
description: "Vacation type. PUBLIC_HOLIDAY requests are managed through holiday calendars, not manual vacation requests.",
|
||||
export const vacationEntitlementToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_vacation",
|
||||
description:
|
||||
"Create a vacation/leave request through the real vacation workflow. Any authenticated user can request leave for their own resource; manager/admin can create requests for others. Always confirm with the user.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
type: {
|
||||
type: "string",
|
||||
enum: ["ANNUAL", "SICK", "OTHER"],
|
||||
description:
|
||||
"Vacation type. PUBLIC_HOLIDAY requests are managed through holiday calendars, not manual vacation requests.",
|
||||
},
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
isHalfDay: { type: "boolean", description: "Half day? Default: false" },
|
||||
halfDayPart: { type: "string", description: "MORNING or AFTERNOON (if half day)" },
|
||||
note: { type: "string", description: "Optional note" },
|
||||
},
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
isHalfDay: { type: "boolean", description: "Half day? Default: false" },
|
||||
halfDayPart: { type: "string", description: "MORNING or AFTERNOON (if half day)" },
|
||||
note: { type: "string", description: "Optional note" },
|
||||
required: ["resourceId", "type", "startDate", "endDate"],
|
||||
},
|
||||
required: ["resourceId", "type", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "approve_vacation",
|
||||
description:
|
||||
"Approve a vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vacationId: { type: "string", description: "Vacation ID" },
|
||||
},
|
||||
required: ["vacationId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "reject_vacation",
|
||||
description:
|
||||
"Reject a pending vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vacationId: { type: "string", description: "Vacation ID" },
|
||||
reason: { type: "string", description: "Rejection reason" },
|
||||
},
|
||||
required: ["vacationId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "cancel_vacation",
|
||||
description:
|
||||
"Cancel a vacation request through the real vacation workflow. Users can cancel their own requests; manager/admin can cancel any request. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vacationId: { type: "string", description: "Vacation ID" },
|
||||
},
|
||||
required: ["vacationId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_pending_vacation_approvals",
|
||||
description: "List vacation requests awaiting approval.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_team_vacation_overlap",
|
||||
description: "Check if team members have overlapping vacations in a date range.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID to check overlap for" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
},
|
||||
required: ["resourceId", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_entitlement_summary",
|
||||
description:
|
||||
"Get vacation entitlement year summary for all resources or a specific resource.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
year: { type: "integer", description: "Year. Default: current year" },
|
||||
resourceName: { type: "string", description: "Filter by resource name (optional)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_entitlement",
|
||||
description:
|
||||
"Set the annual vacation entitlement for a resource/year through the real entitlement workflow. Manager or admin role required. Carryover is computed automatically. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
year: { type: "integer", description: "Year" },
|
||||
entitledDays: { type: "number", description: "Number of entitled vacation days" },
|
||||
},
|
||||
required: ["resourceId", "year", "entitledDays"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "approve_vacation",
|
||||
description: "Approve a vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vacationId: { type: "string", description: "Vacation ID" },
|
||||
},
|
||||
required: ["vacationId"],
|
||||
},
|
||||
approve_vacation: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
reject_vacation: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_pending_vacation_approvals: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_entitlement_summary: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
set_entitlement: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "reject_vacation",
|
||||
description: "Reject a pending vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vacationId: { type: "string", description: "Vacation ID" },
|
||||
reason: { type: "string", description: "Rejection reason" },
|
||||
},
|
||||
required: ["vacationId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "cancel_vacation",
|
||||
description: "Cancel a vacation request through the real vacation workflow. Users can cancel their own requests; manager/admin can cancel any request. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vacationId: { type: "string", description: "Vacation ID" },
|
||||
},
|
||||
required: ["vacationId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_pending_vacation_approvals",
|
||||
description: "List vacation requests awaiting approval.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_team_vacation_overlap",
|
||||
description: "Check if team members have overlapping vacations in a date range.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID to check overlap for" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
},
|
||||
required: ["resourceId", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_entitlement_summary",
|
||||
description: "Get vacation entitlement year summary for all resources or a specific resource.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
year: { type: "integer", description: "Year. Default: current year" },
|
||||
resourceName: { type: "string", description: "Filter by resource name (optional)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_entitlement",
|
||||
description: "Set the annual vacation entitlement for a resource/year through the real entitlement workflow. Manager or admin role required. Carryover is computed automatically. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
year: { type: "integer", description: "Year" },
|
||||
entitledDays: { type: "number", description: "Number of entitled vacation days" },
|
||||
},
|
||||
required: ["resourceId", "year", "entitledDays"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {
|
||||
approve_vacation: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
reject_vacation: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_pending_vacation_approvals: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_entitlement_summary: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
set_entitlement: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createVacationEntitlementExecutors(
|
||||
deps: VacationEntitlementDeps,
|
||||
@@ -262,7 +261,9 @@ export function createVacationEntitlementExecutors(
|
||||
startDate: deps.parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: deps.parseIsoDate(params.endDate, "endDate"),
|
||||
...(params.isHalfDay !== undefined ? { isHalfDay: params.isHalfDay } : {}),
|
||||
...(params.halfDayPart !== undefined ? { halfDayPart: params.halfDayPart as "MORNING" | "AFTERNOON" } : {}),
|
||||
...(params.halfDayPart !== undefined
|
||||
? { halfDayPart: params.halfDayPart as "MORNING" | "AFTERNOON" }
|
||||
: {}),
|
||||
...(params.note !== undefined ? { note: params.note } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -273,9 +274,8 @@ export function createVacationEntitlementExecutors(
|
||||
throw error;
|
||||
}
|
||||
|
||||
const effectiveDays = typeof vacation.effectiveDays === "number"
|
||||
? vacation.effectiveDays
|
||||
: null;
|
||||
const effectiveDays =
|
||||
typeof vacation.effectiveDays === "number" ? vacation.effectiveDays : null;
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
@@ -287,10 +287,7 @@ export function createVacationEntitlementExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async approve_vacation(
|
||||
params: { vacationId: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async approve_vacation(params: { vacationId: string }, ctx: ToolContext) {
|
||||
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
let existing;
|
||||
try {
|
||||
@@ -324,10 +321,7 @@ export function createVacationEntitlementExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async reject_vacation(
|
||||
params: { vacationId: string; reason?: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async reject_vacation(params: { vacationId: string; reason?: string }, ctx: ToolContext) {
|
||||
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
let existing;
|
||||
try {
|
||||
@@ -363,10 +357,7 @@ export function createVacationEntitlementExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async cancel_vacation(
|
||||
params: { vacationId: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async cancel_vacation(params: { vacationId: string }, ctx: ToolContext) {
|
||||
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
let existing;
|
||||
try {
|
||||
@@ -399,24 +390,23 @@ export function createVacationEntitlementExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async get_pending_vacation_approvals(
|
||||
params: { limit?: number },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
async get_pending_vacation_approvals(params: { limit?: number }, ctx: ToolContext) {
|
||||
const limit = Math.min(params.limit ?? 20, 50);
|
||||
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
const vacations = await caller.getPendingApprovals();
|
||||
|
||||
return vacations.map((vacation) => ({
|
||||
id: vacation.id,
|
||||
resource: vacation.resource.displayName,
|
||||
eid: vacation.resource.eid,
|
||||
chapter: vacation.resource.chapter,
|
||||
type: vacation.type,
|
||||
start: deps.fmtDate(vacation.startDate),
|
||||
end: deps.fmtDate(vacation.endDate),
|
||||
isHalfDay: vacation.isHalfDay,
|
||||
})).slice(0, limit);
|
||||
return vacations
|
||||
.map((vacation) => ({
|
||||
id: vacation.id,
|
||||
resource: vacation.resource.displayName,
|
||||
eid: vacation.resource.eid,
|
||||
chapter: vacation.resource.chapter,
|
||||
type: vacation.type,
|
||||
start: deps.fmtDate(vacation.startDate),
|
||||
end: deps.fmtDate(vacation.endDate),
|
||||
isHalfDay: vacation.isHalfDay,
|
||||
}))
|
||||
.slice(0, limit);
|
||||
},
|
||||
|
||||
async get_team_vacation_overlap(
|
||||
@@ -459,7 +449,8 @@ export function createVacationEntitlementExecutors(
|
||||
) {
|
||||
if (params.carryoverDays !== undefined) {
|
||||
return {
|
||||
error: "Manual carryoverDays is not supported here. Carryover is computed automatically from prior-year balances.",
|
||||
error:
|
||||
"Manual carryoverDays is not supported here. Carryover is computed automatically from prior-year balances.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
SystemRole,
|
||||
UpdateHolidayCalendarEntrySchema,
|
||||
UpdateHolidayCalendarSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
@@ -47,18 +47,20 @@ type VacationHolidayDeps = {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
limit: number;
|
||||
}) => Promise<Array<{
|
||||
type: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
isHalfDay?: boolean | null;
|
||||
halfDayPart?: string | null;
|
||||
resource: {
|
||||
displayName: string;
|
||||
eid: string;
|
||||
chapter?: string | null;
|
||||
};
|
||||
}>>;
|
||||
}) => Promise<
|
||||
Array<{
|
||||
type: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
isHalfDay?: boolean | null;
|
||||
halfDayPart?: string | null;
|
||||
resource: {
|
||||
displayName: string;
|
||||
eid: string;
|
||||
chapter?: string | null;
|
||||
};
|
||||
}>
|
||||
>;
|
||||
};
|
||||
createHolidayCalendarCaller: (ctx: TRPCContext) => {
|
||||
resolveHolidaysDetail: (params: {
|
||||
@@ -120,11 +122,11 @@ type VacationHolidayDeps = {
|
||||
ctx: ToolContext,
|
||||
identifier: string,
|
||||
) => Promise<ResolvedResource | AssistantToolErrorResult>;
|
||||
resolveHolidayPeriod: (input: {
|
||||
year?: number;
|
||||
periodStart?: string;
|
||||
periodEnd?: string;
|
||||
}) => { year: number | null; periodStart: Date; periodEnd: Date };
|
||||
resolveHolidayPeriod: (input: { year?: number; periodStart?: string; periodEnd?: string }) => {
|
||||
year: number | null;
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
};
|
||||
resolveEntityOrAssistantError: <T>(
|
||||
resolve: () => Promise<T>,
|
||||
notFoundMessage: string,
|
||||
@@ -133,279 +135,334 @@ type VacationHolidayDeps = {
|
||||
fmtDate: (value: Date | null | undefined) => string | null;
|
||||
formatHolidayCalendar: (calendar: HolidayCalendarRecord) => unknown;
|
||||
formatHolidayCalendarEntry: (entry: HolidayCalendarEntryRecord) => unknown;
|
||||
toAssistantHolidayCalendarMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantHolidayCalendarNotFoundError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantHolidayEntryMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantHolidayEntryNotFoundError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantHolidayCalendarMutationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantHolidayCalendarNotFoundError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantHolidayEntryMutationError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
toAssistantHolidayEntryNotFoundError: (error: unknown) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const vacationHolidayReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_vacation_balance",
|
||||
description: "Get the holiday-aware vacation balance for a resource via the real entitlement workflow. Authenticated users can read their own balance; manager/admin/controller can read broader balances.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
year: { type: "integer", description: "Year. Default: current year" },
|
||||
},
|
||||
required: ["resourceId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_vacations_upcoming",
|
||||
description: "List upcoming vacations across all resources, or for a specific resource/team. Shows who is off when.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceName: { type: "string", description: "Filter by resource name (partial match)" },
|
||||
chapter: { type: "string", description: "Filter by chapter/team" },
|
||||
daysAhead: { type: "integer", description: "How many days ahead to look. Default: 30" },
|
||||
limit: { type: "integer", description: "Max results. Default: 30" },
|
||||
export const vacationHolidayReadToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_vacation_balance",
|
||||
description:
|
||||
"Get the holiday-aware vacation balance for a resource via the real entitlement workflow. Authenticated users can read their own balance; manager/admin/controller can read broader balances.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
year: { type: "integer", description: "Year. Default: current year" },
|
||||
},
|
||||
required: ["resourceId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_holidays_by_region",
|
||||
description: "List resolved public holidays for a country, federal state, and optionally a city in a given year or date range. Use this to compare regions such as Bayern vs Hamburg.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
countryCode: { type: "string", description: "Country code such as DE, ES, US, IN." },
|
||||
federalState: { type: "string", description: "Federal state / region code, e.g. BY, HH, NRW." },
|
||||
metroCity: { type: "string", description: "Optional city name for local city-specific holidays, e.g. Augsburg." },
|
||||
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
|
||||
periodStart: { type: "string", description: "Optional start date in YYYY-MM-DD. Requires periodEnd." },
|
||||
periodEnd: { type: "string", description: "Optional end date in YYYY-MM-DD. Requires periodStart." },
|
||||
},
|
||||
required: ["countryCode"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_resource_holidays",
|
||||
description: "List resolved public holidays for a specific resource based on that person's country, federal state, and city context.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Resource ID, EID, or display name." },
|
||||
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
|
||||
periodStart: { type: "string", description: "Optional start date in YYYY-MM-DD. Requires periodEnd." },
|
||||
periodEnd: { type: "string", description: "Optional end date in YYYY-MM-DD. Requires periodStart." },
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_holiday_calendars",
|
||||
description: "List holiday calendars including scope, assignment, active flag, priority, and entry count. Useful to inspect the calendar-editor configuration context.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
includeInactive: { type: "boolean", description: "Include inactive calendars. Default: false." },
|
||||
countryCode: { type: "string", description: "Optional country code filter such as DE or ES." },
|
||||
scopeType: { type: "string", description: "Optional scope filter: COUNTRY, STATE, CITY." },
|
||||
stateCode: { type: "string", description: "Optional state/region code filter such as BY or NRW." },
|
||||
metroCity: { type: "string", description: "Optional city-name filter." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_vacations_upcoming",
|
||||
description:
|
||||
"List upcoming vacations across all resources, or for a specific resource/team. Shows who is off when.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceName: {
|
||||
type: "string",
|
||||
description: "Filter by resource name (partial match)",
|
||||
},
|
||||
chapter: { type: "string", description: "Filter by chapter/team" },
|
||||
daysAhead: { type: "integer", description: "How many days ahead to look. Default: 30" },
|
||||
limit: { type: "integer", description: "Max results. Default: 30" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_holiday_calendar",
|
||||
description: "Get a single holiday calendar including all entries. Accepts either the calendar ID or its name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Holiday calendar ID or name." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_holidays_by_region",
|
||||
description:
|
||||
"List resolved public holidays for a country, federal state, and optionally a city in a given year or date range. Use this to compare regions such as Bayern vs Hamburg.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
countryCode: { type: "string", description: "Country code such as DE, ES, US, IN." },
|
||||
federalState: {
|
||||
type: "string",
|
||||
description: "Federal state / region code, e.g. BY, HH, NRW.",
|
||||
},
|
||||
metroCity: {
|
||||
type: "string",
|
||||
description: "Optional city name for local city-specific holidays, e.g. Augsburg.",
|
||||
},
|
||||
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
|
||||
periodStart: {
|
||||
type: "string",
|
||||
description: "Optional start date in YYYY-MM-DD. Requires periodEnd.",
|
||||
},
|
||||
periodEnd: {
|
||||
type: "string",
|
||||
description: "Optional end date in YYYY-MM-DD. Requires periodStart.",
|
||||
},
|
||||
},
|
||||
required: ["countryCode"],
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "preview_resolved_holiday_calendar",
|
||||
description: "Preview the resolved holiday result for a country/state/city scope and year, including which calendar each holiday comes from.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
countryId: { type: "string", description: "Country ID." },
|
||||
stateCode: { type: "string", description: "Optional state/region code." },
|
||||
metroCityId: { type: "string", description: "Optional metro city ID for city-specific preview." },
|
||||
year: { type: "integer", description: "Full year, e.g. 2026." },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_resource_holidays",
|
||||
description:
|
||||
"List resolved public holidays for a specific resource based on that person's country, federal state, and city context.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Resource ID, EID, or display name." },
|
||||
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
|
||||
periodStart: {
|
||||
type: "string",
|
||||
description: "Optional start date in YYYY-MM-DD. Requires periodEnd.",
|
||||
},
|
||||
periodEnd: {
|
||||
type: "string",
|
||||
description: "Optional end date in YYYY-MM-DD. Requires periodStart.",
|
||||
},
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
required: ["countryId", "year"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_holiday_calendars",
|
||||
description:
|
||||
"List holiday calendars including scope, assignment, active flag, priority, and entry count. Useful to inspect the calendar-editor configuration context.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
includeInactive: {
|
||||
type: "boolean",
|
||||
description: "Include inactive calendars. Default: false.",
|
||||
},
|
||||
countryCode: {
|
||||
type: "string",
|
||||
description: "Optional country code filter such as DE or ES.",
|
||||
},
|
||||
scopeType: {
|
||||
type: "string",
|
||||
description: "Optional scope filter: COUNTRY, STATE, CITY.",
|
||||
},
|
||||
stateCode: {
|
||||
type: "string",
|
||||
description: "Optional state/region code filter such as BY or NRW.",
|
||||
},
|
||||
metroCity: { type: "string", description: "Optional city-name filter." },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_holiday_calendar",
|
||||
description:
|
||||
"Get a single holiday calendar including all entries. Accepts either the calendar ID or its name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
identifier: { type: "string", description: "Holiday calendar ID or name." },
|
||||
},
|
||||
required: ["identifier"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "preview_resolved_holiday_calendar",
|
||||
description:
|
||||
"Preview the resolved holiday result for a country/state/city scope and year, including which calendar each holiday comes from.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
countryId: { type: "string", description: "Country ID." },
|
||||
stateCode: { type: "string", description: "Optional state/region code." },
|
||||
metroCityId: {
|
||||
type: "string",
|
||||
description: "Optional metro city ID for city-specific preview.",
|
||||
},
|
||||
year: { type: "integer", description: "Full year, e.g. 2026." },
|
||||
},
|
||||
required: ["countryId", "year"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
list_holiday_calendars: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
get_holiday_calendar: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
},
|
||||
], {
|
||||
list_holiday_calendars: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
get_holiday_calendar: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export const vacationHolidayMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_holiday_calendar",
|
||||
description: "Create a holiday calendar for a country, state, or city scope. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Calendar name." },
|
||||
scopeType: { type: "string", description: "COUNTRY, STATE, or CITY." },
|
||||
countryId: { type: "string", description: "Country ID." },
|
||||
stateCode: { type: "string", description: "Required for STATE calendars." },
|
||||
metroCityId: { type: "string", description: "Required for CITY calendars." },
|
||||
isActive: { type: "boolean", description: "Whether the calendar is active. Default: true." },
|
||||
priority: { type: "integer", description: "Priority used during calendar resolution. Default: 0." },
|
||||
},
|
||||
required: ["name", "scopeType", "countryId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_holiday_calendar",
|
||||
description: "Update an existing holiday calendar. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Holiday calendar ID." },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
stateCode: { type: "string" },
|
||||
metroCityId: { type: "string" },
|
||||
isActive: { type: "boolean" },
|
||||
priority: { type: "integer" },
|
||||
export const vacationHolidayMutationToolDefinitions: ToolDef[] = withToolAccess(
|
||||
[
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_holiday_calendar",
|
||||
description:
|
||||
"Create a holiday calendar for a country, state, or city scope. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Calendar name." },
|
||||
scopeType: { type: "string", description: "COUNTRY, STATE, or CITY." },
|
||||
countryId: { type: "string", description: "Country ID." },
|
||||
stateCode: { type: "string", description: "Required for STATE calendars." },
|
||||
metroCityId: { type: "string", description: "Required for CITY calendars." },
|
||||
isActive: {
|
||||
type: "boolean",
|
||||
description: "Whether the calendar is active. Default: true.",
|
||||
},
|
||||
priority: {
|
||||
type: "integer",
|
||||
description: "Priority used during calendar resolution. Default: 0.",
|
||||
},
|
||||
},
|
||||
required: ["name", "scopeType", "countryId"],
|
||||
},
|
||||
required: ["id", "data"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_holiday_calendar",
|
||||
description: "Delete a holiday calendar and all of its entries. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Holiday calendar ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_holiday_calendar_entry",
|
||||
description: "Create a holiday entry in an existing calendar. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
holidayCalendarId: { type: "string", description: "Holiday calendar ID." },
|
||||
date: { type: "string", description: "Date in YYYY-MM-DD format." },
|
||||
name: { type: "string", description: "Holiday name." },
|
||||
isRecurringAnnual: { type: "boolean", description: "Whether the holiday repeats every year." },
|
||||
source: { type: "string", description: "Optional source or legal basis." },
|
||||
},
|
||||
required: ["holidayCalendarId", "date", "name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_holiday_calendar_entry",
|
||||
description: "Update an existing holiday calendar entry. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Holiday calendar entry ID." },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
date: { type: "string", description: "Date in YYYY-MM-DD format." },
|
||||
name: { type: "string" },
|
||||
isRecurringAnnual: { type: "boolean" },
|
||||
source: { type: "string" },
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_holiday_calendar",
|
||||
description:
|
||||
"Update an existing holiday calendar. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Holiday calendar ID." },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
stateCode: { type: "string" },
|
||||
metroCityId: { type: "string" },
|
||||
isActive: { type: "boolean" },
|
||||
priority: { type: "integer" },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["id", "data"],
|
||||
},
|
||||
required: ["id", "data"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_holiday_calendar",
|
||||
description:
|
||||
"Delete a holiday calendar and all of its entries. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Holiday calendar ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_holiday_calendar_entry",
|
||||
description:
|
||||
"Create a holiday entry in an existing calendar. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
holidayCalendarId: { type: "string", description: "Holiday calendar ID." },
|
||||
date: { type: "string", description: "Date in YYYY-MM-DD format." },
|
||||
name: { type: "string", description: "Holiday name." },
|
||||
isRecurringAnnual: {
|
||||
type: "boolean",
|
||||
description: "Whether the holiday repeats every year.",
|
||||
},
|
||||
source: { type: "string", description: "Optional source or legal basis." },
|
||||
},
|
||||
required: ["holidayCalendarId", "date", "name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_holiday_calendar_entry",
|
||||
description:
|
||||
"Update an existing holiday calendar entry. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Holiday calendar entry ID." },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
date: { type: "string", description: "Date in YYYY-MM-DD format." },
|
||||
name: { type: "string" },
|
||||
isRecurringAnnual: { type: "boolean" },
|
||||
source: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["id", "data"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_holiday_calendar_entry",
|
||||
description: "Delete a holiday calendar entry. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Holiday calendar entry ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_holiday_calendar_entry",
|
||||
description: "Delete a holiday calendar entry. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Holiday calendar entry ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
create_holiday_calendar: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_holiday_calendar: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
delete_holiday_calendar: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
create_holiday_calendar_entry: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_holiday_calendar_entry: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
delete_holiday_calendar_entry: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
},
|
||||
], {
|
||||
create_holiday_calendar: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_holiday_calendar: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
delete_holiday_calendar: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
create_holiday_calendar_entry: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_holiday_calendar_entry: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
delete_holiday_calendar_entry: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export function createVacationHolidayExecutors(
|
||||
deps: VacationHolidayDeps,
|
||||
@@ -422,12 +479,15 @@ export function createVacationHolidayExecutors(
|
||||
return caller.getBalanceDetail({ resourceId: resource.id, year });
|
||||
},
|
||||
|
||||
async list_vacations_upcoming(params: {
|
||||
resourceName?: string;
|
||||
chapter?: string;
|
||||
daysAhead?: number;
|
||||
limit?: number;
|
||||
}, ctx: ToolContext) {
|
||||
async list_vacations_upcoming(
|
||||
params: {
|
||||
resourceName?: string;
|
||||
chapter?: string;
|
||||
daysAhead?: number;
|
||||
limit?: number;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const daysAhead = params.daysAhead ?? 30;
|
||||
const limit = Math.min(params.limit ?? 30, 50);
|
||||
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
@@ -470,14 +530,17 @@ export function createVacationHolidayExecutors(
|
||||
}));
|
||||
},
|
||||
|
||||
async list_holidays_by_region(params: {
|
||||
countryCode: string;
|
||||
federalState?: string;
|
||||
metroCity?: string;
|
||||
year?: number;
|
||||
periodStart?: string;
|
||||
periodEnd?: string;
|
||||
}, ctx: ToolContext) {
|
||||
async list_holidays_by_region(
|
||||
params: {
|
||||
countryCode: string;
|
||||
federalState?: string;
|
||||
metroCity?: string;
|
||||
year?: number;
|
||||
periodStart?: string;
|
||||
periodEnd?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const { year, periodStart, periodEnd } = deps.resolveHolidayPeriod(params);
|
||||
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
|
||||
const resolved = await caller.resolveHolidaysDetail({
|
||||
@@ -499,12 +562,15 @@ export function createVacationHolidayExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async get_resource_holidays(params: {
|
||||
identifier: string;
|
||||
year?: number;
|
||||
periodStart?: string;
|
||||
periodEnd?: string;
|
||||
}, ctx: ToolContext) {
|
||||
async get_resource_holidays(
|
||||
params: {
|
||||
identifier: string;
|
||||
year?: number;
|
||||
periodStart?: string;
|
||||
periodEnd?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const resource = await deps.resolveResourceIdentifier(ctx, params.identifier);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
@@ -529,13 +595,16 @@ export function createVacationHolidayExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async list_holiday_calendars(params: {
|
||||
includeInactive?: boolean;
|
||||
countryCode?: string;
|
||||
scopeType?: "COUNTRY" | "STATE" | "CITY";
|
||||
stateCode?: string;
|
||||
metroCity?: string;
|
||||
}, ctx: ToolContext) {
|
||||
async list_holiday_calendars(
|
||||
params: {
|
||||
includeInactive?: boolean;
|
||||
countryCode?: string;
|
||||
scopeType?: "COUNTRY" | "STATE" | "CITY";
|
||||
stateCode?: string;
|
||||
metroCity?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.listCalendarsDetail(params);
|
||||
},
|
||||
@@ -549,12 +618,15 @@ export function createVacationHolidayExecutors(
|
||||
);
|
||||
},
|
||||
|
||||
async preview_resolved_holiday_calendar(params: {
|
||||
countryId: string;
|
||||
stateCode?: string;
|
||||
metroCityId?: string;
|
||||
year: number;
|
||||
}, ctx: ToolContext) {
|
||||
async preview_resolved_holiday_calendar(
|
||||
params: {
|
||||
countryId: string;
|
||||
stateCode?: string;
|
||||
metroCityId?: string;
|
||||
year: number;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const input = PreviewResolvedHolidaysSchema.parse(params);
|
||||
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
|
||||
return deps.resolveEntityOrAssistantError(
|
||||
@@ -563,15 +635,18 @@ export function createVacationHolidayExecutors(
|
||||
);
|
||||
},
|
||||
|
||||
async create_holiday_calendar(params: {
|
||||
name: string;
|
||||
scopeType: "COUNTRY" | "STATE" | "CITY";
|
||||
countryId: string;
|
||||
stateCode?: string;
|
||||
metroCityId?: string;
|
||||
isActive?: boolean;
|
||||
priority?: number;
|
||||
}, ctx: ToolContext) {
|
||||
async create_holiday_calendar(
|
||||
params: {
|
||||
name: string;
|
||||
scopeType: "COUNTRY" | "STATE" | "CITY";
|
||||
countryId: string;
|
||||
stateCode?: string;
|
||||
metroCityId?: string;
|
||||
isActive?: boolean;
|
||||
priority?: number;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertAdminRole(ctx);
|
||||
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
@@ -595,16 +670,19 @@ export function createVacationHolidayExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async update_holiday_calendar(params: {
|
||||
id: string;
|
||||
data: {
|
||||
name?: string;
|
||||
stateCode?: string | null;
|
||||
metroCityId?: string | null;
|
||||
isActive?: boolean;
|
||||
priority?: number;
|
||||
};
|
||||
}, ctx: ToolContext) {
|
||||
async update_holiday_calendar(
|
||||
params: {
|
||||
id: string;
|
||||
data: {
|
||||
name?: string;
|
||||
stateCode?: string | null;
|
||||
metroCityId?: string | null;
|
||||
isActive?: boolean;
|
||||
priority?: number;
|
||||
};
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertAdminRole(ctx);
|
||||
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
|
||||
const input = {
|
||||
@@ -655,13 +733,16 @@ export function createVacationHolidayExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async create_holiday_calendar_entry(params: {
|
||||
holidayCalendarId: string;
|
||||
date: string;
|
||||
name: string;
|
||||
isRecurringAnnual?: boolean;
|
||||
source?: string;
|
||||
}, ctx: ToolContext) {
|
||||
async create_holiday_calendar_entry(
|
||||
params: {
|
||||
holidayCalendarId: string;
|
||||
date: string;
|
||||
name: string;
|
||||
isRecurringAnnual?: boolean;
|
||||
source?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertAdminRole(ctx);
|
||||
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
@@ -685,15 +766,18 @@ export function createVacationHolidayExecutors(
|
||||
};
|
||||
},
|
||||
|
||||
async update_holiday_calendar_entry(params: {
|
||||
id: string;
|
||||
data: {
|
||||
date?: string;
|
||||
name?: string;
|
||||
isRecurringAnnual?: boolean;
|
||||
source?: string | null;
|
||||
};
|
||||
}, ctx: ToolContext) {
|
||||
async update_holiday_calendar_entry(
|
||||
params: {
|
||||
id: string;
|
||||
data: {
|
||||
date?: string;
|
||||
name?: string;
|
||||
isRecurringAnnual?: boolean;
|
||||
source?: string | null;
|
||||
};
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
deps.assertAdminRole(ctx);
|
||||
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
|
||||
const input = {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
/**
|
||||
* AI Assistant router — provides a chat endpoint that uses OpenAI Function Calling
|
||||
* to answer questions about CapaKraken data and modify resources/projects.
|
||||
* to answer questions about Nexus data and modify resources/projects.
|
||||
*/
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
||||
import {
|
||||
TOOL_DEFINITIONS,
|
||||
} from "./assistant-tools.js";
|
||||
import { TOOL_DEFINITIONS } from "./assistant-tools.js";
|
||||
import { type ChatMessage } from "./assistant-confirmation.js";
|
||||
import {
|
||||
assistantChatInputSchema,
|
||||
@@ -41,8 +39,7 @@ export { getAvailableAssistantTools } from "./assistant-tool-policy.js";
|
||||
export { selectAssistantToolsForRequest } from "./assistant-tool-selection.js";
|
||||
|
||||
export const assistantRouter = createTRPCRouter({
|
||||
listPendingApprovals: protectedProcedure
|
||||
.query(({ ctx }) => listPendingApprovalPayloads(ctx)),
|
||||
listPendingApprovals: protectedProcedure.query(({ ctx }) => listPendingApprovalPayloads(ctx)),
|
||||
chat: protectedProcedure
|
||||
.input(assistantChatInputSchema)
|
||||
.mutation(({ ctx, input }) => runAssistantChat(ctx, input)),
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import {
|
||||
PASSWORD_MAX_LENGTH,
|
||||
PASSWORD_MIN_LENGTH,
|
||||
PASSWORD_POLICY_MESSAGE,
|
||||
} from "@capakraken/shared";
|
||||
import { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_POLICY_MESSAGE } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc.js";
|
||||
@@ -15,7 +11,7 @@ const RESET_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
function resetEmailHtml(resetUrl: string): string {
|
||||
return `
|
||||
<p>You requested a password reset for your CapaKraken account.</p>
|
||||
<p>You requested a password reset for your Nexus account.</p>
|
||||
<p>Click the link below to set a new password:</p>
|
||||
<p><a href="${resetUrl}">${resetUrl}</a></p>
|
||||
<p>This link expires in 1 hour and can only be used once.</p>
|
||||
@@ -70,7 +66,7 @@ export const authRouter = createTRPCRouter({
|
||||
|
||||
void sendEmail({
|
||||
to: input.email,
|
||||
subject: "CapaKraken — reset your password",
|
||||
subject: "Nexus — reset your password",
|
||||
text: `You requested a password reset.\n\nReset your password: ${resetUrl}\n\nThis link expires in 1 hour. If you did not request this, ignore this email.`,
|
||||
html: resetEmailHtml(resetUrl),
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CreateBlueprintSchema,
|
||||
RolePresetsSchema,
|
||||
UpdateBlueprintSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { makeAuditLogger } from "../lib/audit-helpers.js";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Prisma, PrismaClient } from "@capakraken/db";
|
||||
import type { Prisma, PrismaClient } from "@nexus/db";
|
||||
import {
|
||||
CreateBlueprintSchema,
|
||||
UpdateBlueprintSchema,
|
||||
type BlueprintFieldDefinition,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { validateCustomFields } from "@capakraken/engine";
|
||||
import { BlueprintTarget, type BlueprintFieldDefinition } from "@capakraken/shared";
|
||||
import { validateCustomFields } from "@nexus/engine";
|
||||
import { BlueprintTarget, type BlueprintFieldDefinition } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
updateBlueprint,
|
||||
updateBlueprintRolePresets,
|
||||
} from "./blueprint-procedure-support.js";
|
||||
import { CreateBlueprintSchema } from "@capakraken/shared";
|
||||
import { CreateBlueprintSchema } from "@nexus/shared";
|
||||
|
||||
export const blueprintRouter = createTRPCRouter({
|
||||
listSummaries: planningReadProcedure.query(({ ctx }) => listBlueprintSummaries(ctx)),
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
CreateCalculationRuleSchema,
|
||||
UpdateCalculationRuleSchema,
|
||||
} from "@capakraken/shared";
|
||||
import { CreateCalculationRuleSchema, UpdateCalculationRuleSchema } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { PROJECT_BRIEF_SELECT } from "../db/selects.js";
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import {
|
||||
CreateCalculationRuleSchema,
|
||||
UpdateCalculationRuleSchema,
|
||||
} from "@capakraken/shared";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import { CreateCalculationRuleSchema, UpdateCalculationRuleSchema } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
type CreateCalculationRuleInput = z.infer<typeof CreateCalculationRuleSchema>;
|
||||
@@ -19,7 +16,9 @@ export function buildCalculationRuleCreateData(
|
||||
...(input.description !== undefined ? { description: input.description } : {}),
|
||||
...(input.projectId !== undefined ? { projectId: input.projectId } : {}),
|
||||
...(input.orderType !== undefined ? { orderType: input.orderType as never } : {}),
|
||||
...(input.costReductionPercent !== undefined ? { costReductionPercent: input.costReductionPercent } : {}),
|
||||
...(input.costReductionPercent !== undefined
|
||||
? { costReductionPercent: input.costReductionPercent }
|
||||
: {}),
|
||||
priority: input.priority,
|
||||
isActive: input.isActive,
|
||||
};
|
||||
@@ -35,8 +34,12 @@ export function buildCalculationRuleUpdateData(
|
||||
...(input.projectId !== undefined ? { projectId: input.projectId } : {}),
|
||||
...(input.orderType !== undefined ? { orderType: input.orderType as never } : {}),
|
||||
...(input.costEffect !== undefined ? { costEffect: input.costEffect } : {}),
|
||||
...(input.costReductionPercent !== undefined ? { costReductionPercent: input.costReductionPercent } : {}),
|
||||
...(input.chargeabilityEffect !== undefined ? { chargeabilityEffect: input.chargeabilityEffect } : {}),
|
||||
...(input.costReductionPercent !== undefined
|
||||
? { costReductionPercent: input.costReductionPercent }
|
||||
: {}),
|
||||
...(input.chargeabilityEffect !== undefined
|
||||
? { chargeabilityEffect: input.chargeabilityEffect }
|
||||
: {}),
|
||||
...(input.priority !== undefined ? { priority: input.priority } : {}),
|
||||
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
||||
};
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
getMonthRange,
|
||||
getMonthKeys,
|
||||
type AssignmentSlice,
|
||||
} from "@capakraken/engine";
|
||||
import type { PrismaClient } from "@capakraken/db";
|
||||
import type { WeekdayAvailability, PermissionKey } from "@capakraken/shared";
|
||||
import { PermissionKey as PermissionKeys, round1 } from "@capakraken/shared";
|
||||
import { isChargeabilityActualBooking, listAssignmentBookings } from "@capakraken/application";
|
||||
} from "@nexus/engine";
|
||||
import type { PrismaClient } from "@nexus/db";
|
||||
import type { WeekdayAvailability, PermissionKey } from "@nexus/shared";
|
||||
import { PermissionKey as PermissionKeys, round1 } from "@nexus/shared";
|
||||
import { isChargeabilityActualBooking, listAssignmentBookings } from "@nexus/application";
|
||||
import { z } from "zod";
|
||||
import { anonymizeResources, getAnonymizationDirectory } from "../lib/anonymization.js";
|
||||
import {
|
||||
@@ -32,19 +32,33 @@ function isIsoDateInMonth(isoDate: string, monthKey: string): boolean {
|
||||
function getMonthCapacityDerivation(input: {
|
||||
monthKey: string;
|
||||
availability: WeekdayAvailability;
|
||||
context: (Awaited<ReturnType<typeof loadResourceDailyAvailabilityContexts>> extends Map<string, infer T> ? T : never) | undefined;
|
||||
context:
|
||||
| (Awaited<ReturnType<typeof loadResourceDailyAvailabilityContexts>> extends Map<
|
||||
string,
|
||||
infer T
|
||||
>
|
||||
? T
|
||||
: never)
|
||||
| undefined;
|
||||
baseAvailableHours: number;
|
||||
effectiveAvailableHours: number;
|
||||
}) {
|
||||
const holidayDates = [...(input.context?.holidayDates ?? new Set<string>())]
|
||||
.filter((isoDate) => isIsoDateInMonth(isoDate, input.monthKey))
|
||||
.sort();
|
||||
const publicHolidayWorkdayCount = holidayDates.reduce((count, isoDate) => (
|
||||
count + (getAvailabilityHoursForDate(input.availability, new Date(`${isoDate}T00:00:00.000Z`)) > 0 ? 1 : 0)
|
||||
), 0);
|
||||
const publicHolidayHoursDeduction = holidayDates.reduce((sum, isoDate) => (
|
||||
sum + getAvailabilityHoursForDate(input.availability, new Date(`${isoDate}T00:00:00.000Z`))
|
||||
), 0);
|
||||
const publicHolidayWorkdayCount = holidayDates.reduce(
|
||||
(count, isoDate) =>
|
||||
count +
|
||||
(getAvailabilityHoursForDate(input.availability, new Date(`${isoDate}T00:00:00.000Z`)) > 0
|
||||
? 1
|
||||
: 0),
|
||||
0,
|
||||
);
|
||||
const publicHolidayHoursDeduction = holidayDates.reduce(
|
||||
(sum, isoDate) =>
|
||||
sum + getAvailabilityHoursForDate(input.availability, new Date(`${isoDate}T00:00:00.000Z`)),
|
||||
0,
|
||||
);
|
||||
|
||||
let absenceDayEquivalent = 0;
|
||||
let absenceHoursDeduction = 0;
|
||||
@@ -52,7 +66,10 @@ function getMonthCapacityDerivation(input: {
|
||||
if (!isIsoDateInMonth(isoDate, input.monthKey) || input.context?.holidayDates.has(isoDate)) {
|
||||
continue;
|
||||
}
|
||||
const dayHours = getAvailabilityHoursForDate(input.availability, new Date(`${isoDate}T00:00:00.000Z`));
|
||||
const dayHours = getAvailabilityHoursForDate(
|
||||
input.availability,
|
||||
new Date(`${isoDate}T00:00:00.000Z`),
|
||||
);
|
||||
if (dayHours <= 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -238,7 +255,9 @@ async function queryChargeabilityReport(
|
||||
departed: false,
|
||||
rolledOff: false,
|
||||
...(input.orgUnitId ? { orgUnitId: input.orgUnitId } : {}),
|
||||
...(input.managementLevelGroupId ? { managementLevelGroupId: input.managementLevelGroupId } : {}),
|
||||
...(input.managementLevelGroupId
|
||||
? { managementLevelGroupId: input.managementLevelGroupId }
|
||||
: {}),
|
||||
...(input.countryId ? { countryId: input.countryId } : {}),
|
||||
};
|
||||
|
||||
@@ -268,12 +287,14 @@ async function queryChargeabilityReport(
|
||||
return {
|
||||
monthKeys,
|
||||
resources: [],
|
||||
groupTotals: monthKeys.map((key) => buildGroupTotal({
|
||||
monthKey: key,
|
||||
totalFte: 0,
|
||||
chargeabilityRatio: 0,
|
||||
targetRatio: 0,
|
||||
})),
|
||||
groupTotals: monthKeys.map((key) =>
|
||||
buildGroupTotal({
|
||||
monthKey: key,
|
||||
totalFte: 0,
|
||||
chargeabilityRatio: 0,
|
||||
targetRatio: 0,
|
||||
}),
|
||||
),
|
||||
explainability: buildChargeabilityExplainability(input),
|
||||
};
|
||||
}
|
||||
@@ -300,12 +321,13 @@ async function queryChargeabilityReport(
|
||||
);
|
||||
|
||||
const projectIds = [...new Set(allBookings.map((booking) => booking.projectId))];
|
||||
const projectUtilCats = projectIds.length > 0
|
||||
? await db.project.findMany({
|
||||
where: { id: { in: projectIds } },
|
||||
select: { id: true, utilizationCategory: { select: { code: true } } },
|
||||
})
|
||||
: [];
|
||||
const projectUtilCats =
|
||||
projectIds.length > 0
|
||||
? await db.project.findMany({
|
||||
where: { id: { in: projectIds } },
|
||||
select: { id: true, utilizationCategory: { select: { code: true } } },
|
||||
})
|
||||
: [];
|
||||
const projectUtilCatMap = new Map(
|
||||
projectUtilCats.map((project) => [project.id, project.utilizationCategory?.code ?? null]),
|
||||
);
|
||||
@@ -324,90 +346,97 @@ async function queryChargeabilityReport(
|
||||
},
|
||||
}));
|
||||
|
||||
const resourceRows = await Promise.all(resources.map(async (resource) => {
|
||||
const resourceAssignments = assignments.filter((assignment) => assignment.resourceId === resource.id);
|
||||
const targetPct = resource.managementLevelGroup?.targetPercentage
|
||||
?? (resource.chargeabilityTarget / 100);
|
||||
const availability = resource.availability as unknown as WeekdayAvailability;
|
||||
const context = availabilityContexts.get(resource.id);
|
||||
const resourceRows = await Promise.all(
|
||||
resources.map(async (resource) => {
|
||||
const resourceAssignments = assignments.filter(
|
||||
(assignment) => assignment.resourceId === resource.id,
|
||||
);
|
||||
const targetPct =
|
||||
resource.managementLevelGroup?.targetPercentage ?? resource.chargeabilityTarget / 100;
|
||||
const availability = resource.availability as unknown as WeekdayAvailability;
|
||||
const context = availabilityContexts.get(resource.id);
|
||||
|
||||
const months = await Promise.all(monthKeys.map(async (key) => {
|
||||
const [year, month] = key.split("-").map(Number) as [number, number];
|
||||
const { start: monthStart, end: monthEnd } = getMonthRange(year, month);
|
||||
const baseAvailableHours = calculateEffectiveAvailableHours({
|
||||
availability,
|
||||
periodStart: monthStart,
|
||||
periodEnd: monthEnd,
|
||||
context: undefined,
|
||||
});
|
||||
const availableHours = calculateEffectiveAvailableHours({
|
||||
availability,
|
||||
periodStart: monthStart,
|
||||
periodEnd: monthEnd,
|
||||
context,
|
||||
});
|
||||
const slices: AssignmentSlice[] = resourceAssignments.flatMap((assignment) => {
|
||||
const totalChargeableHours = calculateEffectiveBookedHours({
|
||||
availability,
|
||||
startDate: assignment.startDate,
|
||||
endDate: assignment.endDate,
|
||||
hoursPerDay: assignment.hoursPerDay,
|
||||
periodStart: monthStart,
|
||||
periodEnd: monthEnd,
|
||||
context,
|
||||
});
|
||||
if (totalChargeableHours <= 0) {
|
||||
return [];
|
||||
}
|
||||
const months = await Promise.all(
|
||||
monthKeys.map(async (key) => {
|
||||
const [year, month] = key.split("-").map(Number) as [number, number];
|
||||
const { start: monthStart, end: monthEnd } = getMonthRange(year, month);
|
||||
const baseAvailableHours = calculateEffectiveAvailableHours({
|
||||
availability,
|
||||
periodStart: monthStart,
|
||||
periodEnd: monthEnd,
|
||||
context: undefined,
|
||||
});
|
||||
const availableHours = calculateEffectiveAvailableHours({
|
||||
availability,
|
||||
periodStart: monthStart,
|
||||
periodEnd: monthEnd,
|
||||
context,
|
||||
});
|
||||
const slices: AssignmentSlice[] = resourceAssignments.flatMap((assignment) => {
|
||||
const totalChargeableHours = calculateEffectiveBookedHours({
|
||||
availability,
|
||||
startDate: assignment.startDate,
|
||||
endDate: assignment.endDate,
|
||||
hoursPerDay: assignment.hoursPerDay,
|
||||
periodStart: monthStart,
|
||||
periodEnd: monthEnd,
|
||||
context,
|
||||
});
|
||||
if (totalChargeableHours <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const categoryCode = assignment.project.utilizationCategory?.code;
|
||||
const categoryCode = assignment.project.utilizationCategory?.code;
|
||||
|
||||
return {
|
||||
hoursPerDay: assignment.hoursPerDay,
|
||||
workingDays: 0,
|
||||
categoryCode: typeof categoryCode === "string" && categoryCode.length > 0 ? categoryCode : "Chg",
|
||||
totalChargeableHours,
|
||||
};
|
||||
});
|
||||
return {
|
||||
hoursPerDay: assignment.hoursPerDay,
|
||||
workingDays: 0,
|
||||
categoryCode:
|
||||
typeof categoryCode === "string" && categoryCode.length > 0 ? categoryCode : "Chg",
|
||||
totalChargeableHours,
|
||||
};
|
||||
});
|
||||
|
||||
const forecast = deriveResourceForecast({
|
||||
const forecast = deriveResourceForecast({
|
||||
fte: resource.fte,
|
||||
targetPercentage: targetPct,
|
||||
assignments: slices,
|
||||
sah: availableHours,
|
||||
});
|
||||
const derivation = getMonthCapacityDerivation({
|
||||
monthKey: key,
|
||||
availability,
|
||||
context,
|
||||
baseAvailableHours,
|
||||
effectiveAvailableHours: availableHours,
|
||||
});
|
||||
|
||||
return buildReportMonth({
|
||||
monthKey: key,
|
||||
sahHours: availableHours,
|
||||
targetRatio: targetPct,
|
||||
forecast,
|
||||
derivation,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
id: resource.id,
|
||||
eid: resource.eid,
|
||||
displayName: resource.displayName,
|
||||
fte: resource.fte,
|
||||
targetPercentage: targetPct,
|
||||
assignments: slices,
|
||||
sah: availableHours,
|
||||
});
|
||||
const derivation = getMonthCapacityDerivation({
|
||||
monthKey: key,
|
||||
availability,
|
||||
context,
|
||||
baseAvailableHours,
|
||||
effectiveAvailableHours: availableHours,
|
||||
});
|
||||
|
||||
return buildReportMonth({
|
||||
monthKey: key,
|
||||
sahHours: availableHours,
|
||||
targetRatio: targetPct,
|
||||
forecast,
|
||||
derivation,
|
||||
});
|
||||
}));
|
||||
|
||||
return {
|
||||
id: resource.id,
|
||||
eid: resource.eid,
|
||||
displayName: resource.displayName,
|
||||
fte: resource.fte,
|
||||
country: resource.country?.code ?? null,
|
||||
federalState: resource.federalState ?? null,
|
||||
city: resource.metroCity?.name ?? null,
|
||||
orgUnit: resource.orgUnit?.name ?? null,
|
||||
mgmtGroup: resource.managementLevelGroup?.name ?? null,
|
||||
mgmtLevel: resource.managementLevel?.name ?? null,
|
||||
targetPct,
|
||||
months,
|
||||
};
|
||||
}));
|
||||
country: resource.country?.code ?? null,
|
||||
federalState: resource.federalState ?? null,
|
||||
city: resource.metroCity?.name ?? null,
|
||||
orgUnit: resource.orgUnit?.name ?? null,
|
||||
mgmtGroup: resource.managementLevelGroup?.name ?? null,
|
||||
mgmtLevel: resource.managementLevel?.name ?? null,
|
||||
targetPct,
|
||||
months,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const groupTotals = monthKeys.map((key, monthIdx) => {
|
||||
const groupInputs = resourceRows.map((resource) => ({
|
||||
@@ -446,10 +475,11 @@ export function buildChargeabilityReportDetail(
|
||||
) {
|
||||
const resourceQuery = input.resourceQuery?.trim().toLowerCase();
|
||||
const matchingResources = resourceQuery
|
||||
? report.resources.filter((resource) => (
|
||||
resource.displayName.toLowerCase().includes(resourceQuery)
|
||||
|| resource.eid.toLowerCase().includes(resourceQuery)
|
||||
))
|
||||
? report.resources.filter(
|
||||
(resource) =>
|
||||
resource.displayName.toLowerCase().includes(resourceQuery) ||
|
||||
resource.eid.toLowerCase().includes(resourceQuery),
|
||||
)
|
||||
: report.resources;
|
||||
const resourceLimit = Math.min(Math.max(input.resourceLimit ?? 25, 1), 100);
|
||||
const resources = matchingResources.slice(0, resourceLimit).map((resource) => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateClientSchema, UpdateClientSchema } from "@capakraken/shared";
|
||||
import { CreateClientSchema, UpdateClientSchema } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Prisma, PrismaClient } from "@capakraken/db";
|
||||
import { CreateClientSchema, UpdateClientSchema, type ClientTree } from "@capakraken/shared";
|
||||
import type { Prisma, PrismaClient } from "@nexus/db";
|
||||
import { CreateClientSchema, UpdateClientSchema, type ClientTree } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -68,27 +68,27 @@ export async function findClientByIdentifier<TClient>(
|
||||
): Promise<TClient> {
|
||||
const normalizedIdentifier = identifier.trim();
|
||||
|
||||
let client = await db.client.findUnique({
|
||||
let client = (await db.client.findUnique({
|
||||
where: { id: normalizedIdentifier },
|
||||
...extraArgs,
|
||||
}) as TClient | null;
|
||||
})) as TClient | null;
|
||||
|
||||
if (!client) {
|
||||
client = await db.client.findUnique({
|
||||
client = (await db.client.findUnique({
|
||||
where: { code: normalizedIdentifier },
|
||||
...extraArgs,
|
||||
}) as TClient | null;
|
||||
})) as TClient | null;
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
client = await db.client.findFirst({
|
||||
client = (await db.client.findFirst({
|
||||
where: { name: { equals: normalizedIdentifier, mode: "insensitive" } },
|
||||
...extraArgs,
|
||||
}) as TClient | null;
|
||||
})) as TClient | null;
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
client = await db.client.findFirst({
|
||||
client = (await db.client.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ name: { contains: normalizedIdentifier, mode: "insensitive" } },
|
||||
@@ -96,7 +96,7 @@ export async function findClientByIdentifier<TClient>(
|
||||
],
|
||||
},
|
||||
...extraArgs,
|
||||
}) as TClient | null;
|
||||
})) as TClient | null;
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
@@ -109,9 +109,7 @@ export async function findClientByIdentifier<TClient>(
|
||||
return client;
|
||||
}
|
||||
|
||||
export function buildClientCreateData(
|
||||
input: CreateClientInput,
|
||||
): Prisma.ClientUncheckedCreateInput {
|
||||
export function buildClientCreateData(input: CreateClientInput): Prisma.ClientUncheckedCreateInput {
|
||||
return {
|
||||
name: input.name,
|
||||
...(input.code ? { code: input.code } : {}),
|
||||
@@ -121,9 +119,7 @@ export function buildClientCreateData(
|
||||
};
|
||||
}
|
||||
|
||||
export function buildClientUpdateData(
|
||||
input: UpdateClientInput,
|
||||
): Prisma.ClientUncheckedUpdateInput {
|
||||
export function buildClientUpdateData(input: UpdateClientInput): Prisma.ClientUncheckedUpdateInput {
|
||||
return {
|
||||
...(input.name !== undefined ? { name: input.name } : {}),
|
||||
...(input.code !== undefined ? { code: input.code } : {}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateClientSchema } from "@capakraken/shared";
|
||||
import { CreateClientSchema } from "@nexus/shared";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
computeEvenSpread,
|
||||
distributeHoursToWeeks,
|
||||
summarizeEstimateDemandLines,
|
||||
} from "@capakraken/engine";
|
||||
} from "@nexus/engine";
|
||||
import { fmtEur } from "../lib/format-utils.js";
|
||||
import type { ProjectGraphSnapshot } from "./computation-graph-project-snapshot.js";
|
||||
import { type GraphLink, type GraphNode, fmtNum, l, n } from "./computation-graph-shared.js";
|
||||
@@ -33,7 +33,13 @@ export function buildProjectEstimateGraph(input: ProjectEstimateGraphInput): {
|
||||
|
||||
const lines = latestVersion.demandLines;
|
||||
const summary = summarizeEstimateDemandLines(lines);
|
||||
const { totalHours, totalCostCents, totalPriceCents, marginCents, marginPercent: marginPct } = summary;
|
||||
const {
|
||||
totalHours,
|
||||
totalCostCents,
|
||||
totalPriceCents,
|
||||
marginCents,
|
||||
marginPercent: marginPct,
|
||||
} = summary;
|
||||
|
||||
const avgCostRate = totalHours > 0 ? Math.round(totalCostCents / totalHours) : 0;
|
||||
const avgBillRate = totalHours > 0 ? Math.round(totalPriceCents / totalHours) : 0;
|
||||
@@ -47,20 +53,109 @@ export function buildProjectEstimateGraph(input: ProjectEstimateGraphInput): {
|
||||
const snapshotCount = latestVersion.resourceSnapshots?.length ?? 0;
|
||||
|
||||
nodes.push(
|
||||
n("input.estLines", "Demand Lines", `${lines.length}`, "count", "INPUT", "Estimate demand line count", 0),
|
||||
n("input.avgCostRate", "Avg Cost Rate", fmtEur(avgCostRate), "cents/h", "INPUT", "Average cost rate across demand lines", 0),
|
||||
n("input.avgBillRate", "Avg Bill Rate", fmtEur(avgBillRate), "cents/h", "INPUT", "Average bill rate across demand lines", 0),
|
||||
...(snapshotCount > 0 ? [
|
||||
n("input.resourceSnapshots", "Res. Snapshots", `${snapshotCount}`, "count", "INPUT", "Resource rate snapshots frozen in estimate version", 0),
|
||||
] : []),
|
||||
n("est.totalHours", "Est. Hours", fmtNum(totalHours), "hours", "ESTIMATE", "Total estimated hours", 2, "Σ(line.hours)"),
|
||||
n("est.totalCostCents", "Est. Cost", fmtEur(totalCostCents), "EUR", "ESTIMATE", "Total estimated cost", 2, "Σ(hours × costRate)"),
|
||||
n("est.totalPriceCents", "Est. Price", fmtEur(totalPriceCents), "EUR", "ESTIMATE", "Total estimated price", 2, "Σ(hours × billRate)"),
|
||||
n("est.marginCents", "Margin", fmtEur(marginCents), "EUR", "ESTIMATE", "Price minus cost", 3, "price - cost"),
|
||||
n("est.marginPercent", "Margin %", `${marginPct.toFixed(1)}%`, "%", "ESTIMATE", "Margin as percentage of price", 3, "margin / price × 100"),
|
||||
...(chapterCount > 1 ? [
|
||||
n("est.chapters", "Chapters", `${chapterCount}`, "count", "ESTIMATE", `Demand lines grouped by ${chapterCount} chapters`, 1),
|
||||
] : []),
|
||||
n(
|
||||
"input.estLines",
|
||||
"Demand Lines",
|
||||
`${lines.length}`,
|
||||
"count",
|
||||
"INPUT",
|
||||
"Estimate demand line count",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.avgCostRate",
|
||||
"Avg Cost Rate",
|
||||
fmtEur(avgCostRate),
|
||||
"cents/h",
|
||||
"INPUT",
|
||||
"Average cost rate across demand lines",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.avgBillRate",
|
||||
"Avg Bill Rate",
|
||||
fmtEur(avgBillRate),
|
||||
"cents/h",
|
||||
"INPUT",
|
||||
"Average bill rate across demand lines",
|
||||
0,
|
||||
),
|
||||
...(snapshotCount > 0
|
||||
? [
|
||||
n(
|
||||
"input.resourceSnapshots",
|
||||
"Res. Snapshots",
|
||||
`${snapshotCount}`,
|
||||
"count",
|
||||
"INPUT",
|
||||
"Resource rate snapshots frozen in estimate version",
|
||||
0,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
n(
|
||||
"est.totalHours",
|
||||
"Est. Hours",
|
||||
fmtNum(totalHours),
|
||||
"hours",
|
||||
"ESTIMATE",
|
||||
"Total estimated hours",
|
||||
2,
|
||||
"Σ(line.hours)",
|
||||
),
|
||||
n(
|
||||
"est.totalCostCents",
|
||||
"Est. Cost",
|
||||
fmtEur(totalCostCents),
|
||||
"EUR",
|
||||
"ESTIMATE",
|
||||
"Total estimated cost",
|
||||
2,
|
||||
"Σ(hours × costRate)",
|
||||
),
|
||||
n(
|
||||
"est.totalPriceCents",
|
||||
"Est. Price",
|
||||
fmtEur(totalPriceCents),
|
||||
"EUR",
|
||||
"ESTIMATE",
|
||||
"Total estimated price",
|
||||
2,
|
||||
"Σ(hours × billRate)",
|
||||
),
|
||||
n(
|
||||
"est.marginCents",
|
||||
"Margin",
|
||||
fmtEur(marginCents),
|
||||
"EUR",
|
||||
"ESTIMATE",
|
||||
"Price minus cost",
|
||||
3,
|
||||
"price - cost",
|
||||
),
|
||||
n(
|
||||
"est.marginPercent",
|
||||
"Margin %",
|
||||
`${marginPct.toFixed(1)}%`,
|
||||
"%",
|
||||
"ESTIMATE",
|
||||
"Margin as percentage of price",
|
||||
3,
|
||||
"margin / price × 100",
|
||||
),
|
||||
...(chapterCount > 1
|
||||
? [
|
||||
n(
|
||||
"est.chapters",
|
||||
"Chapters",
|
||||
`${chapterCount}`,
|
||||
"count",
|
||||
"ESTIMATE",
|
||||
`Demand lines grouped by ${chapterCount} chapters`,
|
||||
1,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
);
|
||||
|
||||
links.push(
|
||||
@@ -73,56 +168,159 @@ export function buildProjectEstimateGraph(input: ProjectEstimateGraphInput): {
|
||||
l("est.totalCostCents", "est.marginCents", "−", 2),
|
||||
l("est.marginCents", "est.marginPercent", "÷ price × 100", 2),
|
||||
l("est.totalPriceCents", "est.marginPercent", "÷", 1),
|
||||
...(snapshotCount > 0 ? [
|
||||
l("input.resourceSnapshots", "input.avgCostRate", "LCR snapshot", 1),
|
||||
l("input.resourceSnapshots", "input.avgBillRate", "UCR snapshot", 1),
|
||||
] : []),
|
||||
...(chapterCount > 1 ? [
|
||||
l("input.estLines", "est.chapters", "group by", 1),
|
||||
l("est.chapters", "est.totalHours", "Σ per chapter", 1),
|
||||
] : []),
|
||||
...(snapshotCount > 0
|
||||
? [
|
||||
l("input.resourceSnapshots", "input.avgCostRate", "LCR snapshot", 1),
|
||||
l("input.resourceSnapshots", "input.avgBillRate", "UCR snapshot", 1),
|
||||
]
|
||||
: []),
|
||||
...(chapterCount > 1
|
||||
? [
|
||||
l("input.estLines", "est.chapters", "group by", 1),
|
||||
l("est.chapters", "est.totalHours", "Σ per chapter", 1),
|
||||
]
|
||||
: []),
|
||||
);
|
||||
|
||||
const scopeItems = latestVersion.scopeItems ?? [];
|
||||
if (scopeItems.length > 0) {
|
||||
const totalFrameCount = scopeItems.reduce((sum, scopeItem) => sum + (scopeItem.frameCount ?? 0), 0);
|
||||
const totalItemCount = scopeItems.reduce((sum, scopeItem) => sum + (scopeItem.itemCount ?? 0), 0);
|
||||
const totalFrameCount = scopeItems.reduce(
|
||||
(sum, scopeItem) => sum + (scopeItem.frameCount ?? 0),
|
||||
0,
|
||||
);
|
||||
const totalItemCount = scopeItems.reduce(
|
||||
(sum, scopeItem) => sum + (scopeItem.itemCount ?? 0),
|
||||
0,
|
||||
);
|
||||
const scopeTypes = new Set(scopeItems.map((scopeItem) => scopeItem.scopeType));
|
||||
|
||||
nodes.push(
|
||||
n("effort.scopeItems", "Scope Items", `${scopeItems.length}`, "count", "EFFORT", `${scopeItems.length} scope items across ${scopeTypes.size} type(s)`, 0),
|
||||
...(totalFrameCount > 0 ? [
|
||||
n("effort.totalFrames", "Total Frames", `${totalFrameCount}`, "frames", "EFFORT", "Sum of frame counts across scope items", 1),
|
||||
] : []),
|
||||
...(totalItemCount > 0 ? [
|
||||
n("effort.totalItems", "Total Items", fmtNum(totalItemCount), "items", "EFFORT", "Sum of item counts across scope items", 1),
|
||||
] : []),
|
||||
n("effort.effortRules", "Effort Rules", `${effortRuleCount}`, "count", "EFFORT", "Configured effort expansion rules (scopeType → discipline)", 0),
|
||||
n("effort.expandedHours", "Expanded Hours", fmtNum(totalHours), "hours", "EFFORT", "Total hours from scope-to-effort expansion (unitCount × hoursPerUnit)", 2, "Σ(unitCount × hoursPerUnit)"),
|
||||
n(
|
||||
"effort.scopeItems",
|
||||
"Scope Items",
|
||||
`${scopeItems.length}`,
|
||||
"count",
|
||||
"EFFORT",
|
||||
`${scopeItems.length} scope items across ${scopeTypes.size} type(s)`,
|
||||
0,
|
||||
),
|
||||
...(totalFrameCount > 0
|
||||
? [
|
||||
n(
|
||||
"effort.totalFrames",
|
||||
"Total Frames",
|
||||
`${totalFrameCount}`,
|
||||
"frames",
|
||||
"EFFORT",
|
||||
"Sum of frame counts across scope items",
|
||||
1,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(totalItemCount > 0
|
||||
? [
|
||||
n(
|
||||
"effort.totalItems",
|
||||
"Total Items",
|
||||
fmtNum(totalItemCount),
|
||||
"items",
|
||||
"EFFORT",
|
||||
"Sum of item counts across scope items",
|
||||
1,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
n(
|
||||
"effort.effortRules",
|
||||
"Effort Rules",
|
||||
`${effortRuleCount}`,
|
||||
"count",
|
||||
"EFFORT",
|
||||
"Configured effort expansion rules (scopeType → discipline)",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"effort.expandedHours",
|
||||
"Expanded Hours",
|
||||
fmtNum(totalHours),
|
||||
"hours",
|
||||
"EFFORT",
|
||||
"Total hours from scope-to-effort expansion (unitCount × hoursPerUnit)",
|
||||
2,
|
||||
"Σ(unitCount × hoursPerUnit)",
|
||||
),
|
||||
);
|
||||
|
||||
links.push(
|
||||
l("effort.scopeItems", "effort.expandedHours", "expand", 2),
|
||||
l("effort.effortRules", "effort.expandedHours", "× hoursPerUnit", 2),
|
||||
...(totalFrameCount > 0 ? [
|
||||
l("effort.scopeItems", "effort.totalFrames", "Σ frames", 1),
|
||||
l("effort.totalFrames", "effort.expandedHours", "per_frame", 1),
|
||||
] : []),
|
||||
...(totalItemCount > 0 ? [
|
||||
l("effort.scopeItems", "effort.totalItems", "Σ items", 1),
|
||||
l("effort.totalItems", "effort.expandedHours", "per_item", 1),
|
||||
] : []),
|
||||
...(totalFrameCount > 0
|
||||
? [
|
||||
l("effort.scopeItems", "effort.totalFrames", "Σ frames", 1),
|
||||
l("effort.totalFrames", "effort.expandedHours", "per_frame", 1),
|
||||
]
|
||||
: []),
|
||||
...(totalItemCount > 0
|
||||
? [
|
||||
l("effort.scopeItems", "effort.totalItems", "Σ items", 1),
|
||||
l("effort.totalItems", "effort.expandedHours", "per_item", 1),
|
||||
]
|
||||
: []),
|
||||
l("effort.expandedHours", "est.totalHours", "→ demand lines", 2),
|
||||
);
|
||||
}
|
||||
|
||||
if (experienceRuleCount > 0) {
|
||||
nodes.push(
|
||||
n("exp.ruleCount", "Exp. Rules", `${experienceRuleCount}`, "count", "EXPERIENCE", "Experience multiplier rules (chapter/location/level → rate adjustments)", 0),
|
||||
n("exp.costMultiplier", "Cost Multiplier", "per rule", "×", "EXPERIENCE", "Multiplier applied to cost rate (costRateCents × multiplier)", 1, "costRate × costMultiplier"),
|
||||
n("exp.billMultiplier", "Bill Multiplier", "per rule", "×", "EXPERIENCE", "Multiplier applied to bill rate (billRateCents × multiplier)", 1, "billRate × billMultiplier"),
|
||||
n("exp.shoringRatio", "Shoring Ratio", "per rule", "ratio", "EXPERIENCE", "Offshore/nearshore effort factor (onsiteHours + offshoreHours × (1 + additionalEffort))", 2, "onsite + offshore × (1 + addlEffort)"),
|
||||
n("exp.adjustedRates", "Adjusted Rates", "applied", "—", "EXPERIENCE", "Final cost and bill rates after experience multipliers", 2, "rate × multiplier"),
|
||||
n(
|
||||
"exp.ruleCount",
|
||||
"Exp. Rules",
|
||||
`${experienceRuleCount}`,
|
||||
"count",
|
||||
"EXPERIENCE",
|
||||
"Experience multiplier rules (chapter/location/level → rate adjustments)",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"exp.costMultiplier",
|
||||
"Cost Multiplier",
|
||||
"per rule",
|
||||
"×",
|
||||
"EXPERIENCE",
|
||||
"Multiplier applied to cost rate (costRateCents × multiplier)",
|
||||
1,
|
||||
"costRate × costMultiplier",
|
||||
),
|
||||
n(
|
||||
"exp.billMultiplier",
|
||||
"Bill Multiplier",
|
||||
"per rule",
|
||||
"×",
|
||||
"EXPERIENCE",
|
||||
"Multiplier applied to bill rate (billRateCents × multiplier)",
|
||||
1,
|
||||
"billRate × billMultiplier",
|
||||
),
|
||||
n(
|
||||
"exp.shoringRatio",
|
||||
"Shoring Ratio",
|
||||
"per rule",
|
||||
"ratio",
|
||||
"EXPERIENCE",
|
||||
"Offshore/nearshore effort factor (onsiteHours + offshoreHours × (1 + additionalEffort))",
|
||||
2,
|
||||
"onsite + offshore × (1 + addlEffort)",
|
||||
),
|
||||
n(
|
||||
"exp.adjustedRates",
|
||||
"Adjusted Rates",
|
||||
"applied",
|
||||
"—",
|
||||
"EXPERIENCE",
|
||||
"Final cost and bill rates after experience multipliers",
|
||||
2,
|
||||
"rate × multiplier",
|
||||
),
|
||||
);
|
||||
|
||||
links.push(
|
||||
@@ -147,7 +345,8 @@ export function buildProjectEstimateGraph(input: ProjectEstimateGraphInput): {
|
||||
} | null;
|
||||
|
||||
const hasCommercialAdjustments = terms && (terms.contingencyPercent || terms.discountPercent);
|
||||
const hasCommercialMeta = terms && (terms.pricingModel || terms.paymentTermDays || terms.warrantyMonths);
|
||||
const hasCommercialMeta =
|
||||
terms && (terms.pricingModel || terms.paymentTermDays || terms.warrantyMonths);
|
||||
|
||||
if (hasCommercialAdjustments) {
|
||||
const contingencyPct = terms!.contingencyPercent ?? 0;
|
||||
@@ -160,14 +359,84 @@ export function buildProjectEstimateGraph(input: ProjectEstimateGraphInput): {
|
||||
const adjustedMarginPct = adjustedPrice > 0 ? (adjustedMargin / adjustedPrice) * 100 : 0;
|
||||
|
||||
nodes.push(
|
||||
n("input.contingencyPct", "Contingency %", `${contingencyPct}%`, "%", "INPUT", "Contingency percentage (risk buffer on cost)", 0),
|
||||
n("input.discountPct", "Discount %", `${discountPct}%`, "%", "INPUT", "Discount percentage (reduction on sell side)", 0),
|
||||
n("comm.contingencyCents", "Contingency", fmtEur(contingencyCents), "EUR", "COMMERCIAL", "Contingency surcharge", 2, "baseCost × contingency%"),
|
||||
n("comm.discountCents", "Discount", fmtEur(discountCents), "EUR", "COMMERCIAL", "Discount deduction", 2, "basePrice × discount%"),
|
||||
n("comm.adjustedCost", "Adj. Cost", fmtEur(adjustedCost), "EUR", "COMMERCIAL", "Cost plus contingency", 3, "baseCost + contingency"),
|
||||
n("comm.adjustedPrice", "Adj. Price", fmtEur(adjustedPrice), "EUR", "COMMERCIAL", "Price minus discount", 3, "basePrice - discount"),
|
||||
n("comm.adjustedMargin", "Adj. Margin", fmtEur(adjustedMargin), "EUR", "COMMERCIAL", "Adjusted margin", 3, "adjPrice - adjCost"),
|
||||
n("comm.adjustedMarginPct", "Adj. Margin %", `${adjustedMarginPct.toFixed(1)}%`, "%", "COMMERCIAL", "Adjusted margin percentage", 3, "adjMargin / adjPrice × 100"),
|
||||
n(
|
||||
"input.contingencyPct",
|
||||
"Contingency %",
|
||||
`${contingencyPct}%`,
|
||||
"%",
|
||||
"INPUT",
|
||||
"Contingency percentage (risk buffer on cost)",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.discountPct",
|
||||
"Discount %",
|
||||
`${discountPct}%`,
|
||||
"%",
|
||||
"INPUT",
|
||||
"Discount percentage (reduction on sell side)",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"comm.contingencyCents",
|
||||
"Contingency",
|
||||
fmtEur(contingencyCents),
|
||||
"EUR",
|
||||
"COMMERCIAL",
|
||||
"Contingency surcharge",
|
||||
2,
|
||||
"baseCost × contingency%",
|
||||
),
|
||||
n(
|
||||
"comm.discountCents",
|
||||
"Discount",
|
||||
fmtEur(discountCents),
|
||||
"EUR",
|
||||
"COMMERCIAL",
|
||||
"Discount deduction",
|
||||
2,
|
||||
"basePrice × discount%",
|
||||
),
|
||||
n(
|
||||
"comm.adjustedCost",
|
||||
"Adj. Cost",
|
||||
fmtEur(adjustedCost),
|
||||
"EUR",
|
||||
"COMMERCIAL",
|
||||
"Cost plus contingency",
|
||||
3,
|
||||
"baseCost + contingency",
|
||||
),
|
||||
n(
|
||||
"comm.adjustedPrice",
|
||||
"Adj. Price",
|
||||
fmtEur(adjustedPrice),
|
||||
"EUR",
|
||||
"COMMERCIAL",
|
||||
"Price minus discount",
|
||||
3,
|
||||
"basePrice - discount",
|
||||
),
|
||||
n(
|
||||
"comm.adjustedMargin",
|
||||
"Adj. Margin",
|
||||
fmtEur(adjustedMargin),
|
||||
"EUR",
|
||||
"COMMERCIAL",
|
||||
"Adjusted margin",
|
||||
3,
|
||||
"adjPrice - adjCost",
|
||||
),
|
||||
n(
|
||||
"comm.adjustedMarginPct",
|
||||
"Adj. Margin %",
|
||||
`${adjustedMarginPct.toFixed(1)}%`,
|
||||
"%",
|
||||
"COMMERCIAL",
|
||||
"Adjusted margin percentage",
|
||||
3,
|
||||
"adjMargin / adjPrice × 100",
|
||||
),
|
||||
);
|
||||
|
||||
links.push(
|
||||
@@ -189,27 +458,73 @@ export function buildProjectEstimateGraph(input: ProjectEstimateGraphInput): {
|
||||
if (hasCommercialMeta || (terms?.paymentMilestones && terms.paymentMilestones.length > 0)) {
|
||||
if (terms!.pricingModel) {
|
||||
nodes.push(
|
||||
n("comm.pricingModel", "Pricing Model", terms!.pricingModel.replace(/_/g, " "), "—", "COMMERCIAL", `Pricing model: ${terms!.pricingModel}`, 0),
|
||||
n(
|
||||
"comm.pricingModel",
|
||||
"Pricing Model",
|
||||
terms!.pricingModel.replace(/_/g, " "),
|
||||
"—",
|
||||
"COMMERCIAL",
|
||||
`Pricing model: ${terms!.pricingModel}`,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (terms!.paymentTermDays) {
|
||||
nodes.push(
|
||||
n("comm.paymentTermDays", "Payment Terms", `${terms!.paymentTermDays} days`, "days", "COMMERCIAL", `Net payment terms: ${terms!.paymentTermDays} days`, 0),
|
||||
n(
|
||||
"comm.paymentTermDays",
|
||||
"Payment Terms",
|
||||
`${terms!.paymentTermDays} days`,
|
||||
"days",
|
||||
"COMMERCIAL",
|
||||
`Net payment terms: ${terms!.paymentTermDays} days`,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (terms!.warrantyMonths) {
|
||||
nodes.push(
|
||||
n("comm.warrantyMonths", "Warranty", `${terms!.warrantyMonths} mo`, "months", "COMMERCIAL", `Warranty period: ${terms!.warrantyMonths} months`, 0),
|
||||
n(
|
||||
"comm.warrantyMonths",
|
||||
"Warranty",
|
||||
`${terms!.warrantyMonths} mo`,
|
||||
"months",
|
||||
"COMMERCIAL",
|
||||
`Warranty period: ${terms!.warrantyMonths} months`,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
const milestones = terms!.paymentMilestones ?? [];
|
||||
if (milestones.length > 0) {
|
||||
nodes.push(
|
||||
n("comm.milestones", "Milestones", `${milestones.length}`, "count", "COMMERCIAL", `${milestones.length} payment milestones (${milestones.map((milestone) => `${milestone.label}: ${milestone.percent}%`).join(", ")})`, 2),
|
||||
n("comm.milestoneTotalPct", "Milestone Sum", `${milestones.reduce((sum, milestone) => sum + milestone.percent, 0).toFixed(0)}%`, "%", "COMMERCIAL", "Sum of milestone percentages (should be 100%)", 2, "Σ(milestone.percent)"),
|
||||
n(
|
||||
"comm.milestones",
|
||||
"Milestones",
|
||||
`${milestones.length}`,
|
||||
"count",
|
||||
"COMMERCIAL",
|
||||
`${milestones.length} payment milestones (${milestones.map((milestone) => `${milestone.label}: ${milestone.percent}%`).join(", ")})`,
|
||||
2,
|
||||
),
|
||||
n(
|
||||
"comm.milestoneTotalPct",
|
||||
"Milestone Sum",
|
||||
`${milestones.reduce((sum, milestone) => sum + milestone.percent, 0).toFixed(0)}%`,
|
||||
"%",
|
||||
"COMMERCIAL",
|
||||
"Sum of milestone percentages (should be 100%)",
|
||||
2,
|
||||
"Σ(milestone.percent)",
|
||||
),
|
||||
);
|
||||
links.push(
|
||||
l(hasCommercialAdjustments ? "comm.adjustedPrice" : "est.totalPriceCents", "comm.milestones", "× %", 1),
|
||||
l(
|
||||
hasCommercialAdjustments ? "comm.adjustedPrice" : "est.totalPriceCents",
|
||||
"comm.milestones",
|
||||
"× %",
|
||||
1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -232,11 +547,54 @@ export function buildProjectEstimateGraph(input: ProjectEstimateGraphInput): {
|
||||
});
|
||||
|
||||
nodes.push(
|
||||
n("spread.monthCount", "Months", `${spreadResult.months.length}`, "count", "SPREAD", `${spreadResult.months.length} months in project date range`, 1),
|
||||
n("spread.weekCount", "Weeks", `${weeklyResult.weeks.length}`, "count", "SPREAD", `${weeklyResult.weeks.length} ISO weeks in project date range`, 1),
|
||||
n("spread.monthlySpread", "Monthly Spread", hasManualSpreads ? "manual + even" : "even", "—", "SPREAD", "Hours distributed across months weighted by working days", 2, "hours × (monthWorkDays / totalWorkDays)"),
|
||||
n("spread.weeklyPhasing", "Weekly Phasing", "even", "—", "SPREAD", "Hours distributed across ISO weeks (even/front/back-loaded)", 2, "totalHours / weekCount"),
|
||||
n("spread.totalDistributed", "Distributed Hours", fmtNum(weeklyResult.totalDistributedHours), "hours", "SPREAD", "Total hours after weekly distribution (should match estimate)", 3, "Σ(weeklyHours)"),
|
||||
n(
|
||||
"spread.monthCount",
|
||||
"Months",
|
||||
`${spreadResult.months.length}`,
|
||||
"count",
|
||||
"SPREAD",
|
||||
`${spreadResult.months.length} months in project date range`,
|
||||
1,
|
||||
),
|
||||
n(
|
||||
"spread.weekCount",
|
||||
"Weeks",
|
||||
`${weeklyResult.weeks.length}`,
|
||||
"count",
|
||||
"SPREAD",
|
||||
`${weeklyResult.weeks.length} ISO weeks in project date range`,
|
||||
1,
|
||||
),
|
||||
n(
|
||||
"spread.monthlySpread",
|
||||
"Monthly Spread",
|
||||
hasManualSpreads ? "manual + even" : "even",
|
||||
"—",
|
||||
"SPREAD",
|
||||
"Hours distributed across months weighted by working days",
|
||||
2,
|
||||
"hours × (monthWorkDays / totalWorkDays)",
|
||||
),
|
||||
n(
|
||||
"spread.weeklyPhasing",
|
||||
"Weekly Phasing",
|
||||
"even",
|
||||
"—",
|
||||
"SPREAD",
|
||||
"Hours distributed across ISO weeks (even/front/back-loaded)",
|
||||
2,
|
||||
"totalHours / weekCount",
|
||||
),
|
||||
n(
|
||||
"spread.totalDistributed",
|
||||
"Distributed Hours",
|
||||
fmtNum(weeklyResult.totalDistributedHours),
|
||||
"hours",
|
||||
"SPREAD",
|
||||
"Total hours after weekly distribution (should match estimate)",
|
||||
3,
|
||||
"Σ(weeklyHours)",
|
||||
),
|
||||
);
|
||||
|
||||
links.push(
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { computeBudgetStatus } from "@capakraken/engine";
|
||||
import { computeBudgetStatus } from "@nexus/engine";
|
||||
import { fmtEur } from "../lib/format-utils.js";
|
||||
import { buildProjectEstimateGraph } from "./computation-graph-project-estimate.js";
|
||||
import { loadProjectGraphSnapshot, type ProjectGraphInput } from "./computation-graph-project-snapshot.js";
|
||||
import {
|
||||
loadProjectGraphSnapshot,
|
||||
type ProjectGraphInput,
|
||||
} from "./computation-graph-project-snapshot.js";
|
||||
import { type GraphLink, type GraphNode, l, n } from "./computation-graph-shared.js";
|
||||
|
||||
export async function readProjectGraphSnapshot(
|
||||
ctx: Parameters<typeof loadProjectGraphSnapshot>[0],
|
||||
input: ProjectGraphInput,
|
||||
) {
|
||||
const {
|
||||
project,
|
||||
latestVersion,
|
||||
effortRuleCount,
|
||||
experienceRuleCount,
|
||||
projectAllocations,
|
||||
} = await loadProjectGraphSnapshot(ctx, input);
|
||||
const { project, latestVersion, effortRuleCount, experienceRuleCount, projectAllocations } =
|
||||
await loadProjectGraphSnapshot(ctx, input);
|
||||
|
||||
const nodes: GraphNode[] = [];
|
||||
const links: GraphLink[] = [];
|
||||
@@ -22,12 +20,46 @@ export async function readProjectGraphSnapshot(
|
||||
const hasBudget = project.budgetCents > 0;
|
||||
const hasDateRange = !!(project.startDate && project.endDate);
|
||||
nodes.push(
|
||||
n("input.budgetCents", "Project Budget", hasBudget ? fmtEur(project.budgetCents) : "Not set", hasBudget ? "EUR" : "—", "INPUT", hasBudget ? `Budget for ${project.name}` : `No budget defined for ${project.name}`, 0),
|
||||
n("input.winProbability", "Win Probability", `${project.winProbability}%`, "%", "INPUT", "Project win probability", 0),
|
||||
...(hasDateRange ? [
|
||||
n("input.projectStart", "Project Start", project.startDate!.toISOString().slice(0, 10), "date", "INPUT", "Project start date", 0),
|
||||
n("input.projectEnd", "Project End", project.endDate!.toISOString().slice(0, 10), "date", "INPUT", "Project end date", 0),
|
||||
] : []),
|
||||
n(
|
||||
"input.budgetCents",
|
||||
"Project Budget",
|
||||
hasBudget ? fmtEur(project.budgetCents) : "Not set",
|
||||
hasBudget ? "EUR" : "—",
|
||||
"INPUT",
|
||||
hasBudget ? `Budget for ${project.name}` : `No budget defined for ${project.name}`,
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.winProbability",
|
||||
"Win Probability",
|
||||
`${project.winProbability}%`,
|
||||
"%",
|
||||
"INPUT",
|
||||
"Project win probability",
|
||||
0,
|
||||
),
|
||||
...(hasDateRange
|
||||
? [
|
||||
n(
|
||||
"input.projectStart",
|
||||
"Project Start",
|
||||
project.startDate!.toISOString().slice(0, 10),
|
||||
"date",
|
||||
"INPUT",
|
||||
"Project start date",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.projectEnd",
|
||||
"Project End",
|
||||
project.endDate!.toISOString().slice(0, 10),
|
||||
"date",
|
||||
"INPUT",
|
||||
"Project end date",
|
||||
0,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
);
|
||||
|
||||
const estimateGraph = buildProjectEstimateGraph({
|
||||
@@ -55,13 +87,75 @@ export async function readProjectGraphSnapshot(
|
||||
);
|
||||
|
||||
nodes.push(
|
||||
n("budget.confirmedCents", "Confirmed", fmtEur(budgetStatus.confirmedCents), "EUR", "BUDGET", "Confirmed allocation costs", 2, "Σ(CONFIRMED allocs)"),
|
||||
n("budget.proposedCents", "Proposed", fmtEur(budgetStatus.proposedCents), "EUR", "BUDGET", "Proposed allocation costs", 2, "Σ(PROPOSED allocs)"),
|
||||
n("budget.allocatedCents", "Allocated", fmtEur(budgetStatus.allocatedCents), "EUR", "BUDGET", "Total allocated", 2, "confirmed + proposed"),
|
||||
n("budget.remainingCents", "Remaining", hasBudget ? fmtEur(budgetStatus.remainingCents) : "N/A", hasBudget ? "EUR" : "—", "BUDGET", hasBudget ? "Remaining budget" : "Cannot compute — no budget set", 3, hasBudget ? "budget - allocated" : "needs budget"),
|
||||
n("budget.utilizationPct", "Utilization", hasBudget ? `${budgetStatus.utilizationPercent.toFixed(1)}%` : "N/A", hasBudget ? "%" : "—", "BUDGET", hasBudget ? "Budget utilization" : "Cannot compute — no budget set", 3, hasBudget ? "allocated / budget × 100" : "needs budget"),
|
||||
n("budget.weightedCents", "Win-Weighted", fmtEur(budgetStatus.winProbabilityWeightedCents), "EUR", "BUDGET", "Win-weighted cost", 3, "allocated × winProb / 100"),
|
||||
n("budget.allocCount", "Allocations", `${projectAllocations.length}`, "count", "BUDGET", `${projectAllocations.length} resource allocations on project`, 1),
|
||||
n(
|
||||
"budget.confirmedCents",
|
||||
"Confirmed",
|
||||
fmtEur(budgetStatus.confirmedCents),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
"Confirmed allocation costs",
|
||||
2,
|
||||
"Σ(CONFIRMED allocs)",
|
||||
),
|
||||
n(
|
||||
"budget.proposedCents",
|
||||
"Proposed",
|
||||
fmtEur(budgetStatus.proposedCents),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
"Proposed allocation costs",
|
||||
2,
|
||||
"Σ(PROPOSED allocs)",
|
||||
),
|
||||
n(
|
||||
"budget.allocatedCents",
|
||||
"Allocated",
|
||||
fmtEur(budgetStatus.allocatedCents),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
"Total allocated",
|
||||
2,
|
||||
"confirmed + proposed",
|
||||
),
|
||||
n(
|
||||
"budget.remainingCents",
|
||||
"Remaining",
|
||||
hasBudget ? fmtEur(budgetStatus.remainingCents) : "N/A",
|
||||
hasBudget ? "EUR" : "—",
|
||||
"BUDGET",
|
||||
hasBudget ? "Remaining budget" : "Cannot compute — no budget set",
|
||||
3,
|
||||
hasBudget ? "budget - allocated" : "needs budget",
|
||||
),
|
||||
n(
|
||||
"budget.utilizationPct",
|
||||
"Utilization",
|
||||
hasBudget ? `${budgetStatus.utilizationPercent.toFixed(1)}%` : "N/A",
|
||||
hasBudget ? "%" : "—",
|
||||
"BUDGET",
|
||||
hasBudget ? "Budget utilization" : "Cannot compute — no budget set",
|
||||
3,
|
||||
hasBudget ? "allocated / budget × 100" : "needs budget",
|
||||
),
|
||||
n(
|
||||
"budget.weightedCents",
|
||||
"Win-Weighted",
|
||||
fmtEur(budgetStatus.winProbabilityWeightedCents),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
"Win-weighted cost",
|
||||
3,
|
||||
"allocated × winProb / 100",
|
||||
),
|
||||
n(
|
||||
"budget.allocCount",
|
||||
"Allocations",
|
||||
`${projectAllocations.length}`,
|
||||
"count",
|
||||
"BUDGET",
|
||||
`${projectAllocations.length} resource allocations on project`,
|
||||
1,
|
||||
),
|
||||
);
|
||||
|
||||
links.push(
|
||||
@@ -81,13 +175,20 @@ export async function readProjectGraphSnapshot(
|
||||
const estimatedCost = estimateGraph.estimatedCostCents;
|
||||
const gapCents = budgetStatus.allocatedCents - estimatedCost;
|
||||
nodes.push(
|
||||
n("budget.estVsActualGap", "Est. vs Actual", fmtEur(Math.abs(gapCents)), "EUR", "BUDGET",
|
||||
n(
|
||||
"budget.estVsActualGap",
|
||||
"Est. vs Actual",
|
||||
fmtEur(Math.abs(gapCents)),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
gapCents > 0
|
||||
? `Actual allocations exceed estimate by ${fmtEur(gapCents)}`
|
||||
: gapCents < 0
|
||||
? `Actual allocations under estimate by ${fmtEur(Math.abs(gapCents))}`
|
||||
: "Actual allocations match estimate",
|
||||
3, "allocated - estCost"),
|
||||
3,
|
||||
"allocated - estCost",
|
||||
),
|
||||
);
|
||||
links.push(
|
||||
l("budget.allocatedCents", "budget.estVsActualGap", "−", 1),
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
calculateAllocation,
|
||||
deriveResourceForecast,
|
||||
type AssignmentSlice,
|
||||
} from "@capakraken/engine";
|
||||
import type { CalculationRule, WeekdayAvailability } from "@capakraken/shared";
|
||||
import { calculateAllocation, deriveResourceForecast, type AssignmentSlice } from "@nexus/engine";
|
||||
import type { CalculationRule, WeekdayAvailability } from "@nexus/shared";
|
||||
import type { loadResourceGraphAvailability } from "./computation-graph-resource-availability.js";
|
||||
|
||||
type ResourceGraphAssignment = {
|
||||
@@ -55,7 +51,9 @@ export function buildResourceAllocationSummary(input: ResourceGraphAllocationInp
|
||||
let hasRulesEffect = false;
|
||||
|
||||
for (const assignment of input.assignments) {
|
||||
const overlapStart = new Date(Math.max(input.monthStart.getTime(), assignment.startDate.getTime()));
|
||||
const overlapStart = new Date(
|
||||
Math.max(input.monthStart.getTime(), assignment.startDate.getTime()),
|
||||
);
|
||||
const overlapEnd = new Date(Math.min(input.monthEnd.getTime(), assignment.endDate.getTime()));
|
||||
const categoryCode = assignment.project.utilizationCategory?.code ?? "Chg";
|
||||
|
||||
@@ -110,15 +108,17 @@ export function buildResourceAllocationSummary(input: ResourceGraphAllocationInp
|
||||
sah: input.effectiveAvailableHours,
|
||||
});
|
||||
|
||||
const dailyCostCents = input.assignments.length > 0
|
||||
? Math.round(input.assignments[0]!.hoursPerDay * input.resourceLcrCents)
|
||||
: 0;
|
||||
const avgHoursPerDay = input.assignments.length > 0
|
||||
? input.assignments.reduce((sum, assignment) => sum + assignment.hoursPerDay, 0) / input.assignments.length
|
||||
: 0;
|
||||
const utilizationPct = input.effectiveAvailableHours > 0
|
||||
? (totalAllocHours / input.effectiveAvailableHours) * 100
|
||||
: 0;
|
||||
const dailyCostCents =
|
||||
input.assignments.length > 0
|
||||
? Math.round(input.assignments[0]!.hoursPerDay * input.resourceLcrCents)
|
||||
: 0;
|
||||
const avgHoursPerDay =
|
||||
input.assignments.length > 0
|
||||
? input.assignments.reduce((sum, assignment) => sum + assignment.hoursPerDay, 0) /
|
||||
input.assignments.length
|
||||
: 0;
|
||||
const utilizationPct =
|
||||
input.effectiveAvailableHours > 0 ? (totalAllocHours / input.effectiveAvailableHours) * 100 : 0;
|
||||
const chargeableHours = forecast.chg * input.effectiveAvailableHours;
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VacationStatus } from "@capakraken/db";
|
||||
import type { WeekdayAvailability } from "@capakraken/shared";
|
||||
import { VacationStatus } from "@nexus/db";
|
||||
import type { WeekdayAvailability } from "@nexus/shared";
|
||||
import {
|
||||
asHolidayResolverDb,
|
||||
collectHolidayAvailability,
|
||||
@@ -29,18 +29,14 @@ type ResourceAvailabilityInput = {
|
||||
monthEnd: Date;
|
||||
};
|
||||
|
||||
function getAvailabilityHoursForDate(
|
||||
availability: WeekdayAvailability,
|
||||
date: Date,
|
||||
): number {
|
||||
const dayKey = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][date.getUTCDay()] as keyof WeekdayAvailability;
|
||||
function getAvailabilityHoursForDate(availability: WeekdayAvailability, date: Date): number {
|
||||
const dayKey = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][
|
||||
date.getUTCDay()
|
||||
] as keyof WeekdayAvailability;
|
||||
return availability[dayKey] ?? 0;
|
||||
}
|
||||
|
||||
function sumAvailabilityHoursForDates(
|
||||
availability: WeekdayAvailability,
|
||||
dates: Date[],
|
||||
): number {
|
||||
function sumAvailabilityHoursForDates(availability: WeekdayAvailability, dates: Date[]): number {
|
||||
return dates.reduce((sum, date) => sum + getAvailabilityHoursForDate(availability, date), 0);
|
||||
}
|
||||
|
||||
@@ -90,15 +86,17 @@ export async function loadResourceGraphAvailability(input: ResourceAvailabilityI
|
||||
|
||||
const contexts = await loadResourceDailyAvailabilityContexts(
|
||||
db,
|
||||
[{
|
||||
id: resource.id,
|
||||
availability: weeklyAvailability,
|
||||
countryId: resource.countryId,
|
||||
countryCode: resource.country?.code,
|
||||
federalState: resource.federalState,
|
||||
metroCityId: resource.metroCityId,
|
||||
metroCityName: resource.metroCity?.name,
|
||||
}],
|
||||
[
|
||||
{
|
||||
id: resource.id,
|
||||
availability: weeklyAvailability,
|
||||
countryId: resource.countryId,
|
||||
countryCode: resource.country?.code,
|
||||
federalState: resource.federalState,
|
||||
metroCityId: resource.metroCityId,
|
||||
metroCityName: resource.metroCity?.name,
|
||||
},
|
||||
],
|
||||
monthStart,
|
||||
monthEnd,
|
||||
);
|
||||
@@ -128,10 +126,13 @@ export async function loadResourceGraphAvailability(input: ResourceAvailabilityI
|
||||
periodEnd: monthEnd,
|
||||
context: availabilityContext,
|
||||
});
|
||||
const publicHolidayDates = resolvedHolidays.map((holiday) => new Date(`${holiday.date}T00:00:00.000Z`));
|
||||
const publicHolidayWorkdayCount = publicHolidayDates.reduce((count, date) => (
|
||||
count + (getAvailabilityHoursForDate(weeklyAvailability, date) > 0 ? 1 : 0)
|
||||
), 0);
|
||||
const publicHolidayDates = resolvedHolidays.map(
|
||||
(holiday) => new Date(`${holiday.date}T00:00:00.000Z`),
|
||||
);
|
||||
const publicHolidayWorkdayCount = publicHolidayDates.reduce(
|
||||
(count, date) => count + (getAvailabilityHoursForDate(weeklyAvailability, date) > 0 ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
const publicHolidayHoursDeduction = sumAvailabilityHoursForDates(
|
||||
weeklyAvailability,
|
||||
publicHolidayDates,
|
||||
@@ -143,21 +144,27 @@ export async function loadResourceGraphAvailability(input: ResourceAvailabilityI
|
||||
const baseHours = getAvailabilityHoursForDate(weeklyAvailability, absence.date);
|
||||
return sum + baseHours * (absence.isHalfDay ? 0.5 : 1);
|
||||
}, 0);
|
||||
const effectiveHoursPerWorkingDay = effectiveWorkingDays > 0
|
||||
? effectiveAvailableHours / effectiveWorkingDays
|
||||
: 0;
|
||||
const effectiveHoursPerWorkingDay =
|
||||
effectiveWorkingDays > 0 ? effectiveAvailableHours / effectiveWorkingDays : 0;
|
||||
const holidayScopeSummary = [
|
||||
resource.country?.code ?? "—",
|
||||
resource.federalState ?? "—",
|
||||
resource.metroCity?.name ?? "—",
|
||||
].join(" / ");
|
||||
const holidayExamples = resolvedHolidays.length > 0
|
||||
? resolvedHolidays.slice(0, 4).map((holiday) => `${holiday.date} ${holiday.name}`).join(", ")
|
||||
: "none";
|
||||
const holidayScopeBreakdown = resolvedHolidays.reduce<Record<string, number>>((counts, holiday) => {
|
||||
counts[holiday.scope] = (counts[holiday.scope] ?? 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
const holidayExamples =
|
||||
resolvedHolidays.length > 0
|
||||
? resolvedHolidays
|
||||
.slice(0, 4)
|
||||
.map((holiday) => `${holiday.date} ${holiday.name}`)
|
||||
.join(", ")
|
||||
: "none";
|
||||
const holidayScopeBreakdown = resolvedHolidays.reduce<Record<string, number>>(
|
||||
(counts, holiday) => {
|
||||
counts[holiday.scope] = (counts[holiday.scope] ?? 0) + 1;
|
||||
return counts;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
absenceDateStrings,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computeBudgetStatus } from "@capakraken/engine";
|
||||
import { computeBudgetStatus } from "@nexus/engine";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
import { fmtEur } from "../lib/format-utils.js";
|
||||
import { type GraphLink, type GraphNode, l, n } from "./computation-graph-shared.js";
|
||||
@@ -25,9 +25,9 @@ export async function readResourceBudgetGraph(
|
||||
monthStart: Date,
|
||||
monthEnd: Date,
|
||||
): Promise<ResourceBudgetGraph> {
|
||||
const budgetProject = assignments.find((assignment) => (
|
||||
assignment.project.budgetCents != null && assignment.project.budgetCents > 0
|
||||
))?.project;
|
||||
const budgetProject = assignments.find(
|
||||
(assignment) => assignment.project.budgetCents != null && assignment.project.budgetCents > 0,
|
||||
)?.project;
|
||||
|
||||
if (!budgetProject?.budgetCents) {
|
||||
return { nodes: [], links: [] };
|
||||
@@ -59,14 +59,84 @@ export async function readResourceBudgetGraph(
|
||||
|
||||
return {
|
||||
nodes: [
|
||||
n("input.budgetCents", "Project Budget", fmtEur(budgetProject.budgetCents), "EUR", "INPUT", `Budget for ${budgetProject.name}`, 0),
|
||||
n("input.winProbability", "Win Probability", `${budgetProject.winProbability}%`, "%", "INPUT", "Project win probability", 0),
|
||||
n("budget.confirmedCents", "Confirmed", fmtEur(budgetStatus.confirmedCents), "EUR", "BUDGET", "Sum of CONFIRMED/ACTIVE allocation costs", 2, "Σ(confirmed allocs)"),
|
||||
n("budget.proposedCents", "Proposed", fmtEur(budgetStatus.proposedCents), "EUR", "BUDGET", "Sum of PROPOSED allocation costs", 2, "Σ(proposed allocs)"),
|
||||
n("budget.allocatedCents", "Allocated", fmtEur(budgetStatus.allocatedCents), "EUR", "BUDGET", "Total allocated budget", 2, "confirmed + proposed"),
|
||||
n("budget.remainingCents", "Remaining", fmtEur(budgetStatus.remainingCents), "EUR", "BUDGET", "Remaining budget", 3, "budget - allocated"),
|
||||
n("budget.utilizationPct", "Utilization", `${budgetStatus.utilizationPercent.toFixed(1)}%`, "%", "BUDGET", "Budget utilization percentage", 3, "allocated / budget × 100"),
|
||||
n("budget.weightedCents", "Win-Weighted", fmtEur(budgetStatus.winProbabilityWeightedCents), "EUR", "BUDGET", "Win-probability-weighted cost", 3, "allocated × winProb / 100"),
|
||||
n(
|
||||
"input.budgetCents",
|
||||
"Project Budget",
|
||||
fmtEur(budgetProject.budgetCents),
|
||||
"EUR",
|
||||
"INPUT",
|
||||
`Budget for ${budgetProject.name}`,
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.winProbability",
|
||||
"Win Probability",
|
||||
`${budgetProject.winProbability}%`,
|
||||
"%",
|
||||
"INPUT",
|
||||
"Project win probability",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"budget.confirmedCents",
|
||||
"Confirmed",
|
||||
fmtEur(budgetStatus.confirmedCents),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
"Sum of CONFIRMED/ACTIVE allocation costs",
|
||||
2,
|
||||
"Σ(confirmed allocs)",
|
||||
),
|
||||
n(
|
||||
"budget.proposedCents",
|
||||
"Proposed",
|
||||
fmtEur(budgetStatus.proposedCents),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
"Sum of PROPOSED allocation costs",
|
||||
2,
|
||||
"Σ(proposed allocs)",
|
||||
),
|
||||
n(
|
||||
"budget.allocatedCents",
|
||||
"Allocated",
|
||||
fmtEur(budgetStatus.allocatedCents),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
"Total allocated budget",
|
||||
2,
|
||||
"confirmed + proposed",
|
||||
),
|
||||
n(
|
||||
"budget.remainingCents",
|
||||
"Remaining",
|
||||
fmtEur(budgetStatus.remainingCents),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
"Remaining budget",
|
||||
3,
|
||||
"budget - allocated",
|
||||
),
|
||||
n(
|
||||
"budget.utilizationPct",
|
||||
"Utilization",
|
||||
`${budgetStatus.utilizationPercent.toFixed(1)}%`,
|
||||
"%",
|
||||
"BUDGET",
|
||||
"Budget utilization percentage",
|
||||
3,
|
||||
"allocated / budget × 100",
|
||||
),
|
||||
n(
|
||||
"budget.weightedCents",
|
||||
"Win-Weighted",
|
||||
fmtEur(budgetStatus.winProbabilityWeightedCents),
|
||||
"EUR",
|
||||
"BUDGET",
|
||||
"Win-probability-weighted cost",
|
||||
3,
|
||||
"allocated × winProb / 100",
|
||||
),
|
||||
],
|
||||
links: [
|
||||
l("alloc.totalCostCents", "budget.confirmedCents", "per assignment", 1),
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { WeekdayAvailability } from "@capakraken/shared";
|
||||
import type { WeekdayAvailability } from "@nexus/shared";
|
||||
import { fmtEur } from "../lib/format-utils.js";
|
||||
import { type GraphLink, type GraphNode, fmtPct, fmtNum, l, n } from "./computation-graph-shared.js";
|
||||
import {
|
||||
type GraphLink,
|
||||
type GraphNode,
|
||||
fmtPct,
|
||||
fmtNum,
|
||||
l,
|
||||
n,
|
||||
} from "./computation-graph-shared.js";
|
||||
|
||||
type ResourceGraphResolvedHoliday = {
|
||||
date: string;
|
||||
@@ -116,84 +123,510 @@ function describeWeeklyAvailability(availability: WeekdayAvailability): {
|
||||
}
|
||||
|
||||
export function buildResourceGraphSnapshot(input: ResourceGraphPresentationInput) {
|
||||
const { totalHours: weeklyTotalHours, label: availabilityLabel } = describeWeeklyAvailability(input.weeklyAvailability);
|
||||
const { totalHours: weeklyTotalHours, label: availabilityLabel } = describeWeeklyAvailability(
|
||||
input.weeklyAvailability,
|
||||
);
|
||||
const hasScheduleRules = !!input.scheduleRules;
|
||||
|
||||
const nodes: GraphNode[] = [
|
||||
n("input.fte", "FTE", fmtNum(input.resource.fte, 2), "ratio", "INPUT", "Resource FTE factor", 0),
|
||||
n("input.country", "Country", input.resource.country?.name ?? input.resource.country?.code ?? "—", "text", "INPUT", "Country used for base working-time and national holiday rules", 0),
|
||||
n("input.state", "State", input.resource.federalState ?? "—", "text", "INPUT", "Federal state / region used for regional holidays", 0),
|
||||
n("input.city", "City", input.resource.metroCity?.name ?? "—", "text", "INPUT", "City / metro used for local holidays", 0),
|
||||
n("input.holidayContext", "Holiday Context", input.holidayScopeSummary, "text", "INPUT", "Resolved holiday scope chain: country / state / city", 0),
|
||||
n("input.holidayExamples", "Holiday Dates", input.holidayExamples, "text", "INPUT", `Resolved holidays in ${input.month}; scopes: COUNTRY ${input.holidayScopeBreakdown.COUNTRY ?? 0}, STATE ${input.holidayScopeBreakdown.STATE ?? 0}, CITY ${input.holidayScopeBreakdown.CITY ?? 0}`, 0),
|
||||
n("input.dailyHours", "Country Hours", `${input.dailyHours} h`, "hours", "INPUT", `Base daily working hours (${input.resource.country?.code ?? "?"})`, 0),
|
||||
n(
|
||||
"input.fte",
|
||||
"FTE",
|
||||
fmtNum(input.resource.fte, 2),
|
||||
"ratio",
|
||||
"INPUT",
|
||||
"Resource FTE factor",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.country",
|
||||
"Country",
|
||||
input.resource.country?.name ?? input.resource.country?.code ?? "—",
|
||||
"text",
|
||||
"INPUT",
|
||||
"Country used for base working-time and national holiday rules",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.state",
|
||||
"State",
|
||||
input.resource.federalState ?? "—",
|
||||
"text",
|
||||
"INPUT",
|
||||
"Federal state / region used for regional holidays",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.city",
|
||||
"City",
|
||||
input.resource.metroCity?.name ?? "—",
|
||||
"text",
|
||||
"INPUT",
|
||||
"City / metro used for local holidays",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.holidayContext",
|
||||
"Holiday Context",
|
||||
input.holidayScopeSummary,
|
||||
"text",
|
||||
"INPUT",
|
||||
"Resolved holiday scope chain: country / state / city",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.holidayExamples",
|
||||
"Holiday Dates",
|
||||
input.holidayExamples,
|
||||
"text",
|
||||
"INPUT",
|
||||
`Resolved holidays in ${input.month}; scopes: COUNTRY ${input.holidayScopeBreakdown.COUNTRY ?? 0}, STATE ${input.holidayScopeBreakdown.STATE ?? 0}, CITY ${input.holidayScopeBreakdown.CITY ?? 0}`,
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.dailyHours",
|
||||
"Country Hours",
|
||||
`${input.dailyHours} h`,
|
||||
"hours",
|
||||
"INPUT",
|
||||
`Base daily working hours (${input.resource.country?.code ?? "?"})`,
|
||||
0,
|
||||
),
|
||||
...(hasScheduleRules
|
||||
? [n("input.scheduleRules", "Schedule Rules", "Spain", "—", "INPUT", "Variable daily hours (regular/friday/summer)", 0)]
|
||||
? [
|
||||
n(
|
||||
"input.scheduleRules",
|
||||
"Schedule Rules",
|
||||
"Spain",
|
||||
"—",
|
||||
"INPUT",
|
||||
"Variable daily hours (regular/friday/summer)",
|
||||
0,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
n("input.weeklyAvail", "Weekly Avail.", `${weeklyTotalHours}h`, "h/week", "INPUT", `Resource availability: ${availabilityLabel}`, 0),
|
||||
n("input.lcrCents", "LCR", fmtEur(input.resource.lcrCents), "cents/h", "INPUT", "Loaded Cost Rate per hour", 0),
|
||||
n("input.hoursPerDay", "Hours/Day", fmtNum(input.avgHoursPerDay), "hours", "INPUT", "Average hours/day across assignments", 0),
|
||||
n("input.absences", "Absences", `${input.absenceCount}`, "count", "INPUT", `Absence days in ${input.month} (${input.vacationDayCount} vacation, ${input.sickDayCount} sick${input.halfDayCount > 0 ? `, ${input.halfDayCount} half-day` : ""})`, 0),
|
||||
n("input.publicHolidays", "Public Holidays", `${input.publicHolidayCount}`, "count", "INPUT", `Resolved holidays in ${input.month}; ${input.publicHolidayWorkdayCount} hit configured working days`, 0),
|
||||
n("input.calcRules", "Active Rules", `${input.calcRulesCount}`, "count", "INPUT", "Active calculation rules", 0),
|
||||
n("input.targetPct", "Target", fmtPct(input.targetPct), "%", "INPUT", `Chargeability target (${input.resource.managementLevelGroup?.name ?? "legacy"})`, 0),
|
||||
n("input.assignmentCount", "Assignments", `${input.assignmentCount}`, "count", "INPUT", `Active assignments in ${input.month}`, 0),
|
||||
n(
|
||||
"input.weeklyAvail",
|
||||
"Weekly Avail.",
|
||||
`${weeklyTotalHours}h`,
|
||||
"h/week",
|
||||
"INPUT",
|
||||
`Resource availability: ${availabilityLabel}`,
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.lcrCents",
|
||||
"LCR",
|
||||
fmtEur(input.resource.lcrCents),
|
||||
"cents/h",
|
||||
"INPUT",
|
||||
"Loaded Cost Rate per hour",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.hoursPerDay",
|
||||
"Hours/Day",
|
||||
fmtNum(input.avgHoursPerDay),
|
||||
"hours",
|
||||
"INPUT",
|
||||
"Average hours/day across assignments",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.absences",
|
||||
"Absences",
|
||||
`${input.absenceCount}`,
|
||||
"count",
|
||||
"INPUT",
|
||||
`Absence days in ${input.month} (${input.vacationDayCount} vacation, ${input.sickDayCount} sick${input.halfDayCount > 0 ? `, ${input.halfDayCount} half-day` : ""})`,
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.publicHolidays",
|
||||
"Public Holidays",
|
||||
`${input.publicHolidayCount}`,
|
||||
"count",
|
||||
"INPUT",
|
||||
`Resolved holidays in ${input.month}; ${input.publicHolidayWorkdayCount} hit configured working days`,
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.calcRules",
|
||||
"Active Rules",
|
||||
`${input.calcRulesCount}`,
|
||||
"count",
|
||||
"INPUT",
|
||||
"Active calculation rules",
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.targetPct",
|
||||
"Target",
|
||||
fmtPct(input.targetPct),
|
||||
"%",
|
||||
"INPUT",
|
||||
`Chargeability target (${input.resource.managementLevelGroup?.name ?? "legacy"})`,
|
||||
0,
|
||||
),
|
||||
n(
|
||||
"input.assignmentCount",
|
||||
"Assignments",
|
||||
`${input.assignmentCount}`,
|
||||
"count",
|
||||
"INPUT",
|
||||
`Active assignments in ${input.month}`,
|
||||
0,
|
||||
),
|
||||
|
||||
n("sah.calendarDays", "Calendar Days", `${input.sahCalendarDays}`, "days", "SAH", "Total calendar days in period", 1),
|
||||
n("sah.weekendDays", "Weekend Days", `${input.sahWeekendDays}`, "days", "SAH", "Saturday + Sunday count", 1),
|
||||
n("sah.grossWorkingDays", "Gross Work Days", `${input.baseWorkingDays}`, "days", "SAH", "Working days from the resource-specific weekly availability before holidays/absences", 1, "count(availability > 0)"),
|
||||
n("sah.baseHours", "Base Hours", fmtNum(input.baseAvailableHours), "hours", "SAH", "Available hours from weekly availability before holiday/absence deductions", 1, "Σ(daily availability)"),
|
||||
n("sah.publicHolidayDays", "Holiday Ded.", `${input.publicHolidayWorkdayCount}`, "days", "SAH", "Holiday workdays deducted after applying country/state/city scope and weekday availability", 1),
|
||||
n("sah.publicHolidayHours", "Holiday Hrs Ded.", fmtNum(input.publicHolidayHoursDeduction), "hours", "SAH", "Hours removed by resolved public holidays", 1, "Σ(availability on holiday dates)"),
|
||||
n("sah.absenceDays", "Absence Ded.", `${input.absenceCount}`, "days", "SAH", "Vacation/sick days that hit working days and are not already public holidays", 1),
|
||||
n("sah.absenceHours", "Absence Hrs Ded.", fmtNum(input.absenceHoursDeduction), "hours", "SAH", "Hours removed by vacation/sick absences", 1, "Σ(availability × absence fraction)"),
|
||||
n("sah.netWorkingDays", "Net Work Days", `${input.effectiveWorkingDays}`, "days", "SAH", "Remaining working days after holiday and absence deductions", 2, "gross - holidays - absences"),
|
||||
n("sah.effectiveHoursPerDay", "Eff. Hrs/Day", fmtNum(input.effectiveHoursPerWorkingDay), "hours", "SAH", "Average effective hours per remaining working day", 2, "SAH / net work days"),
|
||||
n("sah.sah", "SAH", fmtNum(input.effectiveAvailableHours), "hours", "SAH", "Effective available hours after weekly availability, local holidays and absences", 2, "base hours - holiday hours - absence hours"),
|
||||
n(
|
||||
"sah.calendarDays",
|
||||
"Calendar Days",
|
||||
`${input.sahCalendarDays}`,
|
||||
"days",
|
||||
"SAH",
|
||||
"Total calendar days in period",
|
||||
1,
|
||||
),
|
||||
n(
|
||||
"sah.weekendDays",
|
||||
"Weekend Days",
|
||||
`${input.sahWeekendDays}`,
|
||||
"days",
|
||||
"SAH",
|
||||
"Saturday + Sunday count",
|
||||
1,
|
||||
),
|
||||
n(
|
||||
"sah.grossWorkingDays",
|
||||
"Gross Work Days",
|
||||
`${input.baseWorkingDays}`,
|
||||
"days",
|
||||
"SAH",
|
||||
"Working days from the resource-specific weekly availability before holidays/absences",
|
||||
1,
|
||||
"count(availability > 0)",
|
||||
),
|
||||
n(
|
||||
"sah.baseHours",
|
||||
"Base Hours",
|
||||
fmtNum(input.baseAvailableHours),
|
||||
"hours",
|
||||
"SAH",
|
||||
"Available hours from weekly availability before holiday/absence deductions",
|
||||
1,
|
||||
"Σ(daily availability)",
|
||||
),
|
||||
n(
|
||||
"sah.publicHolidayDays",
|
||||
"Holiday Ded.",
|
||||
`${input.publicHolidayWorkdayCount}`,
|
||||
"days",
|
||||
"SAH",
|
||||
"Holiday workdays deducted after applying country/state/city scope and weekday availability",
|
||||
1,
|
||||
),
|
||||
n(
|
||||
"sah.publicHolidayHours",
|
||||
"Holiday Hrs Ded.",
|
||||
fmtNum(input.publicHolidayHoursDeduction),
|
||||
"hours",
|
||||
"SAH",
|
||||
"Hours removed by resolved public holidays",
|
||||
1,
|
||||
"Σ(availability on holiday dates)",
|
||||
),
|
||||
n(
|
||||
"sah.absenceDays",
|
||||
"Absence Ded.",
|
||||
`${input.absenceCount}`,
|
||||
"days",
|
||||
"SAH",
|
||||
"Vacation/sick days that hit working days and are not already public holidays",
|
||||
1,
|
||||
),
|
||||
n(
|
||||
"sah.absenceHours",
|
||||
"Absence Hrs Ded.",
|
||||
fmtNum(input.absenceHoursDeduction),
|
||||
"hours",
|
||||
"SAH",
|
||||
"Hours removed by vacation/sick absences",
|
||||
1,
|
||||
"Σ(availability × absence fraction)",
|
||||
),
|
||||
n(
|
||||
"sah.netWorkingDays",
|
||||
"Net Work Days",
|
||||
`${input.effectiveWorkingDays}`,
|
||||
"days",
|
||||
"SAH",
|
||||
"Remaining working days after holiday and absence deductions",
|
||||
2,
|
||||
"gross - holidays - absences",
|
||||
),
|
||||
n(
|
||||
"sah.effectiveHoursPerDay",
|
||||
"Eff. Hrs/Day",
|
||||
fmtNum(input.effectiveHoursPerWorkingDay),
|
||||
"hours",
|
||||
"SAH",
|
||||
"Average effective hours per remaining working day",
|
||||
2,
|
||||
"SAH / net work days",
|
||||
),
|
||||
n(
|
||||
"sah.sah",
|
||||
"SAH",
|
||||
fmtNum(input.effectiveAvailableHours),
|
||||
"hours",
|
||||
"SAH",
|
||||
"Effective available hours after weekly availability, local holidays and absences",
|
||||
2,
|
||||
"base hours - holiday hours - absence hours",
|
||||
),
|
||||
|
||||
n("alloc.workingDays", "Work Days", `${input.totalWorkingDaysInMonth}`, "days", "ALLOCATION", "Working days covered by assignments in period", 1, "Σ(overlap workdays)"),
|
||||
n("alloc.totalHours", "Total Hours", fmtNum(input.totalAllocHours), "hours", "ALLOCATION", "Sum of effective hours across assignments", 2, "Σ(min(h/day, avail) × workdays)"),
|
||||
n("alloc.dailyCostCents", "Daily Cost", fmtEur(input.dailyCostCents), "EUR", "ALLOCATION", "Cost per working day", 1, "hoursPerDay × LCR"),
|
||||
n("alloc.totalCostCents", "Total Cost", fmtEur(input.totalAllocCostCents), "EUR", "ALLOCATION", "Sum of daily costs", 2, "Σ(dailyCost × workdays)"),
|
||||
n("alloc.utilizationPct", "Utilization", `${input.utilizationPct.toFixed(1)}%`, "%", "ALLOCATION", "Allocation utilization: allocated hours / SAH", 3, "totalHours / SAH × 100"),
|
||||
n(
|
||||
"alloc.workingDays",
|
||||
"Work Days",
|
||||
`${input.totalWorkingDaysInMonth}`,
|
||||
"days",
|
||||
"ALLOCATION",
|
||||
"Working days covered by assignments in period",
|
||||
1,
|
||||
"Σ(overlap workdays)",
|
||||
),
|
||||
n(
|
||||
"alloc.totalHours",
|
||||
"Total Hours",
|
||||
fmtNum(input.totalAllocHours),
|
||||
"hours",
|
||||
"ALLOCATION",
|
||||
"Sum of effective hours across assignments",
|
||||
2,
|
||||
"Σ(min(h/day, avail) × workdays)",
|
||||
),
|
||||
n(
|
||||
"alloc.dailyCostCents",
|
||||
"Daily Cost",
|
||||
fmtEur(input.dailyCostCents),
|
||||
"EUR",
|
||||
"ALLOCATION",
|
||||
"Cost per working day",
|
||||
1,
|
||||
"hoursPerDay × LCR",
|
||||
),
|
||||
n(
|
||||
"alloc.totalCostCents",
|
||||
"Total Cost",
|
||||
fmtEur(input.totalAllocCostCents),
|
||||
"EUR",
|
||||
"ALLOCATION",
|
||||
"Sum of daily costs",
|
||||
2,
|
||||
"Σ(dailyCost × workdays)",
|
||||
),
|
||||
n(
|
||||
"alloc.utilizationPct",
|
||||
"Utilization",
|
||||
`${input.utilizationPct.toFixed(1)}%`,
|
||||
"%",
|
||||
"ALLOCATION",
|
||||
"Allocation utilization: allocated hours / SAH",
|
||||
3,
|
||||
"totalHours / SAH × 100",
|
||||
),
|
||||
...(input.hasRulesEffect
|
||||
? [
|
||||
n("alloc.chargeableHours", "Chargeable Hrs", fmtNum(input.totalChargeableHours), "hours", "ALLOCATION", "Rules-adjusted chargeable hours", 2, "rules-adjusted"),
|
||||
n("alloc.projectCostCents", "Project Cost", fmtEur(input.totalProjectCostCents), "EUR", "ALLOCATION", "Rules-adjusted project cost", 2, "rules-adjusted"),
|
||||
]
|
||||
n(
|
||||
"alloc.chargeableHours",
|
||||
"Chargeable Hrs",
|
||||
fmtNum(input.totalChargeableHours),
|
||||
"hours",
|
||||
"ALLOCATION",
|
||||
"Rules-adjusted chargeable hours",
|
||||
2,
|
||||
"rules-adjusted",
|
||||
),
|
||||
n(
|
||||
"alloc.projectCostCents",
|
||||
"Project Cost",
|
||||
fmtEur(input.totalProjectCostCents),
|
||||
"EUR",
|
||||
"ALLOCATION",
|
||||
"Rules-adjusted project cost",
|
||||
2,
|
||||
"rules-adjusted",
|
||||
),
|
||||
]
|
||||
: []),
|
||||
|
||||
...(input.absenceCount > 0
|
||||
? [
|
||||
n("rules.activeRules", "Matched Rules", `${input.calcRulesCount} rules`, "—", "RULES", "Rules evaluated for absence days", 1),
|
||||
n("rules.costEffect", "Cost Effect", input.hasRulesEffect ? "ZERO" : "—", "—", "RULES", "How absent days affect project cost", 1, "CHARGE / ZERO / REDUCE"),
|
||||
n("rules.chgEffect", "Chg Effect", input.hasRulesEffect ? "COUNT" : "—", "—", "RULES", "How absent days affect chargeability", 1, "COUNT / SKIP"),
|
||||
...(input.hasRulesEffect
|
||||
? [n("rules.costReduction", "Cost Reduction", "per rule", "—", "RULES", "Cost reduction percentage applied to absent hours", 2, "normalCost × (100 - reductionPct) / 100")]
|
||||
: []),
|
||||
]
|
||||
n(
|
||||
"rules.activeRules",
|
||||
"Matched Rules",
|
||||
`${input.calcRulesCount} rules`,
|
||||
"—",
|
||||
"RULES",
|
||||
"Rules evaluated for absence days",
|
||||
1,
|
||||
),
|
||||
n(
|
||||
"rules.costEffect",
|
||||
"Cost Effect",
|
||||
input.hasRulesEffect ? "ZERO" : "—",
|
||||
"—",
|
||||
"RULES",
|
||||
"How absent days affect project cost",
|
||||
1,
|
||||
"CHARGE / ZERO / REDUCE",
|
||||
),
|
||||
n(
|
||||
"rules.chgEffect",
|
||||
"Chg Effect",
|
||||
input.hasRulesEffect ? "COUNT" : "—",
|
||||
"—",
|
||||
"RULES",
|
||||
"How absent days affect chargeability",
|
||||
1,
|
||||
"COUNT / SKIP",
|
||||
),
|
||||
...(input.hasRulesEffect
|
||||
? [
|
||||
n(
|
||||
"rules.costReduction",
|
||||
"Cost Reduction",
|
||||
"per rule",
|
||||
"—",
|
||||
"RULES",
|
||||
"Cost reduction percentage applied to absent hours",
|
||||
2,
|
||||
"normalCost × (100 - reductionPct) / 100",
|
||||
),
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
|
||||
n("chg.chgHours", "Chg Hours", fmtNum(input.chargeableHours), "hours", "CHARGEABILITY", "Total chargeable hours against effective SAH", 2, "chargeability × SAH"),
|
||||
n("chg.chg", "Chargeability", fmtPct(input.forecast.chg), "%", "CHARGEABILITY", "Chargeability ratio", 3, "chgHours / SAH"),
|
||||
n(
|
||||
"chg.chgHours",
|
||||
"Chg Hours",
|
||||
fmtNum(input.chargeableHours),
|
||||
"hours",
|
||||
"CHARGEABILITY",
|
||||
"Total chargeable hours against effective SAH",
|
||||
2,
|
||||
"chargeability × SAH",
|
||||
),
|
||||
n(
|
||||
"chg.chg",
|
||||
"Chargeability",
|
||||
fmtPct(input.forecast.chg),
|
||||
"%",
|
||||
"CHARGEABILITY",
|
||||
"Chargeability ratio",
|
||||
3,
|
||||
"chgHours / SAH",
|
||||
),
|
||||
...(input.forecast.bd > 0
|
||||
? [n("chg.bd", "BD Ratio", fmtPct(input.forecast.bd), "%", "CHARGEABILITY", `Business development: ${fmtNum(input.forecast.bd * input.effectiveAvailableHours)}h`, 3, "bdHours / SAH")]
|
||||
? [
|
||||
n(
|
||||
"chg.bd",
|
||||
"BD Ratio",
|
||||
fmtPct(input.forecast.bd),
|
||||
"%",
|
||||
"CHARGEABILITY",
|
||||
`Business development: ${fmtNum(input.forecast.bd * input.effectiveAvailableHours)}h`,
|
||||
3,
|
||||
"bdHours / SAH",
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(input.forecast.mdi > 0
|
||||
? [n("chg.mdi", "MD&I Ratio", fmtPct(input.forecast.mdi), "%", "CHARGEABILITY", `MD&I hours: ${fmtNum(input.forecast.mdi * input.effectiveAvailableHours)}h`, 3, "mdiHours / SAH")]
|
||||
? [
|
||||
n(
|
||||
"chg.mdi",
|
||||
"MD&I Ratio",
|
||||
fmtPct(input.forecast.mdi),
|
||||
"%",
|
||||
"CHARGEABILITY",
|
||||
`MD&I hours: ${fmtNum(input.forecast.mdi * input.effectiveAvailableHours)}h`,
|
||||
3,
|
||||
"mdiHours / SAH",
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(input.forecast.mo > 0
|
||||
? [n("chg.mo", "M&O Ratio", fmtPct(input.forecast.mo), "%", "CHARGEABILITY", `M&O hours: ${fmtNum(input.forecast.mo * input.effectiveAvailableHours)}h`, 3, "moHours / SAH")]
|
||||
? [
|
||||
n(
|
||||
"chg.mo",
|
||||
"M&O Ratio",
|
||||
fmtPct(input.forecast.mo),
|
||||
"%",
|
||||
"CHARGEABILITY",
|
||||
`M&O hours: ${fmtNum(input.forecast.mo * input.effectiveAvailableHours)}h`,
|
||||
3,
|
||||
"moHours / SAH",
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(input.forecast.pdr > 0
|
||||
? [n("chg.pdr", "PD&R Ratio", fmtPct(input.forecast.pdr), "%", "CHARGEABILITY", `PD&R hours: ${fmtNum(input.forecast.pdr * input.effectiveAvailableHours)}h`, 3, "pdrHours / SAH")]
|
||||
? [
|
||||
n(
|
||||
"chg.pdr",
|
||||
"PD&R Ratio",
|
||||
fmtPct(input.forecast.pdr),
|
||||
"%",
|
||||
"CHARGEABILITY",
|
||||
`PD&R hours: ${fmtNum(input.forecast.pdr * input.effectiveAvailableHours)}h`,
|
||||
3,
|
||||
"pdrHours / SAH",
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(input.forecast.absence > 0
|
||||
? [n("chg.absence", "Absence Ratio", fmtPct(input.forecast.absence), "%", "CHARGEABILITY", `Absence hours: ${fmtNum(input.forecast.absence * input.effectiveAvailableHours)}h`, 3, "absenceHours / SAH")]
|
||||
? [
|
||||
n(
|
||||
"chg.absence",
|
||||
"Absence Ratio",
|
||||
fmtPct(input.forecast.absence),
|
||||
"%",
|
||||
"CHARGEABILITY",
|
||||
`Absence hours: ${fmtNum(input.forecast.absence * input.effectiveAvailableHours)}h`,
|
||||
3,
|
||||
"absenceHours / SAH",
|
||||
),
|
||||
]
|
||||
: []),
|
||||
n("chg.unassigned", "Unassigned", fmtPct(input.forecast.unassigned), "%", "CHARGEABILITY", `${fmtNum(input.forecast.unassigned * input.effectiveAvailableHours)}h of ${fmtNum(input.effectiveAvailableHours)}h SAH not assigned`, 3, "max(0, SAH - assigned) / SAH"),
|
||||
n("chg.target", "Target", fmtPct(input.targetPct), "%", "CHARGEABILITY", "Chargeability target from management level", 3),
|
||||
n("chg.gap", "Gap to Target", `${input.forecast.chg - input.targetPct >= 0 ? "+" : ""}${((input.forecast.chg - input.targetPct) * 100).toFixed(1)} pp`, "pp", "CHARGEABILITY", `Chargeability (${fmtPct(input.forecast.chg)}) vs. target (${fmtPct(input.targetPct)})`, 3, "chargeability − target"),
|
||||
n(
|
||||
"chg.unassigned",
|
||||
"Unassigned",
|
||||
fmtPct(input.forecast.unassigned),
|
||||
"%",
|
||||
"CHARGEABILITY",
|
||||
`${fmtNum(input.forecast.unassigned * input.effectiveAvailableHours)}h of ${fmtNum(input.effectiveAvailableHours)}h SAH not assigned`,
|
||||
3,
|
||||
"max(0, SAH - assigned) / SAH",
|
||||
),
|
||||
n(
|
||||
"chg.target",
|
||||
"Target",
|
||||
fmtPct(input.targetPct),
|
||||
"%",
|
||||
"CHARGEABILITY",
|
||||
"Chargeability target from management level",
|
||||
3,
|
||||
),
|
||||
n(
|
||||
"chg.gap",
|
||||
"Gap to Target",
|
||||
`${input.forecast.chg - input.targetPct >= 0 ? "+" : ""}${((input.forecast.chg - input.targetPct) * 100).toFixed(1)} pp`,
|
||||
"pp",
|
||||
"CHARGEABILITY",
|
||||
`Chargeability (${fmtPct(input.forecast.chg)}) vs. target (${fmtPct(input.targetPct)})`,
|
||||
3,
|
||||
"chargeability − target",
|
||||
),
|
||||
|
||||
...input.budgetNodes,
|
||||
];
|
||||
@@ -209,7 +642,9 @@ export function buildResourceGraphSnapshot(input: ResourceGraphPresentationInput
|
||||
l("input.holidayExamples", "sah.publicHolidayDays", "resolved dates", 2),
|
||||
l("input.holidayExamples", "sah.publicHolidayHours", "remove matching day hours", 2),
|
||||
l("input.absences", "sah.absenceHours", "remove absence fractions", 1),
|
||||
...(hasScheduleRules ? [l("input.scheduleRules", "sah.effectiveHoursPerDay", "variable h/day", 1)] : []),
|
||||
...(hasScheduleRules
|
||||
? [l("input.scheduleRules", "sah.effectiveHoursPerDay", "variable h/day", 1)]
|
||||
: []),
|
||||
l("sah.calendarDays", "sah.grossWorkingDays", "− weekends", 2),
|
||||
l("sah.weekendDays", "sah.grossWorkingDays", "−", 1),
|
||||
l("input.publicHolidays", "sah.publicHolidayDays", "∩ workdays", 1),
|
||||
@@ -237,24 +672,31 @@ export function buildResourceGraphSnapshot(input: ResourceGraphPresentationInput
|
||||
|
||||
...(input.absenceCount > 0
|
||||
? [
|
||||
l("input.calcRules", "rules.activeRules", "filter active", 1),
|
||||
l("input.absences", "rules.activeRules", "match trigger", 1),
|
||||
l("rules.activeRules", "rules.costEffect", "→ effect", 1),
|
||||
l("rules.activeRules", "rules.chgEffect", "→ effect", 1),
|
||||
]
|
||||
l("input.calcRules", "rules.activeRules", "filter active", 1),
|
||||
l("input.absences", "rules.activeRules", "match trigger", 1),
|
||||
l("rules.activeRules", "rules.costEffect", "→ effect", 1),
|
||||
l("rules.activeRules", "rules.chgEffect", "→ effect", 1),
|
||||
]
|
||||
: []),
|
||||
|
||||
...(input.hasRulesEffect
|
||||
? [
|
||||
l("rules.costEffect", "alloc.projectCostCents", "apply", 2),
|
||||
l("alloc.totalCostCents", "alloc.projectCostCents", "adjust", 1),
|
||||
l("rules.chgEffect", "alloc.chargeableHours", "apply", 2),
|
||||
l("alloc.totalHours", "alloc.chargeableHours", "adjust", 1),
|
||||
...(input.absenceCount > 0 ? [l("rules.costEffect", "rules.costReduction", "reduce %", 1)] : []),
|
||||
]
|
||||
l("rules.costEffect", "alloc.projectCostCents", "apply", 2),
|
||||
l("alloc.totalCostCents", "alloc.projectCostCents", "adjust", 1),
|
||||
l("rules.chgEffect", "alloc.chargeableHours", "apply", 2),
|
||||
l("alloc.totalHours", "alloc.chargeableHours", "adjust", 1),
|
||||
...(input.absenceCount > 0
|
||||
? [l("rules.costEffect", "rules.costReduction", "reduce %", 1)]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
|
||||
l(input.hasRulesEffect ? "alloc.chargeableHours" : "alloc.totalHours", "chg.chgHours", "Σ Chg", 2),
|
||||
l(
|
||||
input.hasRulesEffect ? "alloc.chargeableHours" : "alloc.totalHours",
|
||||
"chg.chgHours",
|
||||
"Σ Chg",
|
||||
2,
|
||||
),
|
||||
l("chg.chgHours", "chg.chg", "÷ SAH", 2),
|
||||
l("sah.sah", "chg.chg", "÷", 2),
|
||||
...(input.forecast.bd > 0 ? [l("sah.sah", "chg.bd", "÷", 1)] : []),
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
calculateSAH,
|
||||
getMonthRange,
|
||||
DEFAULT_CALCULATION_RULES,
|
||||
} from "@capakraken/engine";
|
||||
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared";
|
||||
import { calculateSAH, getMonthRange, DEFAULT_CALCULATION_RULES } from "@nexus/engine";
|
||||
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@nexus/shared";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
import { loadResourceGraphAvailability } from "./computation-graph-resource-availability.js";
|
||||
|
||||
@@ -32,7 +28,9 @@ export async function loadResourceGraphSnapshot(
|
||||
federalState: true,
|
||||
metroCityId: true,
|
||||
availability: true,
|
||||
country: { select: { id: true, code: true, name: true, dailyWorkingHours: true, scheduleRules: true } },
|
||||
country: {
|
||||
select: { id: true, code: true, name: true, dailyWorkingHours: true, scheduleRules: true },
|
||||
},
|
||||
metroCity: { select: { id: true, name: true } },
|
||||
managementLevelGroup: { select: { id: true, name: true, targetPercentage: true } },
|
||||
},
|
||||
@@ -40,7 +38,8 @@ export async function loadResourceGraphSnapshot(
|
||||
|
||||
const dailyHours = resource.country?.dailyWorkingHours ?? 8;
|
||||
const scheduleRules = resource.country?.scheduleRules as SpainScheduleRule | null;
|
||||
const targetPct = resource.managementLevelGroup?.targetPercentage ?? (resource.chargeabilityTarget / 100);
|
||||
const targetPct =
|
||||
resource.managementLevelGroup?.targetPercentage ?? resource.chargeabilityTarget / 100;
|
||||
|
||||
const availability = resource.availability as WeekdayAvailability | null;
|
||||
const weeklyAvailability: WeekdayAvailability = availability ?? {
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
CreateMetroCitySchema,
|
||||
UpdateCountrySchema,
|
||||
UpdateMetroCitySchema,
|
||||
} from "@capakraken/shared";
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
} from "@nexus/shared";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
@@ -40,9 +40,11 @@ function withAuditUser(userId: string | undefined) {
|
||||
return userId ? { userId } : {};
|
||||
}
|
||||
|
||||
export const countryListInputSchema = z.object({
|
||||
isActive: z.boolean().optional(),
|
||||
}).optional();
|
||||
export const countryListInputSchema = z
|
||||
.object({
|
||||
isActive: z.boolean().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const countryIdentifierInputSchema = z.object({
|
||||
identifier: z.string().trim().min(1),
|
||||
@@ -123,10 +125,7 @@ export async function getCountryById(ctx: CountryProcedureContext, input: Countr
|
||||
);
|
||||
}
|
||||
|
||||
export async function getMetroCityById(
|
||||
ctx: CountryProcedureContext,
|
||||
input: MetroCityIdInput,
|
||||
) {
|
||||
export async function getMetroCityById(ctx: CountryProcedureContext, input: MetroCityIdInput) {
|
||||
return findUniqueOrThrow(
|
||||
ctx.db.metroCity.findUnique({
|
||||
where: { id: input.id },
|
||||
@@ -192,10 +191,7 @@ export async function updateCountry(ctx: CountryProcedureContext, input: UpdateC
|
||||
}
|
||||
|
||||
export async function createMetroCity(ctx: CountryProcedureContext, input: CreateMetroCityInput) {
|
||||
await findUniqueOrThrow(
|
||||
ctx.db.country.findUnique({ where: { id: input.countryId } }),
|
||||
"Country",
|
||||
);
|
||||
await findUniqueOrThrow(ctx.db.country.findUnique({ where: { id: input.countryId } }), "Country");
|
||||
|
||||
const created = await ctx.db.metroCity.create({
|
||||
data: buildMetroCityCreateData(input),
|
||||
@@ -215,10 +211,7 @@ export async function createMetroCity(ctx: CountryProcedureContext, input: Creat
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function updateMetroCity(
|
||||
ctx: CountryProcedureContext,
|
||||
input: UpdateMetroCityInput,
|
||||
) {
|
||||
export async function updateMetroCity(ctx: CountryProcedureContext, input: UpdateMetroCityInput) {
|
||||
const existing = await findUniqueOrThrow(
|
||||
ctx.db.metroCity.findUnique({ where: { id: input.id } }),
|
||||
"Metro city",
|
||||
@@ -245,10 +238,7 @@ export async function updateMetroCity(
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function deleteMetroCity(
|
||||
ctx: CountryProcedureContext,
|
||||
input: MetroCityIdInput,
|
||||
) {
|
||||
export async function deleteMetroCity(ctx: CountryProcedureContext, input: MetroCityIdInput) {
|
||||
const city = await findUniqueOrThrow(
|
||||
ctx.db.metroCity.findUnique({
|
||||
where: { id: input.id },
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Prisma, type PrismaClient } from "@capakraken/db";
|
||||
import { Prisma, type PrismaClient } from "@nexus/db";
|
||||
import {
|
||||
CreateCountrySchema,
|
||||
CreateMetroCitySchema,
|
||||
UpdateCountrySchema,
|
||||
UpdateMetroCitySchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -31,9 +31,7 @@ export function jsonOrNull(val: unknown): Prisma.InputJsonValue | typeof Prisma.
|
||||
return val as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
export function buildCountryListWhere(
|
||||
input: CountryListInput,
|
||||
): Prisma.CountryWhereInput {
|
||||
export function buildCountryListWhere(input: CountryListInput): Prisma.CountryWhereInput {
|
||||
return {
|
||||
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
||||
};
|
||||
@@ -47,30 +45,30 @@ export async function findCountryByIdentifier<TCountry>(
|
||||
const normalizedIdentifier = identifier.trim();
|
||||
const upperIdentifier = normalizedIdentifier.toUpperCase();
|
||||
|
||||
let country = await db.country.findUnique({
|
||||
let country = (await db.country.findUnique({
|
||||
where: { id: normalizedIdentifier },
|
||||
...extraArgs,
|
||||
}) as TCountry | null;
|
||||
})) as TCountry | null;
|
||||
|
||||
if (!country) {
|
||||
country = await db.country.findFirst({
|
||||
country = (await db.country.findFirst({
|
||||
where: { code: { equals: upperIdentifier, mode: "insensitive" } },
|
||||
...extraArgs,
|
||||
}) as TCountry | null;
|
||||
})) as TCountry | null;
|
||||
}
|
||||
|
||||
if (!country) {
|
||||
country = await db.country.findFirst({
|
||||
country = (await db.country.findFirst({
|
||||
where: { name: { equals: normalizedIdentifier, mode: "insensitive" } },
|
||||
...extraArgs,
|
||||
}) as TCountry | null;
|
||||
})) as TCountry | null;
|
||||
}
|
||||
|
||||
if (!country) {
|
||||
country = await db.country.findFirst({
|
||||
country = (await db.country.findFirst({
|
||||
where: { name: { contains: normalizedIdentifier, mode: "insensitive" } },
|
||||
...extraArgs,
|
||||
}) as TCountry | null;
|
||||
})) as TCountry | null;
|
||||
}
|
||||
|
||||
if (!country) {
|
||||
@@ -116,8 +114,12 @@ export function buildCountryUpdateData(
|
||||
return {
|
||||
...(input.code !== undefined ? { code: input.code } : {}),
|
||||
...(input.name !== undefined ? { name: input.name } : {}),
|
||||
...(input.dailyWorkingHours !== undefined ? { dailyWorkingHours: input.dailyWorkingHours } : {}),
|
||||
...(input.scheduleRules !== undefined ? { scheduleRules: jsonOrNull(input.scheduleRules) } : {}),
|
||||
...(input.dailyWorkingHours !== undefined
|
||||
? { dailyWorkingHours: input.dailyWorkingHours }
|
||||
: {}),
|
||||
...(input.scheduleRules !== undefined
|
||||
? { scheduleRules: jsonOrNull(input.scheduleRules) }
|
||||
: {}),
|
||||
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
||||
};
|
||||
}
|
||||
@@ -139,9 +141,7 @@ export function buildMetroCityUpdateData(
|
||||
};
|
||||
}
|
||||
|
||||
export function assertMetroCityDeletable(
|
||||
city: MetroCityDeleteRecord,
|
||||
): void {
|
||||
export function assertMetroCityDeletable(city: MetroCityDeleteRecord): void {
|
||||
if (city._count.resources > 0) {
|
||||
throw new TRPCError({
|
||||
code: "PRECONDITION_FAILED",
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
updateCountry,
|
||||
updateMetroCity,
|
||||
} from "./country-procedure-support.js";
|
||||
import { CreateCountrySchema, CreateMetroCitySchema } from "@capakraken/shared";
|
||||
import type { CreateCountryInput } from "@capakraken/shared";
|
||||
import { CreateCountrySchema, CreateMetroCitySchema } from "@nexus/shared";
|
||||
import type { CreateCountryInput } from "@nexus/shared";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const countryRouter = createTRPCRouter({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FieldType } from "@capakraken/shared";
|
||||
import { FieldType } from "@nexus/shared";
|
||||
|
||||
export interface CustomFieldFilterInput {
|
||||
key: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type BudgetForecastRow, getDashboardProjectHealth } from "@capakraken/application";
|
||||
import { type BudgetForecastRow, getDashboardProjectHealth } from "@nexus/application";
|
||||
import { fmtEur } from "../lib/format-utils.js";
|
||||
|
||||
type DashboardBudgetForecastCalendarLocation = {
|
||||
@@ -121,11 +121,8 @@ export function mapProjectHealthDetailRows(
|
||||
const projects: DashboardProjectHealthDetail["projects"] = rows
|
||||
.map((project): DashboardProjectHealthDetail["projects"][number] => {
|
||||
const overall = project.compositeScore;
|
||||
const rating: DashboardProjectHealthDetail["projects"][number]["rating"] = overall >= 80
|
||||
? "healthy"
|
||||
: overall >= 50
|
||||
? "at_risk"
|
||||
: "critical";
|
||||
const rating: DashboardProjectHealthDetail["projects"][number]["rating"] =
|
||||
overall >= 80 ? "healthy" : overall >= 50 ? "at_risk" : "critical";
|
||||
|
||||
return {
|
||||
projectId: project.id,
|
||||
@@ -188,14 +185,16 @@ export function mapBudgetForecastDetailRows(
|
||||
budgetCents: forecast.budgetCents,
|
||||
spent: fmtEur(forecast.spentCents),
|
||||
spentCents: forecast.spentCents,
|
||||
remaining: fmtEur(forecast.remainingCents ?? (forecast.budgetCents - forecast.spentCents)),
|
||||
remainingCents: forecast.remainingCents ?? (forecast.budgetCents - forecast.spentCents),
|
||||
projected: forecast.burnRate > 0
|
||||
? fmtEur(forecast.spentCents + Math.max(0, forecast.budgetCents - forecast.spentCents))
|
||||
: fmtEur(forecast.spentCents),
|
||||
projectedCents: forecast.burnRate > 0
|
||||
? Math.max(forecast.spentCents, forecast.budgetCents)
|
||||
: forecast.spentCents,
|
||||
remaining: fmtEur(forecast.remainingCents ?? forecast.budgetCents - forecast.spentCents),
|
||||
remainingCents: forecast.remainingCents ?? forecast.budgetCents - forecast.spentCents,
|
||||
projected:
|
||||
forecast.burnRate > 0
|
||||
? fmtEur(forecast.spentCents + Math.max(0, forecast.budgetCents - forecast.spentCents))
|
||||
: fmtEur(forecast.spentCents),
|
||||
projectedCents:
|
||||
forecast.burnRate > 0
|
||||
? Math.max(forecast.spentCents, forecast.budgetCents)
|
||||
: forecast.spentCents,
|
||||
burnRate: fmtEur(forecast.burnRate),
|
||||
burnRateCents: forecast.burnRate,
|
||||
utilization: `${forecast.pctUsed}%`,
|
||||
@@ -203,11 +202,8 @@ export function mapBudgetForecastDetailRows(
|
||||
activeAssignmentCount: forecast.activeAssignmentCount ?? null,
|
||||
calendarLocations: forecast.calendarLocations ?? [],
|
||||
derivation: forecast.derivation ?? null,
|
||||
burnStatus: forecast.pctUsed >= 100
|
||||
? "ahead"
|
||||
: forecast.burnRate > 0
|
||||
? "on_track"
|
||||
: "not_started",
|
||||
burnStatus:
|
||||
forecast.pctUsed >= 100 ? "ahead" : forecast.burnRate > 0 ? "on_track" : "not_started",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
getDashboardSkillGaps,
|
||||
getDashboardSkillGapSummary,
|
||||
getDashboardTopValueResources,
|
||||
} from "@capakraken/application";
|
||||
import { round1 } from "@capakraken/shared";
|
||||
} from "@nexus/application";
|
||||
import { round1 } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { anonymizeResources, getAnonymizationDirectory } from "../lib/anonymization.js";
|
||||
import { cacheGet, cacheSet } from "../lib/cache.js";
|
||||
@@ -32,7 +32,7 @@ type TopValueResourceRow = {
|
||||
displayName: string;
|
||||
chapter: string | null;
|
||||
valueScore: number | null;
|
||||
valueScoreBreakdown: import("@capakraken/shared").ValueScoreBreakdown | null;
|
||||
valueScoreBreakdown: import("@nexus/shared").ValueScoreBreakdown | null;
|
||||
valueScoreUpdatedAt: Date | null;
|
||||
lcrCents: number;
|
||||
countryCode: string | null;
|
||||
@@ -74,8 +74,12 @@ type DashboardPeakTimesInput = z.infer<typeof dashboardPeakTimesInputSchema>;
|
||||
type DashboardTopValueResourcesInput = z.infer<typeof dashboardTopValueResourcesInputSchema>;
|
||||
type DashboardDemandInput = z.infer<typeof dashboardDemandInputSchema>;
|
||||
type DashboardDetailInput = z.infer<typeof dashboardDetailInputSchema>;
|
||||
type DashboardChargeabilityOverviewInput = z.infer<typeof dashboardChargeabilityOverviewInputSchema>;
|
||||
type DashboardChargeabilityOverviewRead = Awaited<ReturnType<typeof getDashboardChargeabilityOverview>>;
|
||||
type DashboardChargeabilityOverviewInput = z.infer<
|
||||
typeof dashboardChargeabilityOverviewInputSchema
|
||||
>;
|
||||
type DashboardChargeabilityOverviewRead = Awaited<
|
||||
ReturnType<typeof getDashboardChargeabilityOverview>
|
||||
>;
|
||||
|
||||
function formatPct(value: number): string {
|
||||
return `${Math.round(value)}%`;
|
||||
@@ -89,9 +93,10 @@ function mapStatisticsDetail(overview: Awaited<ReturnType<typeof getDashboardOve
|
||||
totalAllocations: overview.totalAllocations,
|
||||
approvedVacations: overview.approvedVacations,
|
||||
totalEstimates: overview.totalEstimates,
|
||||
totalBudget: overview.budgetSummary.totalBudgetCents > 0
|
||||
? fmtEur(overview.budgetSummary.totalBudgetCents)
|
||||
: "N/A",
|
||||
totalBudget:
|
||||
overview.budgetSummary.totalBudgetCents > 0
|
||||
? fmtEur(overview.budgetSummary.totalBudgetCents)
|
||||
: "N/A",
|
||||
projectsByStatus: Object.fromEntries(
|
||||
overview.projectsByStatus.map((entry) => [entry.status, entry.count]),
|
||||
),
|
||||
@@ -190,31 +195,34 @@ async function getTopValueResourcesCached(
|
||||
}
|
||||
|
||||
function getUserRole(ctx: DashboardProcedureContext) {
|
||||
return (ctx.session?.user as { role?: string } | undefined)?.role
|
||||
?? ctx.dbUser?.systemRole
|
||||
?? "USER";
|
||||
return (
|
||||
(ctx.session?.user as { role?: string } | undefined)?.role ?? ctx.dbUser?.systemRole ?? "USER"
|
||||
);
|
||||
}
|
||||
|
||||
function mapChargeabilityByChapter(
|
||||
rows: DashboardChargeabilityOverviewRead["rows"],
|
||||
month: string,
|
||||
) {
|
||||
const chapterMap = new Map<string, {
|
||||
headcount: number;
|
||||
avgTargetSum: number;
|
||||
avgActualSum: number;
|
||||
avgExpectedSum: number;
|
||||
derivedHeadcount: number;
|
||||
baseAvailableHours: number;
|
||||
effectiveAvailableHours: number;
|
||||
actualBookedHours: number;
|
||||
expectedBookedHours: number;
|
||||
targetBookedHours: number;
|
||||
publicHolidayHoursDeduction: number;
|
||||
absenceDayEquivalent: number;
|
||||
absenceHoursDeduction: number;
|
||||
unassignedHours: number;
|
||||
}>();
|
||||
const chapterMap = new Map<
|
||||
string,
|
||||
{
|
||||
headcount: number;
|
||||
avgTargetSum: number;
|
||||
avgActualSum: number;
|
||||
avgExpectedSum: number;
|
||||
derivedHeadcount: number;
|
||||
baseAvailableHours: number;
|
||||
effectiveAvailableHours: number;
|
||||
actualBookedHours: number;
|
||||
expectedBookedHours: number;
|
||||
targetBookedHours: number;
|
||||
publicHolidayHoursDeduction: number;
|
||||
absenceDayEquivalent: number;
|
||||
absenceHoursDeduction: number;
|
||||
unassignedHours: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const row of rows) {
|
||||
const chapter = row.chapter ?? "Unassigned";
|
||||
@@ -267,27 +275,28 @@ function mapChargeabilityByChapter(
|
||||
avgTarget: formatPct(summary.avgTargetSum / summary.headcount),
|
||||
avgActual: formatPct(summary.avgActualSum / summary.headcount),
|
||||
avgExpected: formatPct(summary.avgExpectedSum / summary.headcount),
|
||||
explainability: summary.derivedHeadcount > 0
|
||||
? {
|
||||
month,
|
||||
resourceCount: summary.headcount,
|
||||
derivedHeadcount: summary.derivedHeadcount,
|
||||
baseAvailableHours: round1(summary.baseAvailableHours),
|
||||
effectiveAvailableHours: round1(summary.effectiveAvailableHours),
|
||||
actualBookedHours: round1(summary.actualBookedHours),
|
||||
expectedBookedHours: round1(summary.expectedBookedHours),
|
||||
targetBookedHours: round1(summary.targetBookedHours),
|
||||
publicHolidayHoursDeduction: round1(summary.publicHolidayHoursDeduction),
|
||||
absenceDayEquivalent: round1(summary.absenceDayEquivalent),
|
||||
absenceHoursDeduction: round1(summary.absenceHoursDeduction),
|
||||
unassignedHours: round1(summary.unassignedHours),
|
||||
}
|
||||
: null,
|
||||
explainability:
|
||||
summary.derivedHeadcount > 0
|
||||
? {
|
||||
month,
|
||||
resourceCount: summary.headcount,
|
||||
derivedHeadcount: summary.derivedHeadcount,
|
||||
baseAvailableHours: round1(summary.baseAvailableHours),
|
||||
effectiveAvailableHours: round1(summary.effectiveAvailableHours),
|
||||
actualBookedHours: round1(summary.actualBookedHours),
|
||||
expectedBookedHours: round1(summary.expectedBookedHours),
|
||||
targetBookedHours: round1(summary.targetBookedHours),
|
||||
publicHolidayHoursDeduction: round1(summary.publicHolidayHoursDeduction),
|
||||
absenceDayEquivalent: round1(summary.absenceDayEquivalent),
|
||||
absenceHoursDeduction: round1(summary.absenceHoursDeduction),
|
||||
unassignedHours: round1(summary.unassignedHours),
|
||||
}
|
||||
: null,
|
||||
}))
|
||||
.sort((left, right) => (
|
||||
right.headcount - left.headcount
|
||||
|| left.chapter.localeCompare(right.chapter)
|
||||
));
|
||||
.sort(
|
||||
(left, right) =>
|
||||
right.headcount - left.headcount || left.chapter.localeCompare(right.chapter),
|
||||
);
|
||||
}
|
||||
|
||||
function getProjectHealthRating(overall: number): "healthy" | "at_risk" | "critical" {
|
||||
@@ -326,14 +335,14 @@ export async function getDashboardDemandRead(
|
||||
return getDemandCached(ctx.db, input);
|
||||
}
|
||||
|
||||
export async function getDashboardDetail(ctx: DashboardProcedureContext, input: DashboardDetailInput) {
|
||||
export async function getDashboardDetail(
|
||||
ctx: DashboardProcedureContext,
|
||||
input: DashboardDetailInput,
|
||||
) {
|
||||
const section = input.section;
|
||||
const result: Record<string, unknown> = {};
|
||||
const needsOverview = (
|
||||
section === "all"
|
||||
|| section === "peak_times"
|
||||
|| section === "demand_pipeline"
|
||||
);
|
||||
const needsOverview =
|
||||
section === "all" || section === "peak_times" || section === "demand_pipeline";
|
||||
const overview = needsOverview ? await getOverviewCached(ctx.db) : null;
|
||||
const now = new Date();
|
||||
const rangeStart = overview?.budgetBasis.windowStart
|
||||
@@ -467,8 +476,7 @@ export async function getDashboardDetail(ctx: DashboardProcedureContext, input:
|
||||
remainingBudgetCents: project.remainingBudgetCents ?? null,
|
||||
calendarContextCount: project.derivation?.calendarContextCount ?? 0,
|
||||
holidayAwareAssignmentCount: project.derivation?.holidayAwareAssignmentCount ?? 0,
|
||||
publicHolidayCostDeductionCents:
|
||||
project.derivation?.publicHolidayCostDeductionCents ?? 0,
|
||||
publicHolidayCostDeductionCents: project.derivation?.publicHolidayCostDeductionCents ?? 0,
|
||||
absenceCostDeductionCents: project.derivation?.absenceCostDeductionCents ?? 0,
|
||||
},
|
||||
}));
|
||||
@@ -478,15 +486,13 @@ export async function getDashboardDetail(ctx: DashboardProcedureContext, input:
|
||||
const skillGapSummary = await getDashboardSkillGapSummaryRead(ctx);
|
||||
result.skillGaps = {
|
||||
totalOpenPositions: skillGapSummary.totalOpenPositions,
|
||||
roleGaps: skillGapSummary.roleGaps
|
||||
.slice(0, 10)
|
||||
.map((gap) => ({
|
||||
role: gap.role,
|
||||
gap: gap.gap,
|
||||
needed: gap.needed,
|
||||
filled: gap.filled,
|
||||
fillRate: gap.fillRate,
|
||||
})),
|
||||
roleGaps: skillGapSummary.roleGaps.slice(0, 10).map((gap) => ({
|
||||
role: gap.role,
|
||||
gap: gap.gap,
|
||||
needed: gap.needed,
|
||||
filled: gap.filled,
|
||||
fillRate: gap.fillRate,
|
||||
})),
|
||||
topSkillsInSupply: skillGapSummary.skillSupplyTop10.slice(0, 5),
|
||||
resourcesByRole: skillGapSummary.resourcesByRole.slice(0, 5),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
DispoStagedRecordType,
|
||||
ImportBatchStatus,
|
||||
StagedRecordStatus,
|
||||
} from "@capakraken/db";
|
||||
import { DispoStagedRecordType, ImportBatchStatus, StagedRecordStatus } from "@nexus/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
type StagedRecordAction = "APPROVE" | "REJECT" | "SKIP";
|
||||
@@ -24,9 +20,7 @@ export function assertImportBatchCancelable(input: {
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveDispoStagedRecordStatus(
|
||||
action: StagedRecordAction,
|
||||
): StagedRecordStatus {
|
||||
export function resolveDispoStagedRecordStatus(action: StagedRecordAction): StagedRecordStatus {
|
||||
switch (action) {
|
||||
case "APPROVE":
|
||||
return StagedRecordStatus.APPROVED;
|
||||
@@ -76,14 +70,10 @@ export function buildCancelledImportBatchUpdateData() {
|
||||
return { status: ImportBatchStatus.CANCELLED } as const;
|
||||
}
|
||||
|
||||
export function buildResolvedStagedRecordUpdateData(
|
||||
action: StagedRecordAction,
|
||||
) {
|
||||
export function buildResolvedStagedRecordUpdateData(action: StagedRecordAction) {
|
||||
return { status: resolveDispoStagedRecordStatus(action) } as const;
|
||||
}
|
||||
|
||||
export function buildDispoImportCommitAuditSummary(
|
||||
counts: Record<string, unknown>,
|
||||
): string {
|
||||
export function buildDispoImportCommitAuditSummary(counts: Record<string, unknown>): string {
|
||||
return `Committed import batch (${JSON.stringify(counts)})`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
DispoStagedRecordType,
|
||||
} from "@capakraken/db";
|
||||
import { commitDispoImportBatch } from "@capakraken/application";
|
||||
import { DispoStagedRecordType } from "@nexus/db";
|
||||
import { commitDispoImportBatch } from "@nexus/application";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
import {
|
||||
@@ -73,8 +71,12 @@ export async function commitImportBatch(
|
||||
) {
|
||||
const result = await commitDispoImportBatch(db, {
|
||||
importBatchId: input.importBatchId,
|
||||
...(input.allowTbdUnresolved !== undefined ? { allowTbdUnresolved: input.allowTbdUnresolved } : {}),
|
||||
...(input.importTbdProjects !== undefined ? { importTbdProjects: input.importTbdProjects } : {}),
|
||||
...(input.allowTbdUnresolved !== undefined
|
||||
? { allowTbdUnresolved: input.allowTbdUnresolved }
|
||||
: {}),
|
||||
...(input.importTbdProjects !== undefined
|
||||
? { importTbdProjects: input.importTbdProjects }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const counts = result as unknown as Record<string, unknown>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { DispoStagedRecordType, ImportBatchStatus, StagedRecordStatus } from "@capakraken/db";
|
||||
import { DispoStagedRecordType, ImportBatchStatus, StagedRecordStatus } from "@nexus/db";
|
||||
import {
|
||||
assessDispoImportReadiness,
|
||||
stageDispoImportBatch as stageDispoImportBatchApplication,
|
||||
} from "@capakraken/application";
|
||||
} from "@nexus/application";
|
||||
import { z } from "zod";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
ImportBatchStatus,
|
||||
StagedRecordStatus,
|
||||
} from "@capakraken/db";
|
||||
import { ImportBatchStatus, StagedRecordStatus } from "@nexus/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
type PaginationInput = {
|
||||
@@ -146,7 +143,9 @@ export async function listStagedAssignments(
|
||||
where: {
|
||||
importBatchId: input.importBatchId,
|
||||
...(input.status !== undefined ? { status: input.status } : {}),
|
||||
...(input.resourceExternalId !== undefined ? { resourceExternalId: input.resourceExternalId } : {}),
|
||||
...(input.resourceExternalId !== undefined
|
||||
? { resourceExternalId: input.resourceExternalId }
|
||||
: {}),
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
...toCursorPagination(input),
|
||||
@@ -165,7 +164,9 @@ export async function listStagedVacations(
|
||||
const items = await db.stagedVacation.findMany({
|
||||
where: {
|
||||
importBatchId: input.importBatchId,
|
||||
...(input.resourceExternalId !== undefined ? { resourceExternalId: input.resourceExternalId } : {}),
|
||||
...(input.resourceExternalId !== undefined
|
||||
? { resourceExternalId: input.resourceExternalId }
|
||||
: {}),
|
||||
},
|
||||
orderBy: { startDate: "asc" },
|
||||
...toCursorPagination(input),
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import {
|
||||
aggregateByDiscipline,
|
||||
expandScopeToEffort,
|
||||
} from "@capakraken/engine";
|
||||
import { aggregateByDiscipline, expandScopeToEffort } from "@nexus/engine";
|
||||
import {
|
||||
ApplyEffortRulesSchema,
|
||||
CreateEffortRuleSetSchema,
|
||||
UpdateEffortRuleSetSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
@@ -41,10 +38,7 @@ type EffortRuleUpdateInput = z.infer<typeof UpdateEffortRuleSetSchema>;
|
||||
type EffortRulePreviewInput = z.infer<typeof effortRulePreviewInputSchema>;
|
||||
type EffortRuleApplyInput = z.infer<typeof ApplyEffortRulesSchema>;
|
||||
|
||||
async function getEffortRuleSetOrThrow(
|
||||
ctx: EffortRuleProcedureContext,
|
||||
id: string,
|
||||
) {
|
||||
async function getEffortRuleSetOrThrow(ctx: EffortRuleProcedureContext, id: string) {
|
||||
return findUniqueOrThrow(
|
||||
ctx.db.effortRuleSet.findUnique({
|
||||
where: { id },
|
||||
@@ -77,9 +71,7 @@ async function getEstimateWithLatestVersionOrThrow(
|
||||
);
|
||||
}
|
||||
|
||||
export async function listEffortRuleSets(
|
||||
ctx: EffortRuleProcedureContext,
|
||||
) {
|
||||
export async function listEffortRuleSets(ctx: EffortRuleProcedureContext) {
|
||||
return ctx.db.effortRuleSet.findMany({
|
||||
include: effortRuleInclude,
|
||||
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import type {
|
||||
EffortRuleInput,
|
||||
ScopeItemInput,
|
||||
} from "@capakraken/engine";
|
||||
import {
|
||||
CreateEffortRuleSetSchema,
|
||||
UpdateEffortRuleSetSchema,
|
||||
} from "@capakraken/shared";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import type { EffortRuleInput, ScopeItemInput } from "@nexus/engine";
|
||||
import { CreateEffortRuleSetSchema, UpdateEffortRuleSetSchema } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
type CreateEffortRuleSetInput = z.infer<typeof CreateEffortRuleSetSchema>;
|
||||
@@ -104,21 +98,20 @@ export function buildEffortRuleSetUpdateData(
|
||||
};
|
||||
}
|
||||
|
||||
export function toScopeItemInputs(
|
||||
scopeItems: ScopeItemRecord[],
|
||||
): ScopeItemInput[] {
|
||||
return scopeItems.map((scopeItem) => ({
|
||||
name: scopeItem.name,
|
||||
scopeType: scopeItem.scopeType as ScopeItemInput["scopeType"],
|
||||
frameCount: scopeItem.frameCount,
|
||||
itemCount: scopeItem.itemCount,
|
||||
unitMode: (scopeItem.unitMode ?? null) as Exclude<ScopeItemInput["unitMode"], undefined>,
|
||||
}) as ScopeItemInput);
|
||||
export function toScopeItemInputs(scopeItems: ScopeItemRecord[]): ScopeItemInput[] {
|
||||
return scopeItems.map(
|
||||
(scopeItem) =>
|
||||
({
|
||||
name: scopeItem.name,
|
||||
scopeType: scopeItem.scopeType as ScopeItemInput["scopeType"],
|
||||
frameCount: scopeItem.frameCount,
|
||||
itemCount: scopeItem.itemCount,
|
||||
unitMode: (scopeItem.unitMode ?? null) as Exclude<ScopeItemInput["unitMode"], undefined>,
|
||||
}) as ScopeItemInput,
|
||||
);
|
||||
}
|
||||
|
||||
export function toEffortRuleEngineInputs(
|
||||
rules: EffortRuleRecord[],
|
||||
): EffortRuleInput[] {
|
||||
export function toEffortRuleEngineInputs(rules: EffortRuleRecord[]): EffortRuleInput[] {
|
||||
return rules.map((rule) => ({
|
||||
scopeType: rule.scopeType as EffortRuleInput["scopeType"],
|
||||
discipline: rule.discipline,
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
ApplyEffortRulesSchema,
|
||||
CreateEffortRuleSetSchema,
|
||||
UpdateEffortRuleSetSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { createTRPCRouter, controllerProcedure, managerProcedure } from "../trpc.js";
|
||||
import {
|
||||
applyEffortRules,
|
||||
|
||||
@@ -9,10 +9,13 @@ import {
|
||||
setEntitlement as setEntitlementUseCase,
|
||||
bulkSetEntitlements as bulkSetEntitlementsUseCase,
|
||||
type ReadEntitlementBalanceDeps,
|
||||
} from "@capakraken/application";
|
||||
} from "@nexus/application";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
import { loadResourceHolidayContext } from "../lib/resource-holiday-context.js";
|
||||
import { countCalendarDaysInPeriod, countVacationChargeableDays } from "../lib/vacation-day-count.js";
|
||||
import {
|
||||
countCalendarDaysInPeriod,
|
||||
countVacationChargeableDays,
|
||||
} from "../lib/vacation-day-count.js";
|
||||
import {
|
||||
countVacationChargeableDaysFromSnapshot,
|
||||
parseVacationSnapshotDateList,
|
||||
@@ -168,7 +171,11 @@ export async function bulkSetEntitlements(
|
||||
entityName: `Bulk Entitlement ${input.year}`,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
after: { year: input.year, entitledDays: input.entitledDays, resourceCount: result.updated } as unknown as Record<string, unknown>,
|
||||
after: {
|
||||
year: input.year,
|
||||
entitledDays: input.entitledDays,
|
||||
resourceCount: result.updated,
|
||||
} as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
summary: `Bulk set entitlement to ${input.entitledDays} days for ${result.updated} resources (${input.year})`,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import { CommercialTermsSchema, PermissionKey, UpdateCommercialTermsSchema } from "@capakraken/shared";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import { CommercialTermsSchema, PermissionKey, UpdateCommercialTermsSchema } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { controllerProcedure, managerProcedure, requirePermission } from "../trpc.js";
|
||||
@@ -26,9 +26,7 @@ export const estimateCommercialProcedures = {
|
||||
|
||||
const version = estimate.versions[0]!;
|
||||
const raw = version.commercialTerms;
|
||||
const terms = raw
|
||||
? CommercialTermsSchema.parse(raw)
|
||||
: CommercialTermsSchema.parse({});
|
||||
const terms = raw ? CommercialTermsSchema.parse(raw) : CommercialTermsSchema.parse({});
|
||||
|
||||
return { versionId: version.id, terms };
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { normalizeEstimateDemandLine, summarizeEstimateDemandLines } from "@capakraken/engine";
|
||||
import { CreateEstimateSchema } from "@capakraken/shared";
|
||||
import { normalizeEstimateDemandLine, summarizeEstimateDemandLines } from "@nexus/engine";
|
||||
import { CreateEstimateSchema } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { lookupRatesBatch } from "../lib/rate-card-lookup.js";
|
||||
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import {
|
||||
aggregateWeeklyByChapter,
|
||||
aggregateWeeklyToMonthly,
|
||||
distributeHoursToWeeks,
|
||||
generateWeekRange,
|
||||
} from "@capakraken/engine";
|
||||
import { PermissionKey, GenerateWeeklyPhasingSchema } from "@capakraken/shared";
|
||||
} from "@nexus/engine";
|
||||
import { PermissionKey, GenerateWeeklyPhasingSchema } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import {
|
||||
controllerProcedure,
|
||||
managerProcedure,
|
||||
requirePermission,
|
||||
} from "../trpc.js";
|
||||
import { getEstimateById } from "@capakraken/application";
|
||||
import { controllerProcedure, managerProcedure, requirePermission } from "../trpc.js";
|
||||
import { getEstimateById } from "@nexus/application";
|
||||
|
||||
type WeeklyPhasingMeta = {
|
||||
startDate: string;
|
||||
@@ -38,9 +34,7 @@ export const estimatePhasingProcedures = {
|
||||
"Estimate",
|
||||
);
|
||||
|
||||
const workingVersion = estimate.versions.find(
|
||||
(version) => version.status === "WORKING",
|
||||
);
|
||||
const workingVersion = estimate.versions.find((version) => version.status === "WORKING");
|
||||
|
||||
if (!workingVersion) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -4,25 +4,22 @@ import {
|
||||
createEstimateExport,
|
||||
createEstimatePlanningHandoff,
|
||||
updateEstimateDraft,
|
||||
} from "@capakraken/application";
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
} from "@nexus/application";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import {
|
||||
CloneEstimateSchema,
|
||||
CreateEstimateExportSchema,
|
||||
CreateEstimatePlanningHandoffSchema,
|
||||
CreateEstimateSchema,
|
||||
UpdateEstimateDraftSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { lookupRate } from "../lib/rate-card-lookup.js";
|
||||
import { emitAllocationCreated } from "../sse/event-bus.js";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
import {
|
||||
autoFillDemandLineRates,
|
||||
withComputedMetrics,
|
||||
} from "./estimate-demand-lines.js";
|
||||
import { autoFillDemandLineRates, withComputedMetrics } from "./estimate-demand-lines.js";
|
||||
|
||||
type EstimateProcedureContext = Pick<TRPCContext, "db" | "dbUser">;
|
||||
|
||||
@@ -77,9 +74,7 @@ type CreateEstimateInput = z.infer<typeof CreateEstimateSchema>;
|
||||
type CloneEstimateInput = z.infer<typeof CloneEstimateSchema>;
|
||||
type UpdateEstimateDraftInput = z.infer<typeof UpdateEstimateDraftSchema>;
|
||||
type CreateEstimateExportInput = z.infer<typeof CreateEstimateExportSchema>;
|
||||
type CreateEstimatePlanningHandoffInput = z.infer<
|
||||
typeof CreateEstimatePlanningHandoffSchema
|
||||
>;
|
||||
type CreateEstimatePlanningHandoffInput = z.infer<typeof CreateEstimatePlanningHandoffSchema>;
|
||||
type LookupDemandLineRateInput = z.infer<typeof lookupDemandLineRateInputSchema>;
|
||||
|
||||
export async function createEstimateRecord(
|
||||
@@ -96,8 +91,11 @@ export async function createEstimateRecord(
|
||||
);
|
||||
}
|
||||
|
||||
const { demandLines: enrichedLines, autoFilledIndices } =
|
||||
await autoFillDemandLineRates(ctx.db, input.demandLines, input.projectId);
|
||||
const { demandLines: enrichedLines, autoFilledIndices } = await autoFillDemandLineRates(
|
||||
ctx.db,
|
||||
input.demandLines,
|
||||
input.projectId,
|
||||
);
|
||||
const enrichedInput = { ...input, demandLines: enrichedLines };
|
||||
|
||||
const estimate = await ctx.db.$transaction(async (tx) => {
|
||||
@@ -196,8 +194,11 @@ export async function updateEstimateDraftRecord(
|
||||
effectiveProjectId = existing?.projectId ?? undefined;
|
||||
}
|
||||
|
||||
const { demandLines: enrichedLines, autoFilledIndices } =
|
||||
await autoFillDemandLineRates(ctx.db, input.demandLines, effectiveProjectId);
|
||||
const { demandLines: enrichedLines, autoFilledIndices } = await autoFillDemandLineRates(
|
||||
ctx.db,
|
||||
input.demandLines,
|
||||
effectiveProjectId,
|
||||
);
|
||||
const enrichedInput = { ...input, demandLines: enrichedLines };
|
||||
|
||||
let estimate;
|
||||
@@ -220,9 +221,8 @@ export async function updateEstimateDraftRecord(
|
||||
name: updated.name,
|
||||
status: updated.status,
|
||||
latestVersionNumber: updated.latestVersionNumber,
|
||||
workingVersionId: updated.versions.find(
|
||||
(version) => version.status === "WORKING",
|
||||
)?.id,
|
||||
workingVersionId: updated.versions.find((version) => version.status === "WORKING")
|
||||
?.id,
|
||||
autoFilledRateCardLines: autoFilledIndices.length,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
@@ -338,11 +338,7 @@ export async function createEstimatePlanningHandoffRecord(
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
code: "NOT_FOUND",
|
||||
messages: [
|
||||
"Estimate not found",
|
||||
"Estimate version not found",
|
||||
"Linked project not found",
|
||||
],
|
||||
messages: ["Estimate not found", "Estimate version not found", "Linked project not found"],
|
||||
},
|
||||
{
|
||||
code: "PRECONDITION_FAILED",
|
||||
@@ -354,8 +350,7 @@ export async function createEstimatePlanningHandoffRecord(
|
||||
"Linked project has an invalid date range",
|
||||
],
|
||||
predicates: [
|
||||
(message) =>
|
||||
message.startsWith("Project window has no working days for demand line"),
|
||||
(message) => message.startsWith("Project window has no working days for demand line"),
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getEstimateById, listEstimates } from "@capakraken/application";
|
||||
import { summarizeEstimateDemandLines } from "@capakraken/engine";
|
||||
import { EstimateListFiltersSchema } from "@capakraken/shared";
|
||||
import { getEstimateById, listEstimates } from "@nexus/application";
|
||||
import { summarizeEstimateDemandLines } from "@nexus/engine";
|
||||
import { EstimateListFiltersSchema } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
@@ -79,13 +79,18 @@ async function readEstimateVersionSnapshot(
|
||||
const version = estimate.versions[0]!;
|
||||
const demandSummary = summarizeEstimateDemandLines(version.demandLines);
|
||||
|
||||
const chapterTotals = version.demandLines.reduce<Record<string, {
|
||||
lineCount: number;
|
||||
hours: number;
|
||||
costTotalCents: number;
|
||||
priceTotalCents: number;
|
||||
currency: string;
|
||||
}>>((acc, line) => {
|
||||
const chapterTotals = version.demandLines.reduce<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
lineCount: number;
|
||||
hours: number;
|
||||
costTotalCents: number;
|
||||
priceTotalCents: number;
|
||||
currency: string;
|
||||
}
|
||||
>
|
||||
>((acc, line) => {
|
||||
const key = line.chapter ?? "Unassigned";
|
||||
const current = acc[key] ?? {
|
||||
lineCount: 0,
|
||||
@@ -107,10 +112,13 @@ async function readEstimateVersionSnapshot(
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const assumptionCategoryTotals = version.assumptions.reduce<Record<string, number>>((acc, assumption) => {
|
||||
acc[assumption.category] = (acc[assumption.category] ?? 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
const assumptionCategoryTotals = version.assumptions.reduce<Record<string, number>>(
|
||||
(acc, assumption) => {
|
||||
acc[assumption.category] = (acc[assumption.category] ?? 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
estimate: {
|
||||
@@ -163,21 +171,17 @@ export const estimateReadProcedures = {
|
||||
list: controllerProcedure
|
||||
.input(EstimateListFiltersSchema.default({}))
|
||||
.query(async ({ ctx, input }) =>
|
||||
listEstimates(
|
||||
ctx.db as unknown as Parameters<typeof listEstimates>[0],
|
||||
input,
|
||||
)),
|
||||
listEstimates(ctx.db as unknown as Parameters<typeof listEstimates>[0], input),
|
||||
),
|
||||
|
||||
getById: controllerProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) =>
|
||||
findUniqueOrThrow(
|
||||
getEstimateById(
|
||||
ctx.db as unknown as Parameters<typeof getEstimateById>[0],
|
||||
input.id,
|
||||
),
|
||||
getEstimateById(ctx.db as unknown as Parameters<typeof getEstimateById>[0], input.id),
|
||||
"Estimate",
|
||||
)),
|
||||
),
|
||||
),
|
||||
|
||||
listVersions: controllerProcedure
|
||||
.input(z.object({ estimateId: z.string() }))
|
||||
@@ -215,12 +219,15 @@ export const estimateReadProcedures = {
|
||||
},
|
||||
}),
|
||||
"Estimate",
|
||||
)),
|
||||
),
|
||||
),
|
||||
|
||||
getVersionSnapshot: controllerProcedure
|
||||
.input(z.object({ estimateId: z.string(), versionId: z.string().optional() }))
|
||||
.query(async ({ ctx, input }) => readEstimateVersionSnapshot(
|
||||
ctx.db as unknown as Parameters<typeof getEstimateById>[0],
|
||||
input,
|
||||
)),
|
||||
.query(async ({ ctx, input }) =>
|
||||
readEstimateVersionSnapshot(
|
||||
ctx.db as unknown as Parameters<typeof getEstimateById>[0],
|
||||
input,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -2,14 +2,14 @@ import {
|
||||
approveEstimateVersion,
|
||||
createEstimateRevision,
|
||||
submitEstimateVersion,
|
||||
} from "@capakraken/application";
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
} from "@nexus/application";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import {
|
||||
ApproveEstimateVersionSchema,
|
||||
CreateEstimateRevisionSchema,
|
||||
PermissionKey,
|
||||
SubmitEstimateVersionSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { managerProcedure, requirePermission } from "../trpc.js";
|
||||
import { rethrowEstimateRouterError } from "./estimate-procedure-support.js";
|
||||
|
||||
@@ -55,10 +55,7 @@ export const estimateVersionWorkflowProcedures = {
|
||||
},
|
||||
{
|
||||
code: "PRECONDITION_FAILED",
|
||||
messages: [
|
||||
"Estimate has no working version",
|
||||
"Only working versions can be submitted",
|
||||
],
|
||||
messages: ["Estimate has no working version", "Only working versions can be submitted"],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
CreateEstimateSchema,
|
||||
PermissionKey,
|
||||
UpdateEstimateDraftSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import {
|
||||
cloneEstimateRecord,
|
||||
createEstimateExportRecord,
|
||||
@@ -32,19 +32,15 @@ export const estimateRouter = createTRPCRouter({
|
||||
...estimatePhasingProcedures,
|
||||
...estimateVersionWorkflowProcedures,
|
||||
|
||||
create: managerProcedure
|
||||
.input(CreateEstimateSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
return createEstimateRecord(ctx, input);
|
||||
}),
|
||||
create: managerProcedure.input(CreateEstimateSchema).mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
return createEstimateRecord(ctx, input);
|
||||
}),
|
||||
|
||||
clone: managerProcedure
|
||||
.input(CloneEstimateSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
return cloneEstimateRecord(ctx, input);
|
||||
}),
|
||||
clone: managerProcedure.input(CloneEstimateSchema).mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
return cloneEstimateRecord(ctx, input);
|
||||
}),
|
||||
|
||||
updateDraft: managerProcedure
|
||||
.input(UpdateEstimateDraftSchema)
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import {
|
||||
applyExperienceMultipliers,
|
||||
applyExperienceMultipliersBatch,
|
||||
} from "@capakraken/engine";
|
||||
import { applyExperienceMultipliers, applyExperienceMultipliersBatch } from "@nexus/engine";
|
||||
import {
|
||||
ApplyExperienceMultipliersSchema,
|
||||
CreateExperienceMultiplierSetSchema,
|
||||
UpdateExperienceMultiplierSetSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
@@ -68,9 +65,7 @@ async function getEstimateWithLatestVersionOrThrow(
|
||||
orderBy: { versionNumber: "desc" },
|
||||
take: 1,
|
||||
include: {
|
||||
demandLines: orderedDemandLines
|
||||
? { orderBy: { createdAt: "asc" } }
|
||||
: true,
|
||||
demandLines: orderedDemandLines ? { orderBy: { createdAt: "asc" } } : true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -79,9 +74,7 @@ async function getEstimateWithLatestVersionOrThrow(
|
||||
);
|
||||
}
|
||||
|
||||
export async function listExperienceMultiplierSets(
|
||||
ctx: ExperienceMultiplierProcedureContext,
|
||||
) {
|
||||
export async function listExperienceMultiplierSets(ctx: ExperienceMultiplierProcedureContext) {
|
||||
return ctx.db.experienceMultiplierSet.findMany({
|
||||
include: experienceMultiplierRuleInclude,
|
||||
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
||||
@@ -209,10 +202,7 @@ export async function previewExperienceMultipliers(
|
||||
const demandLines = version.demandLines;
|
||||
|
||||
const previews = demandLines.map((line) => {
|
||||
const result = applyExperienceMultipliers(
|
||||
buildExperienceMultiplierInput(line),
|
||||
engineRules,
|
||||
);
|
||||
const result = applyExperienceMultipliers(buildExperienceMultiplierInput(line), engineRules);
|
||||
|
||||
return {
|
||||
demandLineId: line.id,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import type { ExperienceMultiplierRule as EngineRule } from "@capakraken/engine";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import type { ExperienceMultiplierRule as EngineRule } from "@nexus/engine";
|
||||
import {
|
||||
CreateExperienceMultiplierSetSchema,
|
||||
UpdateExperienceMultiplierSetSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
type CreateExperienceMultiplierSetInput = z.infer<typeof CreateExperienceMultiplierSetSchema>;
|
||||
@@ -50,10 +50,7 @@ export const experienceMultiplierRuleInclude = {
|
||||
rules: { orderBy: { sortOrder: "asc" as const } },
|
||||
} as const;
|
||||
|
||||
function buildExperienceMultiplierRuleRow(
|
||||
input: ExperienceMultiplierRuleRowInput,
|
||||
index: number,
|
||||
) {
|
||||
function buildExperienceMultiplierRuleRow(input: ExperienceMultiplierRuleRowInput, index: number) {
|
||||
return {
|
||||
...(input.chapter ? { chapter: input.chapter } : {}),
|
||||
...(input.location ? { location: input.location } : {}),
|
||||
@@ -61,7 +58,9 @@ function buildExperienceMultiplierRuleRow(
|
||||
costMultiplier: input.costMultiplier,
|
||||
billMultiplier: input.billMultiplier,
|
||||
...(input.shoringRatio !== undefined ? { shoringRatio: input.shoringRatio } : {}),
|
||||
...(input.additionalEffortRatio !== undefined ? { additionalEffortRatio: input.additionalEffortRatio } : {}),
|
||||
...(input.additionalEffortRatio !== undefined
|
||||
? { additionalEffortRatio: input.additionalEffortRatio }
|
||||
: {}),
|
||||
...(input.description ? { description: input.description } : {}),
|
||||
sortOrder: input.sortOrder ?? index,
|
||||
};
|
||||
@@ -118,23 +117,27 @@ export function toExperienceMultiplierEngineRules(
|
||||
costMultiplier: rule.costMultiplier,
|
||||
billMultiplier: rule.billMultiplier,
|
||||
...(rule.shoringRatio != null ? { shoringRatio: rule.shoringRatio } : {}),
|
||||
...(rule.additionalEffortRatio != null ? { additionalEffortRatio: rule.additionalEffortRatio } : {}),
|
||||
...(rule.additionalEffortRatio != null
|
||||
? { additionalEffortRatio: rule.additionalEffortRatio }
|
||||
: {}),
|
||||
...(rule.description != null ? { description: rule.description } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildExperienceMultiplierInput(
|
||||
line: DemandLineRecord,
|
||||
) {
|
||||
export function buildExperienceMultiplierInput(line: DemandLineRecord) {
|
||||
return {
|
||||
costRateCents: line.costRateCents,
|
||||
billRateCents: line.billRateCents,
|
||||
hours: line.hours,
|
||||
...(line.chapter != null ? { chapter: line.chapter } : {}),
|
||||
...(line.metadata != null && typeof line.metadata === "object" && "location" in (line.metadata as Record<string, unknown>)
|
||||
...(line.metadata != null &&
|
||||
typeof line.metadata === "object" &&
|
||||
"location" in (line.metadata as Record<string, unknown>)
|
||||
? { location: (line.metadata as Record<string, unknown>).location as string }
|
||||
: {}),
|
||||
...(line.staffingAttributes != null && typeof line.staffingAttributes === "object" && "level" in (line.staffingAttributes as Record<string, unknown>)
|
||||
...(line.staffingAttributes != null &&
|
||||
typeof line.staffingAttributes === "object" &&
|
||||
"level" in (line.staffingAttributes as Record<string, unknown>)
|
||||
? { level: (line.staffingAttributes as Record<string, unknown>).level as string }
|
||||
: {}),
|
||||
};
|
||||
@@ -167,7 +170,7 @@ export function buildExperienceMultiplierDemandLineUpdateData(input: {
|
||||
priceTotalCents: newPriceTotal,
|
||||
metadata: {
|
||||
...(typeof input.line.metadata === "object" && input.line.metadata !== null
|
||||
? input.line.metadata as Record<string, unknown>
|
||||
? (input.line.metadata as Record<string, unknown>)
|
||||
: {}),
|
||||
experienceMultiplier: {
|
||||
setId: input.multiplierSet.id,
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
ApplyExperienceMultipliersSchema,
|
||||
CreateExperienceMultiplierSetSchema,
|
||||
UpdateExperienceMultiplierSetSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { createTRPCRouter, controllerProcedure, managerProcedure } from "../trpc.js";
|
||||
import {
|
||||
applyExperienceMultiplierRules,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CreateHolidayCalendarSchema,
|
||||
UpdateHolidayCalendarEntrySchema,
|
||||
UpdateHolidayCalendarSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { makeAuditLogger } from "../lib/audit-helpers.js";
|
||||
@@ -107,12 +107,16 @@ export async function updateHolidayCalendar(
|
||||
data: input.data,
|
||||
});
|
||||
|
||||
await assertHolidayCalendarScopeConsistency(db, {
|
||||
scopeType: existing.scopeType,
|
||||
countryId: existing.countryId,
|
||||
stateCode,
|
||||
metroCityId,
|
||||
}, existing.id);
|
||||
await assertHolidayCalendarScopeConsistency(
|
||||
db,
|
||||
{
|
||||
scopeType: existing.scopeType,
|
||||
countryId: existing.countryId,
|
||||
stateCode,
|
||||
metroCityId,
|
||||
},
|
||||
existing.id,
|
||||
);
|
||||
|
||||
const updated = await db.holidayCalendar.update({
|
||||
where: { id: input.id },
|
||||
@@ -213,14 +217,17 @@ export async function updateHolidayCalendarEntry(
|
||||
db.holidayCalendarEntry.findUnique({ where: { id: input.id } }),
|
||||
"Holiday calendar entry",
|
||||
);
|
||||
const nextDate = input.data.date !== undefined
|
||||
? clampHolidayCalendarDate(input.data.date)
|
||||
: existing.date;
|
||||
const nextDate =
|
||||
input.data.date !== undefined ? clampHolidayCalendarDate(input.data.date) : existing.date;
|
||||
|
||||
await assertHolidayCalendarEntryDateAvailable(db, {
|
||||
holidayCalendarId: existing.holidayCalendarId,
|
||||
date: nextDate,
|
||||
}, existing.id);
|
||||
await assertHolidayCalendarEntryDateAvailable(
|
||||
db,
|
||||
{
|
||||
holidayCalendarId: existing.holidayCalendarId,
|
||||
date: nextDate,
|
||||
},
|
||||
existing.id,
|
||||
);
|
||||
|
||||
const updated = await db.holidayCalendarEntry.update({
|
||||
where: { id: input.id },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PreviewResolvedHolidaysSchema } from "@capakraken/shared";
|
||||
import { PreviewResolvedHolidaysSchema } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js";
|
||||
@@ -33,13 +33,15 @@ function formatResolvedHolidayDetail(holiday: {
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeResolvedHolidaysDetail(holidays: Array<{
|
||||
date: string;
|
||||
name: string;
|
||||
scope: string;
|
||||
calendarName: string;
|
||||
sourceType: string;
|
||||
}>) {
|
||||
function summarizeResolvedHolidaysDetail(
|
||||
holidays: Array<{
|
||||
date: string;
|
||||
name: string;
|
||||
scope: string;
|
||||
calendarName: string;
|
||||
sourceType: string;
|
||||
}>,
|
||||
) {
|
||||
const byScope = new Map<string, number>();
|
||||
const bySourceType = new Map<string, number>();
|
||||
const byCalendar = new Map<string, number>();
|
||||
@@ -63,44 +65,48 @@ function summarizeResolvedHolidaysDetail(holidays: Array<{
|
||||
};
|
||||
}
|
||||
|
||||
const ResolveHolidaysInputSchema = z.object({
|
||||
periodStart: z.coerce.date(),
|
||||
periodEnd: z.coerce.date(),
|
||||
countryId: z.string().optional(),
|
||||
countryCode: z.string().trim().min(1).optional(),
|
||||
stateCode: z.string().trim().min(1).optional(),
|
||||
metroCityId: z.string().optional(),
|
||||
metroCityName: z.string().trim().min(1).optional(),
|
||||
}).superRefine((input, issueCtx) => {
|
||||
if (!input.countryId && !input.countryCode) {
|
||||
issueCtx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Either countryId or countryCode is required.",
|
||||
path: ["countryId"],
|
||||
});
|
||||
}
|
||||
if (input.periodEnd < input.periodStart) {
|
||||
issueCtx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "periodEnd must be on or after periodStart.",
|
||||
path: ["periodEnd"],
|
||||
});
|
||||
}
|
||||
});
|
||||
const ResolveHolidaysInputSchema = z
|
||||
.object({
|
||||
periodStart: z.coerce.date(),
|
||||
periodEnd: z.coerce.date(),
|
||||
countryId: z.string().optional(),
|
||||
countryCode: z.string().trim().min(1).optional(),
|
||||
stateCode: z.string().trim().min(1).optional(),
|
||||
metroCityId: z.string().optional(),
|
||||
metroCityName: z.string().trim().min(1).optional(),
|
||||
})
|
||||
.superRefine((input, issueCtx) => {
|
||||
if (!input.countryId && !input.countryCode) {
|
||||
issueCtx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Either countryId or countryCode is required.",
|
||||
path: ["countryId"],
|
||||
});
|
||||
}
|
||||
if (input.periodEnd < input.periodStart) {
|
||||
issueCtx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "periodEnd must be on or after periodStart.",
|
||||
path: ["periodEnd"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const ResolveResourceHolidaysInputSchema = z.object({
|
||||
resourceId: z.string(),
|
||||
periodStart: z.coerce.date(),
|
||||
periodEnd: z.coerce.date(),
|
||||
}).superRefine((input, issueCtx) => {
|
||||
if (input.periodEnd < input.periodStart) {
|
||||
issueCtx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "periodEnd must be on or after periodStart.",
|
||||
path: ["periodEnd"],
|
||||
});
|
||||
}
|
||||
});
|
||||
const ResolveResourceHolidaysInputSchema = z
|
||||
.object({
|
||||
resourceId: z.string(),
|
||||
periodStart: z.coerce.date(),
|
||||
periodEnd: z.coerce.date(),
|
||||
})
|
||||
.superRefine((input, issueCtx) => {
|
||||
if (input.periodEnd < input.periodStart) {
|
||||
issueCtx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "periodEnd must be on or after periodStart.",
|
||||
path: ["periodEnd"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function readPreviewResolvedHolidaysSnapshot(
|
||||
ctx: HolidayReadContext,
|
||||
@@ -168,11 +174,13 @@ async function readResolvedHolidaysSnapshot(
|
||||
}
|
||||
|
||||
const metroCityName = input.metroCityId
|
||||
? (await ctx.db.metroCity.findUnique({
|
||||
where: { id: input.metroCityId },
|
||||
select: { name: true },
|
||||
}))?.name ?? null
|
||||
: input.metroCityName?.trim() ?? null;
|
||||
? ((
|
||||
await ctx.db.metroCity.findUnique({
|
||||
where: { id: input.metroCityId },
|
||||
select: { name: true },
|
||||
})
|
||||
)?.name ?? null)
|
||||
: (input.metroCityName?.trim() ?? null);
|
||||
|
||||
const resolved = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), {
|
||||
periodStart: input.periodStart,
|
||||
@@ -262,7 +270,9 @@ async function readResolvedResourceHolidaysSnapshot(
|
||||
export const holidayCalendarResolutionReadProcedures = {
|
||||
previewResolvedHolidays: protectedProcedure
|
||||
.input(PreviewResolvedHolidaysSchema)
|
||||
.query(async ({ ctx, input }) => (await readPreviewResolvedHolidaysSnapshot(ctx, input)).holidays),
|
||||
.query(
|
||||
async ({ ctx, input }) => (await readPreviewResolvedHolidaysSnapshot(ctx, input)).holidays,
|
||||
),
|
||||
|
||||
previewResolvedHolidaysDetail: protectedProcedure
|
||||
.input(PreviewResolvedHolidaysSchema)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import type { Prisma } from "@nexus/db";
|
||||
import {
|
||||
CreateHolidayCalendarEntrySchema,
|
||||
CreateHolidayCalendarSchema,
|
||||
UpdateHolidayCalendarEntrySchema,
|
||||
UpdateHolidayCalendarSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
type CreateHolidayCalendarInput = z.infer<typeof CreateHolidayCalendarSchema>;
|
||||
@@ -12,10 +12,7 @@ type UpdateHolidayCalendarInput = z.infer<typeof UpdateHolidayCalendarSchema>;
|
||||
type CreateHolidayCalendarEntryInput = z.infer<typeof CreateHolidayCalendarEntrySchema>;
|
||||
type UpdateHolidayCalendarEntryInput = z.infer<typeof UpdateHolidayCalendarEntrySchema>;
|
||||
|
||||
export const holidayCalendarEntryOrderBy = [
|
||||
{ date: "asc" },
|
||||
{ name: "asc" },
|
||||
] as const;
|
||||
export const holidayCalendarEntryOrderBy = [{ date: "asc" }, { name: "asc" }] as const;
|
||||
|
||||
export const holidayCalendarDetailInclude = {
|
||||
country: { select: { id: true, code: true, name: true } },
|
||||
@@ -41,7 +38,9 @@ export function buildHolidayCalendarCreateData(
|
||||
scopeType: input.scopeType,
|
||||
countryId: input.countryId,
|
||||
...(input.normalizedScope.stateCode ? { stateCode: input.normalizedScope.stateCode } : {}),
|
||||
...(input.normalizedScope.metroCityId ? { metroCityId: input.normalizedScope.metroCityId } : {}),
|
||||
...(input.normalizedScope.metroCityId
|
||||
? { metroCityId: input.normalizedScope.metroCityId }
|
||||
: {}),
|
||||
isActive: input.isActive ?? true,
|
||||
priority: input.priority ?? 0,
|
||||
};
|
||||
@@ -57,7 +56,9 @@ export function buildHolidayCalendarUpdateData(input: {
|
||||
return {
|
||||
...(input.data.name !== undefined ? { name: input.data.name } : {}),
|
||||
...(input.data.stateCode !== undefined ? { stateCode: input.resolvedScope.stateCode } : {}),
|
||||
...(input.data.metroCityId !== undefined ? { metroCityId: input.resolvedScope.metroCityId } : {}),
|
||||
...(input.data.metroCityId !== undefined
|
||||
? { metroCityId: input.resolvedScope.metroCityId }
|
||||
: {}),
|
||||
...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}),
|
||||
...(input.data.priority !== undefined ? { priority: input.data.priority } : {}),
|
||||
};
|
||||
@@ -83,7 +84,9 @@ export function buildHolidayCalendarEntryUpdateData(input: {
|
||||
return {
|
||||
...(input.data.date !== undefined ? { date: input.date } : {}),
|
||||
...(input.data.name !== undefined ? { name: input.data.name } : {}),
|
||||
...(input.data.isRecurringAnnual !== undefined ? { isRecurringAnnual: input.data.isRecurringAnnual } : {}),
|
||||
...(input.data.isRecurringAnnual !== undefined
|
||||
? { isRecurringAnnual: input.data.isRecurringAnnual }
|
||||
: {}),
|
||||
...(input.data.source !== undefined ? { source: input.data.source ?? null } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import {
|
||||
type HolidayCalendarDb,
|
||||
} from "./holiday-calendar-shared.js";
|
||||
import type { HolidayCalendarScopeInput } from "@capakraken/shared";
|
||||
import { type HolidayCalendarDb } from "./holiday-calendar-shared.js";
|
||||
import type { HolidayCalendarScopeInput } from "@nexus/shared";
|
||||
|
||||
type HolidayCalendarScope = HolidayCalendarScopeInput;
|
||||
|
||||
@@ -46,12 +44,14 @@ export function resolveHolidayCalendarUpdateScope(input: {
|
||||
};
|
||||
}) {
|
||||
return {
|
||||
stateCode: input.data.stateCode === undefined
|
||||
? input.existing.stateCode
|
||||
: normalizeHolidayCalendarStateCode(input.data.stateCode),
|
||||
metroCityId: input.data.metroCityId === undefined
|
||||
? input.existing.metroCityId
|
||||
: input.data.metroCityId ?? null,
|
||||
stateCode:
|
||||
input.data.stateCode === undefined
|
||||
? input.existing.stateCode
|
||||
: normalizeHolidayCalendarStateCode(input.data.stateCode),
|
||||
metroCityId:
|
||||
input.data.metroCityId === undefined
|
||||
? input.existing.metroCityId
|
||||
: (input.data.metroCityId ?? null),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,7 @@ import {
|
||||
updateHolidayCalendarEntry,
|
||||
} from "./holiday-calendar-procedure-support.js";
|
||||
import { holidayCalendarResolutionReadProcedures } from "./holiday-calendar-resolution-read.js";
|
||||
import {
|
||||
CreateHolidayCalendarEntrySchema,
|
||||
CreateHolidayCalendarSchema,
|
||||
} from "@capakraken/shared";
|
||||
import { CreateHolidayCalendarEntrySchema, CreateHolidayCalendarSchema } from "@nexus/shared";
|
||||
|
||||
export const holidayCalendarRouter = createTRPCRouter({
|
||||
...holidayCalendarCatalogReadProcedures,
|
||||
@@ -44,5 +41,4 @@ export const holidayCalendarRouter = createTRPCRouter({
|
||||
deleteEntry: adminProcedure
|
||||
.input(holidayCalendarIdInputSchema)
|
||||
.mutation(({ ctx, input }) => deleteHolidayCalendarEntry(ctx, input)),
|
||||
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BlueprintTarget, PermissionKey } from "@capakraken/shared";
|
||||
import type { BlueprintFieldDefinition } from "@capakraken/shared";
|
||||
import { BlueprintTarget, PermissionKey } from "@nexus/shared";
|
||||
import type { BlueprintFieldDefinition } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
import { requirePermission } from "../trpc.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_OPENAI_MODEL } from "@capakraken/shared";
|
||||
import { DEFAULT_OPENAI_MODEL } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type {
|
||||
ChatCompletion,
|
||||
@@ -95,14 +95,18 @@ export async function generateProjectNarrative(
|
||||
);
|
||||
const progressPercent = totalDays > 0 ? Math.round((elapsedDays / totalDays) * 100) : 0;
|
||||
|
||||
const totalDemandHeadcount = project.demandRequirements.reduce((sum, demand) => sum + demand.headcount, 0);
|
||||
const totalDemandHeadcount = project.demandRequirements.reduce(
|
||||
(sum, demand) => sum + demand.headcount,
|
||||
0,
|
||||
);
|
||||
const filledDemandHeadcount = project.demandRequirements.reduce(
|
||||
(sum, demand) => sum + Math.min(demand._count.assignments, demand.headcount),
|
||||
0,
|
||||
);
|
||||
const staffingPercent = totalDemandHeadcount > 0
|
||||
? Math.round((filledDemandHeadcount / totalDemandHeadcount) * 100)
|
||||
: 100;
|
||||
const staffingPercent =
|
||||
totalDemandHeadcount > 0
|
||||
? Math.round((filledDemandHeadcount / totalDemandHeadcount) * 100)
|
||||
: 100;
|
||||
|
||||
const totalCostCents = project.assignments.reduce((sum, assignment) => {
|
||||
const days = countBusinessDays(assignment.startDate, assignment.endDate);
|
||||
@@ -144,7 +148,8 @@ ${dataContext}`;
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "You are a project management analyst providing brief executive summaries. Be factual and action-oriented.",
|
||||
content:
|
||||
"You are a project management analyst providing brief executive summaries. Be factual and action-oriented.",
|
||||
},
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { SystemRole } from "@capakraken/db";
|
||||
import {
|
||||
PASSWORD_MAX_LENGTH,
|
||||
PASSWORD_MIN_LENGTH,
|
||||
PASSWORD_POLICY_MESSAGE,
|
||||
} from "@capakraken/shared";
|
||||
import { SystemRole } from "@nexus/db";
|
||||
import { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_POLICY_MESSAGE } from "@nexus/shared";
|
||||
import { createTRPCRouter, adminProcedure, publicProcedure } from "../trpc.js";
|
||||
import { getAppBaseUrl } from "../lib/app-base-url.js";
|
||||
import { sendEmail } from "../lib/email.js";
|
||||
@@ -16,7 +12,7 @@ const INVITE_TTL_MS = 72 * 60 * 60 * 1000; // 72 hours
|
||||
|
||||
function inviteEmailHtml(inviteUrl: string, role: SystemRole): string {
|
||||
return `
|
||||
<p>You have been invited to join CapaKraken as <strong>${role}</strong>.</p>
|
||||
<p>You have been invited to join Nexus as <strong>${role}</strong>.</p>
|
||||
<p>Click the link below to accept the invitation and set your password:</p>
|
||||
<p><a href="${inviteUrl}">${inviteUrl}</a></p>
|
||||
<p>This link expires in 72 hours and can only be used once.</p>
|
||||
@@ -61,8 +57,8 @@ export const inviteRouter = createTRPCRouter({
|
||||
|
||||
void sendEmail({
|
||||
to: input.email,
|
||||
subject: "You have been invited to CapaKraken",
|
||||
text: `You have been invited to join CapaKraken as ${input.role}.\n\nAccept your invitation: ${inviteUrl}\n\nThis link expires in 72 hours.`,
|
||||
subject: "You have been invited to Nexus",
|
||||
text: `You have been invited to join Nexus as ${input.role}.\n\nAccept your invitation: ${inviteUrl}\n\nThis link expires in 72 hours.`,
|
||||
html: inviteEmailHtml(inviteUrl, input.role),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CreateManagementLevelSchema,
|
||||
UpdateManagementLevelGroupSchema,
|
||||
UpdateManagementLevelSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
@@ -45,9 +45,7 @@ type ManagementLevelCreateInput = z.infer<typeof CreateManagementLevelSchema>;
|
||||
type ManagementLevelUpdateInput = z.infer<typeof managementLevelUpdateInputSchema>;
|
||||
type ManagementLevelIdInput = z.infer<typeof managementLevelIdInputSchema>;
|
||||
|
||||
export async function listManagementLevelGroups(
|
||||
ctx: ManagementLevelProcedureContext,
|
||||
) {
|
||||
export async function listManagementLevelGroups(ctx: ManagementLevelProcedureContext) {
|
||||
return ctx.db.managementLevelGroup.findMany({
|
||||
include: { levels: { orderBy: { name: "asc" } } },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Prisma, PrismaClient } from "@capakraken/db";
|
||||
import type { Prisma, PrismaClient } from "@nexus/db";
|
||||
import {
|
||||
CreateManagementLevelGroupSchema,
|
||||
CreateManagementLevelSchema,
|
||||
UpdateManagementLevelGroupSchema,
|
||||
UpdateManagementLevelSchema,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -88,9 +88,7 @@ export function buildManagementLevelUpdateData(
|
||||
};
|
||||
}
|
||||
|
||||
export function assertManagementLevelDeletable(
|
||||
level: ManagementLevelDeleteRecord,
|
||||
): void {
|
||||
export function assertManagementLevelDeletable(level: ManagementLevelDeleteRecord): void {
|
||||
if (level._count.resources > 0) {
|
||||
throw new TRPCError({
|
||||
code: "PRECONDITION_FAILED",
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
CreateManagementLevelGroupSchema,
|
||||
CreateManagementLevelSchema,
|
||||
} from "@capakraken/shared";
|
||||
import { CreateManagementLevelGroupSchema, CreateManagementLevelSchema } from "@nexus/shared";
|
||||
import { adminProcedure, createTRPCRouter, planningReadProcedure } from "../trpc.js";
|
||||
import {
|
||||
createManagementLevel,
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { PermissionKey, parseTaskAction, resolvePermissions } from "@capakraken/shared";
|
||||
import { PermissionKey, parseTaskAction, resolvePermissions } from "@nexus/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { createNotification } from "../lib/create-notification.js";
|
||||
import { getTaskAction } from "../lib/task-actions.js";
|
||||
import {
|
||||
emitTaskAssigned,
|
||||
emitTaskCompleted,
|
||||
emitTaskStatusChanged,
|
||||
} from "../sse/event-bus.js";
|
||||
import { emitTaskAssigned, emitTaskCompleted, emitTaskStatusChanged } from "../sse/event-bus.js";
|
||||
import {
|
||||
AssignTaskInputSchema,
|
||||
CreateTaskInputSchema,
|
||||
@@ -246,8 +242,8 @@ export async function executeNotificationTaskAction(
|
||||
|
||||
const currentUser = requireNotificationDbUser(ctx);
|
||||
const permissions = resolvePermissions(
|
||||
currentUser.systemRole as import("@capakraken/shared").SystemRole,
|
||||
currentUser.permissionOverrides as import("@capakraken/shared").PermissionOverrides | null,
|
||||
currentUser.systemRole as import("@nexus/shared").SystemRole,
|
||||
currentUser.permissionOverrides as import("@nexus/shared").PermissionOverrides | null,
|
||||
ctx.roleDefaults ?? undefined,
|
||||
);
|
||||
if (handler.permission && !permissions.has(handler.permission as PermissionKey)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateOrgUnitSchema, UpdateOrgUnitSchema } from "@capakraken/shared";
|
||||
import { CreateOrgUnitSchema, UpdateOrgUnitSchema } from "@nexus/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
@@ -34,15 +34,19 @@ function withAuditUser(userId: string | undefined) {
|
||||
return userId ? { userId } : {};
|
||||
}
|
||||
|
||||
export const orgUnitListInputSchema = z.object({
|
||||
level: z.number().int().min(5).max(7).optional(),
|
||||
parentId: z.string().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
}).optional();
|
||||
export const orgUnitListInputSchema = z
|
||||
.object({
|
||||
level: z.number().int().min(5).max(7).optional(),
|
||||
parentId: z.string().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const orgUnitTreeInputSchema = z.object({
|
||||
isActive: z.boolean().optional(),
|
||||
}).optional();
|
||||
export const orgUnitTreeInputSchema = z
|
||||
.object({
|
||||
isActive: z.boolean().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const orgUnitIdInputSchema = z.object({
|
||||
id: z.string(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user