refactor: consolidate duplicated code across web and API packages

- Extract shared render helpers (vacation blocks, range overlay, overbooking blink) into renderHelpers.tsx
- Centralize status badge styles and vacation color maps into status-styles.ts
- Extract dragMath.ts utility from useTimelineDrag for reuse
- Split useInvalidatePlanningViews into useInvalidateTimeline (4 queries) + useInvalidatePlanningViews (8 queries)
- Adopt findUniqueOrThrow() and Prisma select constants across API routers
- Add shared fmtEur() helper for API-side money formatting
- Wrap TimelineResourcePanel and TimelineProjectPanel with React.memo
- Fix pre-existing TS2589 deep type errors in TeamCalendar and VacationModal
- 38 files changed, reducing ~400 lines of duplicated code

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-19 00:10:08 +01:00
parent ddec3a927a
commit e7b74f13bd
38 changed files with 637 additions and 652 deletions
+1 -4
View File
@@ -9,6 +9,7 @@ import type { PermissionKey } from "@planarchy/shared";
import { parseTaskAction } from "@planarchy/shared";
import { createDalleClient, isDalleConfigured, parseAiError } from "../ai-client.js";
import { getTaskAction } from "../lib/task-actions.js";
import { fmtEur } from "../lib/format-utils.js";
import { resolveRecipients } from "../lib/notification-targeting.js";
import {
emitNotificationCreated,
@@ -41,10 +42,6 @@ type ToolExecutor = (params: any, ctx: ToolContext) => Promise<unknown>;
// ─── Helpers ────────────────────────────────────────────────────────────────
function fmtEur(cents: number): string {
return `${(cents / 100).toLocaleString("de-DE", { minimumFractionDigits: 2 })} EUR`;
}
function fmtDate(d: Date | null | undefined): string | null {
return d ? d.toISOString().slice(0, 10) : null;
}
+18 -18
View File
@@ -2,29 +2,29 @@ import {
CreateCalculationRuleSchema,
UpdateCalculationRuleSchema,
} from "@planarchy/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { PROJECT_BRIEF_SELECT } from "../db/selects.js";
import { createTRPCRouter, controllerProcedure, managerProcedure } from "../trpc.js";
export const calculationRuleRouter = createTRPCRouter({
list: controllerProcedure.query(async ({ ctx }) => {
return ctx.db.calculationRule.findMany({
orderBy: [{ priority: "desc" }, { name: "asc" }],
include: { project: { select: { id: true, name: true, shortCode: true } } },
include: { project: { select: PROJECT_BRIEF_SELECT } },
});
}),
getById: controllerProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const rule = await ctx.db.calculationRule.findUnique({
where: { id: input.id },
include: { project: { select: { id: true, name: true, shortCode: true } } },
});
if (!rule) {
throw new TRPCError({ code: "NOT_FOUND", message: "Calculation rule not found" });
}
return rule;
return findUniqueOrThrow(
ctx.db.calculationRule.findUnique({
where: { id: input.id },
include: { project: { select: PROJECT_BRIEF_SELECT } },
}),
"CalculationRule",
);
}),
/** Get all active rules (optimized for engine use — no project include) */
@@ -58,10 +58,10 @@ export const calculationRuleRouter = createTRPCRouter({
.input(UpdateCalculationRuleSchema)
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input;
const existing = await ctx.db.calculationRule.findUnique({ where: { id } });
if (!existing) {
throw new TRPCError({ code: "NOT_FOUND", message: "Calculation rule not found" });
}
await findUniqueOrThrow(
ctx.db.calculationRule.findUnique({ where: { id } }),
"CalculationRule",
);
// Build update data using exactOptionalPropertyTypes pattern
const updateData: Record<string, unknown> = {};
@@ -85,10 +85,10 @@ export const calculationRuleRouter = createTRPCRouter({
delete: managerProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const existing = await ctx.db.calculationRule.findUnique({ where: { id: input.id } });
if (!existing) {
throw new TRPCError({ code: "NOT_FOUND", message: "Calculation rule not found" });
}
await findUniqueOrThrow(
ctx.db.calculationRule.findUnique({ where: { id: input.id } }),
"CalculationRule",
);
await ctx.db.calculationRule.delete({ where: { id: input.id } });
return { success: true };
}),
+1 -4
View File
@@ -12,6 +12,7 @@ import type { CalculationRule, AbsenceDay, SpainScheduleRule, WeekdayAvailabilit
import { VacationStatus } from "@planarchy/db";
import { z } from "zod";
import { createTRPCRouter, controllerProcedure } from "../trpc.js";
import { fmtEur } from "../lib/format-utils.js";
// ─── Graph Types (mirrored from client for API response) ────────────────────
@@ -50,10 +51,6 @@ function l(source: string, target: string, formula: string, weight = 1): GraphLi
return { source, target, formula, weight };
}
function fmtEur(cents: number): string {
return `${(cents / 100).toLocaleString("de-DE", { minimumFractionDigits: 2 })} EUR`;
}
function fmtPct(ratio: number): string {
return `${(ratio * 100).toFixed(1)}%`;
}
+2 -1
View File
@@ -6,6 +6,7 @@
import { VacationType, VacationStatus } from "@planarchy/db";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { RESOURCE_BRIEF_SELECT } from "../db/selects.js";
import { createTRPCRouter, adminProcedure, managerProcedure, protectedProcedure } from "../trpc.js";
/** Types that consume from annual leave balance */
@@ -266,7 +267,7 @@ export const entitlementRouter = createTRPCRouter({
isActive: true,
...(input.chapter ? { chapter: input.chapter } : {}),
},
select: { id: true, displayName: true, eid: true, chapter: true },
select: { ...RESOURCE_BRIEF_SELECT, chapter: true },
orderBy: [{ chapter: "asc" }, { displayName: "asc" }],
});
+5 -7
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { findUniqueOrThrow } from "../db/helpers.js";
import { createTRPCRouter, managerProcedure, protectedProcedure } from "../trpc.js";
import {
emitNotificationCreated,
@@ -598,13 +599,10 @@ export const notificationRouter = createTRPCRouter({
assignTask: managerProcedure
.input(z.object({ id: z.string(), assigneeId: z.string() }))
.mutation(async ({ ctx, input }) => {
const existing = await ctx.db.notification.findUnique({
where: { id: input.id },
});
if (!existing) {
throw new TRPCError({ code: "NOT_FOUND", message: "Task not found" });
}
const existing = await findUniqueOrThrow(
ctx.db.notification.findUnique({ where: { id: input.id } }),
"Task",
);
if (existing.category !== "TASK" && existing.category !== "APPROVAL") {
throw new TRPCError({
@@ -1,6 +1,7 @@
import { buildSplitAllocationReadModel } from "@planarchy/application";
import type { PrismaClient } from "@planarchy/db";
import { AllocationStatus } from "@planarchy/shared";
import { ROLE_BRIEF_SELECT } from "../db/selects.js";
export const PROJECT_PLANNING_ALLOCATION_INCLUDE = {
resource: {
@@ -31,7 +32,7 @@ export const PROJECT_PLANNING_ALLOCATION_INCLUDE = {
},
},
roleEntity: {
select: { id: true, name: true, color: true },
select: ROLE_BRIEF_SELECT,
},
} as const;
+39 -37
View File
@@ -295,30 +295,30 @@ export const resourceRouter = createTRPCRouter({
getHoverCard: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const resource = await ctx.db.resource.findUnique({
where: { id: input.id },
select: {
id: true,
displayName: true,
eid: true,
email: true,
chapter: true,
lcrCents: true,
ucrCents: true,
currency: true,
chargeabilityTarget: true,
skills: true,
availability: true,
isActive: true,
areaRole: { select: { id: true, name: true, color: true } },
country: { select: { name: true, code: true } },
managementLevel: { select: { name: true } },
resourceType: true,
},
});
if (!resource) {
throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
}
const resource = await findUniqueOrThrow(
ctx.db.resource.findUnique({
where: { id: input.id },
select: {
id: true,
displayName: true,
eid: true,
email: true,
chapter: true,
lcrCents: true,
ucrCents: true,
currency: true,
chargeabilityTarget: true,
skills: true,
availability: true,
isActive: true,
areaRole: { select: ROLE_BRIEF_SELECT },
country: { select: { name: true, code: true } },
managementLevel: { select: { name: true } },
resourceType: true,
},
}),
"Resource",
);
const directory = await getAnonymizationDirectory(ctx.db);
const anon = anonymizeResource(resource, directory);
return {
@@ -633,11 +633,14 @@ export const resourceRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
// Find the resource linked to this user
const user = await ctx.db.user.findUnique({
where: { email: ctx.session.user?.email ?? "" },
include: { resource: true },
});
if (!user?.resource) {
const user = await findUniqueOrThrow(
ctx.db.user.findUnique({
where: { email: ctx.session.user?.email ?? "" },
include: { resource: true },
}),
"User",
);
if (!user.resource) {
throw new TRPCError({ code: "NOT_FOUND", message: "No resource linked to your account" });
}
const resourceId = user.resource.id;
@@ -748,17 +751,16 @@ export const resourceRouter = createTRPCRouter({
.input(z.object({ resourceId: z.string() }))
.mutation(async ({ ctx, input }) => {
const [resource, settings] = await Promise.all([
ctx.db.resource.findUnique({
where: { id: input.resourceId },
include: { areaRole: { select: { name: true } } },
}),
findUniqueOrThrow(
ctx.db.resource.findUnique({
where: { id: input.resourceId },
include: { areaRole: { select: { name: true } } },
}),
"Resource",
),
ctx.db.systemSettings.findUnique({ where: { id: "singleton" } }),
]);
if (!resource) {
throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
}
if (!isAiConfigured(settings)) {
throw new TRPCError({
code: "PRECONDITION_FAILED",
+2 -1
View File
@@ -3,6 +3,7 @@ import { CreateRoleSchema, PermissionKey, UpdateRoleSchema } from "@planarchy/sh
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { RESOURCE_BRIEF_SELECT } from "../db/selects.js";
import { emitRoleCreated, emitRoleDeleted, emitRoleUpdated } from "../sse/event-bus.js";
import { createTRPCRouter, managerProcedure, protectedProcedure, requirePermission } from "../trpc.js";
@@ -89,7 +90,7 @@ export const roleRouter = createTRPCRouter({
_count: { select: { resourceRoles: true } },
resourceRoles: {
include: {
resource: { select: { id: true, displayName: true, eid: true } },
resource: { select: RESOURCE_BRIEF_SELECT },
},
},
},
+31 -33
View File
@@ -143,23 +143,22 @@ async function loadTimelineEntriesReadModel(
async function loadProjectShiftContext(db: ShiftDbClient, projectId: string) {
const [project, planningRead] = await Promise.all([
db.project.findUnique({
where: { id: projectId },
select: {
id: true,
budgetCents: true,
winProbability: true,
startDate: true,
endDate: true,
},
}),
findUniqueOrThrow(
db.project.findUnique({
where: { id: projectId },
select: {
id: true,
budgetCents: true,
winProbability: true,
startDate: true,
endDate: true,
},
}),
"Project",
),
loadProjectPlanningReadModel(db, { projectId, activeOnly: true }),
]);
if (!project) {
throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
}
const { demandRequirements, assignments, readModel: projectReadModel } = planningRead;
const resourceIds = getAssignmentResourceIds(projectReadModel);
@@ -337,31 +336,30 @@ export const timelineRouter = createTRPCRouter({
.input(z.object({ projectId: z.string() }))
.query(async ({ ctx, input }) => {
const [project, planningRead] = await Promise.all([
ctx.db.project.findUnique({
where: { id: input.projectId },
select: {
id: true,
name: true,
shortCode: true,
orderType: true,
budgetCents: true,
winProbability: true,
status: true,
startDate: true,
endDate: true,
staffingReqs: true,
},
}),
findUniqueOrThrow(
ctx.db.project.findUnique({
where: { id: input.projectId },
select: {
id: true,
name: true,
shortCode: true,
orderType: true,
budgetCents: true,
winProbability: true,
status: true,
startDate: true,
endDate: true,
staffingReqs: true,
},
}),
"Project",
),
loadProjectPlanningReadModel(ctx.db, {
projectId: input.projectId,
activeOnly: true,
}),
]);
if (!project) {
throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
}
const resourceIds = getAssignmentResourceIds(planningRead.readModel);
const allResourceAllocations =
resourceIds.length === 0
+6 -5
View File
@@ -3,6 +3,7 @@ import { VacationStatus, VacationType } from "@planarchy/db";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { RESOURCE_BRIEF_SELECT } from "../db/selects.js";
import { emitVacationCreated, emitVacationUpdated, emitNotificationCreated, emitTaskAssigned } from "../sse/event-bus.js";
import { createTRPCRouter, adminProcedure, managerProcedure, protectedProcedure } from "../trpc.js";
import { sendEmail } from "../lib/email.js";
@@ -99,7 +100,7 @@ export const vacationRouter = createTRPCRouter({
...(input.endDate ? { startDate: { lte: input.endDate } } : {}),
},
include: {
resource: { select: { id: true, displayName: true, eid: true } },
resource: { select: RESOURCE_BRIEF_SELECT },
requestedBy: { select: { id: true, name: true, email: true } },
approvedBy: { select: { id: true, name: true, email: true } },
},
@@ -120,7 +121,7 @@ export const vacationRouter = createTRPCRouter({
ctx.db.vacation.findUnique({
where: { id: input.id },
include: {
resource: { select: { id: true, displayName: true, eid: true } },
resource: { select: RESOURCE_BRIEF_SELECT },
requestedBy: { select: { id: true, name: true, email: true } },
approvedBy: { select: { id: true, name: true, email: true } },
},
@@ -210,7 +211,7 @@ export const vacationRouter = createTRPCRouter({
: {}),
},
include: {
resource: { select: { id: true, displayName: true, eid: true } },
resource: { select: RESOURCE_BRIEF_SELECT },
requestedBy: { select: { id: true, name: true, email: true } },
},
});
@@ -539,7 +540,7 @@ export const vacationRouter = createTRPCRouter({
return ctx.db.vacation.findMany({
where: { status: VacationStatus.PENDING },
include: {
resource: { select: { id: true, displayName: true, eid: true } },
resource: { select: RESOURCE_BRIEF_SELECT },
requestedBy: { select: { id: true, name: true, email: true } },
},
orderBy: { startDate: "asc" },
@@ -576,7 +577,7 @@ export const vacationRouter = createTRPCRouter({
endDate: { gte: input.startDate },
},
include: {
resource: { select: { id: true, displayName: true, eid: true } },
resource: { select: RESOURCE_BRIEF_SELECT },
},
orderBy: { startDate: "asc" },
take: 20,