refactor(api): extract timeline batch shift support

This commit is contained in:
2026-03-31 17:57:28 +02:00
parent e082f1748b
commit fb09d6487f
5 changed files with 110 additions and 105 deletions
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import {
buildTimelineBatchShiftAuditChanges,
shiftTimelineAllocationWindow,
} from "../router/timeline-allocation-batch-shift-support.js";
describe("timeline allocation batch shift support", () => {
it("shifts and clamps allocation windows for each batch-shift mode", () => {
expect(
shiftTimelineAllocationWindow({
startDate: new Date("2026-04-10T00:00:00.000Z"),
endDate: new Date("2026-04-12T00:00:00.000Z"),
daysDelta: 2,
mode: "move",
}),
).toEqual({
startDate: new Date("2026-04-12T00:00:00.000Z"),
endDate: new Date("2026-04-14T00:00:00.000Z"),
});
expect(
shiftTimelineAllocationWindow({
startDate: new Date("2026-04-10T00:00:00.000Z"),
endDate: new Date("2026-04-12T00:00:00.000Z"),
daysDelta: 5,
mode: "resize-start",
}),
).toEqual({
startDate: new Date("2026-04-12T00:00:00.000Z"),
endDate: new Date("2026-04-12T00:00:00.000Z"),
});
expect(
shiftTimelineAllocationWindow({
startDate: new Date("2026-04-10T00:00:00.000Z"),
endDate: new Date("2026-04-12T00:00:00.000Z"),
daysDelta: -5,
mode: "resize-end",
}),
).toEqual({
startDate: new Date("2026-04-10T00:00:00.000Z"),
endDate: new Date("2026-04-10T00:00:00.000Z"),
});
});
it("builds batch-shift audit payloads", () => {
expect(
buildTimelineBatchShiftAuditChanges({
mode: "move",
daysDelta: 3,
count: 2,
}),
).toEqual({
operation: "batchShift",
mode: "move",
daysDelta: 3,
count: 2,
});
});
});
@@ -4,8 +4,6 @@ import {
assertTimelineDateRangeValid,
buildTimelineAllocationEntryUpdate,
buildTimelineAllocationMetadata,
buildTimelineBatchShiftAuditChanges,
shiftTimelineAllocationWindow,
validateTimelineAllocationDateRanges,
} from "../router/timeline-allocation-mutation-support.js";
@@ -36,59 +34,6 @@ describe("timeline allocation mutation support", () => {
)).toThrowError(TRPCError);
});
it("shifts and clamps allocation windows for each batch-shift mode", () => {
expect(
shiftTimelineAllocationWindow({
startDate: new Date("2026-04-10T00:00:00.000Z"),
endDate: new Date("2026-04-12T00:00:00.000Z"),
daysDelta: 2,
mode: "move",
}),
).toEqual({
startDate: new Date("2026-04-12T00:00:00.000Z"),
endDate: new Date("2026-04-14T00:00:00.000Z"),
});
expect(
shiftTimelineAllocationWindow({
startDate: new Date("2026-04-10T00:00:00.000Z"),
endDate: new Date("2026-04-12T00:00:00.000Z"),
daysDelta: 5,
mode: "resize-start",
}),
).toEqual({
startDate: new Date("2026-04-12T00:00:00.000Z"),
endDate: new Date("2026-04-12T00:00:00.000Z"),
});
expect(
shiftTimelineAllocationWindow({
startDate: new Date("2026-04-10T00:00:00.000Z"),
endDate: new Date("2026-04-12T00:00:00.000Z"),
daysDelta: -5,
mode: "resize-end",
}),
).toEqual({
startDate: new Date("2026-04-10T00:00:00.000Z"),
endDate: new Date("2026-04-10T00:00:00.000Z"),
});
});
it("builds batch-shift audit payloads", () => {
expect(
buildTimelineBatchShiftAuditChanges({
mode: "move",
daysDelta: 3,
count: 2,
}),
).toEqual({
operation: "batchShift",
mode: "move",
daysDelta: 3,
count: 2,
});
});
it("validates date ranges in bulk", () => {
expect(() =>
validateTimelineAllocationDateRanges([
@@ -0,0 +1,47 @@
import { Prisma } from "@capakraken/db";
export type TimelineBatchShiftMode = "move" | "resize-start" | "resize-end";
export function shiftTimelineAllocationWindow(input: {
startDate: Date;
endDate: Date;
daysDelta: number;
mode: TimelineBatchShiftMode;
}): { startDate: Date; endDate: Date } {
const startDate = new Date(input.startDate);
const endDate = new Date(input.endDate);
if (input.mode === "move") {
startDate.setDate(startDate.getDate() + input.daysDelta);
endDate.setDate(endDate.getDate() + input.daysDelta);
return { startDate, endDate };
}
if (input.mode === "resize-start") {
startDate.setDate(startDate.getDate() + input.daysDelta);
if (startDate > endDate) {
startDate.setTime(endDate.getTime());
}
return { startDate, endDate };
}
endDate.setDate(endDate.getDate() + input.daysDelta);
if (endDate < startDate) {
endDate.setTime(startDate.getTime());
}
return { startDate, endDate };
}
export function buildTimelineBatchShiftAuditChanges(input: {
mode: TimelineBatchShiftMode;
daysDelta: number;
count: number;
}): Prisma.InputJsonValue {
return {
operation: "batchShift",
mode: input.mode,
daysDelta: input.daysDelta,
count: input.count,
} as unknown as Prisma.InputJsonValue;
}
@@ -1,8 +1,5 @@
import { Prisma } from "@capakraken/db";
import { TRPCError } from "@trpc/server";
export type TimelineBatchShiftMode = "move" | "resize-start" | "resize-end";
export function assertTimelineDateRangeValid(startDate: Date, endDate: Date): void {
if (endDate >= startDate) {
return;
@@ -39,37 +36,6 @@ export function validateTimelineAllocationDateRanges(
}
}
export function shiftTimelineAllocationWindow(input: {
startDate: Date;
endDate: Date;
daysDelta: number;
mode: TimelineBatchShiftMode;
}): { startDate: Date; endDate: Date } {
const startDate = new Date(input.startDate);
const endDate = new Date(input.endDate);
if (input.mode === "move") {
startDate.setDate(startDate.getDate() + input.daysDelta);
endDate.setDate(endDate.getDate() + input.daysDelta);
return { startDate, endDate };
}
if (input.mode === "resize-start") {
startDate.setDate(startDate.getDate() + input.daysDelta);
if (startDate > endDate) {
startDate.setTime(endDate.getTime());
}
return { startDate, endDate };
}
endDate.setDate(endDate.getDate() + input.daysDelta);
if (endDate < startDate) {
endDate.setTime(startDate.getTime());
}
return { startDate, endDate };
}
export function buildTimelineAllocationUpdateAuditChanges(input: {
allocationId: string;
previousHoursPerDay: number;
@@ -80,7 +46,7 @@ export function buildTimelineAllocationUpdateAuditChanges(input: {
nextStartDate: Date;
nextEndDate: Date;
includeSaturday: boolean;
}): Prisma.InputJsonValue {
}) {
return {
before: {
id: input.allocationId,
@@ -95,7 +61,7 @@ export function buildTimelineAllocationUpdateAuditChanges(input: {
endDate: input.nextEndDate,
includeSaturday: input.includeSaturday,
},
} as unknown as Prisma.InputJsonValue;
} as const;
}
export function buildTimelineAllocationEntryUpdate(input: {
@@ -124,16 +90,3 @@ export function buildTimelineAllocationEntryUpdate(input: {
},
};
}
export function buildTimelineBatchShiftAuditChanges(input: {
mode: TimelineBatchShiftMode;
daysDelta: number;
count: number;
}): Prisma.InputJsonValue {
return {
operation: "batchShift",
mode: input.mode,
daysDelta: input.daysDelta,
count: input.count,
} as unknown as Prisma.InputJsonValue;
}
@@ -5,7 +5,7 @@ import {
buildTimelineBatchShiftAuditChanges,
shiftTimelineAllocationWindow,
type TimelineBatchShiftMode,
} from "./timeline-allocation-mutation-support.js";
} from "./timeline-allocation-batch-shift-support.js";
export async function applyTimelineBatchAllocationShift(input: {
db: PrismaClient;