feat: timeline UI overhaul with project/resource panel redesign, quick filters, and API improvements

Redesigned timeline project and resource panels with expanded detail views,
added quick filter toolbar, improved drag handling, and enhanced vacation/entitlement
router logic. Includes e2e test updates and minor API fixes.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-15 09:28:59 +01:00
parent fa2019f521
commit a83edb2f9d
23 changed files with 2464 additions and 734 deletions
+33 -4
View File
@@ -27,6 +27,7 @@ import {
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js";
import { emitAllocationCreated, emitAllocationDeleted, emitAllocationUpdated } from "../sse/event-bus.js";
import { createTRPCRouter, managerProcedure, protectedProcedure, requirePermission } from "../trpc.js";
import { PROJECT_BRIEF_SELECT, RESOURCE_BRIEF_SELECT, ROLE_BRIEF_SELECT } from "../db/selects.js";
@@ -102,7 +103,7 @@ function toAssignmentUpdateInput(input: AllocationEntryUpdateInput) {
}
async function loadAllocationReadModel(
db: Pick<import("@planarchy/db").PrismaClient, "demandRequirement" | "assignment">,
db: Pick<import("@planarchy/db").PrismaClient, "demandRequirement" | "assignment" | "systemSettings" | "resource">,
input: AllocationListFilters,
) {
const [demandRequirements, assignments] = await Promise.all([
@@ -127,7 +128,22 @@ async function loadAllocationReadModel(
}),
]);
return buildSplitAllocationReadModel({ demandRequirements, assignments });
const readModel = buildSplitAllocationReadModel({ demandRequirements, assignments });
const directory = await getAnonymizationDirectory(db as import("@planarchy/db").PrismaClient);
if (!directory) return readModel;
function anonymizeAllocation<T extends { resource?: { id: string; eid?: string | null; displayName?: string | null; email?: string | null } | null }>(alloc: T): T {
if (!alloc.resource) return alloc;
return { ...alloc, resource: anonymizeResource(alloc.resource, directory) };
}
return {
...readModel,
allocations: readModel.allocations.map(anonymizeAllocation),
demands: readModel.demands.map(anonymizeAllocation),
assignments: readModel.assignments.map(anonymizeAllocation),
};
}
async function findAllocationEntryOrNull(
@@ -236,7 +252,7 @@ export const allocationRouter = createTRPCRouter({
}),
)
.query(async ({ ctx, input }) => {
return ctx.db.demandRequirement.findMany({
const demands = await ctx.db.demandRequirement.findMany({
where: {
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.status ? { status: input.status } : {}),
@@ -245,6 +261,14 @@ export const allocationRouter = createTRPCRouter({
include: DEMAND_INCLUDE,
orderBy: { startDate: "asc" },
});
const dir = await getAnonymizationDirectory(ctx.db);
if (!dir) return demands;
return demands.map((d) => ({
...d,
assignments: d.assignments.map((a) =>
a.resource ? { ...a, resource: anonymizeResource(a.resource, dir) } : a,
),
}));
}),
listAssignments: protectedProcedure
@@ -257,7 +281,7 @@ export const allocationRouter = createTRPCRouter({
}),
)
.query(async ({ ctx, input }) => {
return ctx.db.assignment.findMany({
const assignments = await ctx.db.assignment.findMany({
where: {
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.resourceId ? { resourceId: input.resourceId } : {}),
@@ -267,6 +291,11 @@ export const allocationRouter = createTRPCRouter({
include: ASSIGNMENT_INCLUDE,
orderBy: { startDate: "asc" },
});
const dir = await getAnonymizationDirectory(ctx.db);
if (!dir) return assignments;
return assignments.map((a) =>
a.resource ? { ...a, resource: anonymizeResource(a.resource, dir) } : a,
);
}),
createDemandRequirement: managerProcedure