refactor(api): extract timeline filter support
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
import type { TimelineEntriesFilters } from "./timeline-read-shared.js";
|
||||
|
||||
type TimelineSelfServiceContext = Pick<TRPCContext, "db" | "dbUser">;
|
||||
|
||||
function createUtcDate(year: number, month: number, day: number): Date {
|
||||
return new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
export function createTimelineDateRange(input: {
|
||||
startDate?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
durationDays?: number | undefined;
|
||||
}): { startDate: Date; endDate: Date } {
|
||||
const now = new Date();
|
||||
const startDate = input.startDate
|
||||
? new Date(`${input.startDate}T00:00:00.000Z`)
|
||||
: createUtcDate(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||
|
||||
if (Number.isNaN(startDate.getTime())) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Invalid startDate: ${input.startDate}`,
|
||||
});
|
||||
}
|
||||
|
||||
const endDate = input.endDate
|
||||
? new Date(`${input.endDate}T00:00:00.000Z`)
|
||||
: createUtcDate(
|
||||
startDate.getUTCFullYear(),
|
||||
startDate.getUTCMonth(),
|
||||
startDate.getUTCDate() + Math.max((input.durationDays ?? 21) - 1, 0),
|
||||
);
|
||||
|
||||
if (Number.isNaN(endDate.getTime())) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Invalid endDate: ${input.endDate}`,
|
||||
});
|
||||
}
|
||||
if (endDate < startDate) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "endDate must be on or after startDate.",
|
||||
});
|
||||
}
|
||||
|
||||
return { startDate, endDate };
|
||||
}
|
||||
|
||||
export function normalizeTimelineStringList(values?: string[] | undefined): string[] | undefined {
|
||||
const normalized = values
|
||||
?.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0);
|
||||
|
||||
return normalized && normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function createTimelineFilters(input: {
|
||||
resourceIds?: string[] | undefined;
|
||||
projectIds?: string[] | undefined;
|
||||
clientIds?: string[] | undefined;
|
||||
chapters?: string[] | undefined;
|
||||
eids?: string[] | undefined;
|
||||
countryCodes?: string[] | undefined;
|
||||
}): Omit<TimelineEntriesFilters, "startDate" | "endDate"> {
|
||||
return {
|
||||
resourceIds: normalizeTimelineStringList(input.resourceIds),
|
||||
projectIds: normalizeTimelineStringList(input.projectIds),
|
||||
clientIds: normalizeTimelineStringList(input.clientIds),
|
||||
chapters: normalizeTimelineStringList(input.chapters),
|
||||
eids: normalizeTimelineStringList(input.eids),
|
||||
countryCodes: normalizeTimelineStringList(input.countryCodes),
|
||||
};
|
||||
}
|
||||
|
||||
async function findOwnedTimelineResourceId(
|
||||
ctx: TimelineSelfServiceContext,
|
||||
): Promise<string | null> {
|
||||
if (!ctx.dbUser?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resource = await ctx.db.resource.findFirst({
|
||||
where: { userId: ctx.dbUser.id },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
return resource?.id ?? null;
|
||||
}
|
||||
|
||||
export async function buildSelfServiceTimelineInput(
|
||||
ctx: TimelineSelfServiceContext,
|
||||
input: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
projectIds?: string[] | undefined;
|
||||
clientIds?: string[] | undefined;
|
||||
},
|
||||
): Promise<TimelineEntriesFilters | null> {
|
||||
const ownedResourceId = await findOwnedTimelineResourceId(ctx);
|
||||
if (!ownedResourceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
resourceIds: [ownedResourceId],
|
||||
projectIds: normalizeTimelineStringList(input.projectIds),
|
||||
clientIds: normalizeTimelineStringList(input.clientIds),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTimelineEntriesDetailInput(input: {
|
||||
startDate?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
durationDays?: number | undefined;
|
||||
resourceIds?: string[] | undefined;
|
||||
projectIds?: string[] | undefined;
|
||||
clientIds?: string[] | undefined;
|
||||
chapters?: string[] | undefined;
|
||||
eids?: string[] | undefined;
|
||||
countryCodes?: string[] | undefined;
|
||||
}) {
|
||||
const period = createTimelineDateRange(input);
|
||||
const filters = createTimelineFilters(input);
|
||||
|
||||
return {
|
||||
period,
|
||||
filters,
|
||||
timelineInput: {
|
||||
...filters,
|
||||
startDate: period.startDate,
|
||||
endDate: period.endDate,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import { buildSplitAllocationReadModel } from "@capakraken/application";
|
||||
import type { PrismaClient } from "@capakraken/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
import { loadTimelineEntryRecords } from "./timeline-entry-query-support.js";
|
||||
export {
|
||||
buildSelfServiceTimelineInput,
|
||||
buildTimelineEntriesDetailInput,
|
||||
createTimelineDateRange,
|
||||
createTimelineFilters,
|
||||
} from "./timeline-filter-support.js";
|
||||
|
||||
export type ShiftDbClient = Pick<
|
||||
PrismaClient,
|
||||
@@ -39,7 +43,6 @@ export const TimelineWindowFiltersSchema = z.object({
|
||||
});
|
||||
|
||||
type TimelineWindowFiltersInput = z.infer<typeof TimelineWindowFiltersSchema>;
|
||||
type TimelineSelfServiceContext = Pick<TRPCContext, "db" | "dbUser">;
|
||||
type TimelineAnonymizationDirectory = Awaited<ReturnType<typeof getAnonymizationDirectory>>;
|
||||
|
||||
export function getAssignmentResourceIds(
|
||||
@@ -61,77 +64,6 @@ export function fmtDate(value: Date | null | undefined): string | null {
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function createUtcDate(year: number, month: number, day: number): Date {
|
||||
return new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
export function createTimelineDateRange(input: {
|
||||
startDate?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
durationDays?: number | undefined;
|
||||
}): { startDate: Date; endDate: Date } {
|
||||
const now = new Date();
|
||||
const startDate = input.startDate
|
||||
? new Date(`${input.startDate}T00:00:00.000Z`)
|
||||
: createUtcDate(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||
|
||||
if (Number.isNaN(startDate.getTime())) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Invalid startDate: ${input.startDate}`,
|
||||
});
|
||||
}
|
||||
|
||||
const endDate = input.endDate
|
||||
? new Date(`${input.endDate}T00:00:00.000Z`)
|
||||
: createUtcDate(
|
||||
startDate.getUTCFullYear(),
|
||||
startDate.getUTCMonth(),
|
||||
startDate.getUTCDate() + Math.max((input.durationDays ?? 21) - 1, 0),
|
||||
);
|
||||
|
||||
if (Number.isNaN(endDate.getTime())) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Invalid endDate: ${input.endDate}`,
|
||||
});
|
||||
}
|
||||
if (endDate < startDate) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "endDate must be on or after startDate.",
|
||||
});
|
||||
}
|
||||
|
||||
return { startDate, endDate };
|
||||
}
|
||||
|
||||
function normalizeStringList(values?: string[] | undefined): string[] | undefined {
|
||||
const normalized = values
|
||||
?.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0);
|
||||
|
||||
return normalized && normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function createTimelineFilters(input: {
|
||||
resourceIds?: string[] | undefined;
|
||||
projectIds?: string[] | undefined;
|
||||
clientIds?: string[] | undefined;
|
||||
chapters?: string[] | undefined;
|
||||
eids?: string[] | undefined;
|
||||
countryCodes?: string[] | undefined;
|
||||
}): Omit<TimelineEntriesFilters, "startDate" | "endDate"> {
|
||||
return {
|
||||
resourceIds: normalizeStringList(input.resourceIds),
|
||||
projectIds: normalizeStringList(input.projectIds),
|
||||
clientIds: normalizeStringList(input.clientIds),
|
||||
chapters: normalizeStringList(input.chapters),
|
||||
eids: normalizeStringList(input.eids),
|
||||
countryCodes: normalizeStringList(input.countryCodes),
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyTimelineEntriesView() {
|
||||
return buildSplitAllocationReadModel({
|
||||
demandRequirements: [],
|
||||
@@ -155,68 +87,6 @@ export function buildTimelineEntriesViewResponse<
|
||||
};
|
||||
}
|
||||
|
||||
async function findOwnedTimelineResourceId(
|
||||
ctx: TimelineSelfServiceContext,
|
||||
): Promise<string | null> {
|
||||
if (!ctx.dbUser?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resource = await ctx.db.resource.findFirst({
|
||||
where: { userId: ctx.dbUser.id },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
return resource?.id ?? null;
|
||||
}
|
||||
|
||||
export async function buildSelfServiceTimelineInput(
|
||||
ctx: TimelineSelfServiceContext,
|
||||
input: TimelineWindowFiltersInput,
|
||||
): Promise<TimelineEntriesFilters | null> {
|
||||
const ownedResourceId = await findOwnedTimelineResourceId(ctx);
|
||||
if (!ownedResourceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
resourceIds: [ownedResourceId],
|
||||
projectIds: normalizeStringList(input.projectIds),
|
||||
clientIds: normalizeStringList(input.clientIds),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTimelineEntriesDetailInput(input: {
|
||||
startDate?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
durationDays?: number | undefined;
|
||||
resourceIds?: string[] | undefined;
|
||||
projectIds?: string[] | undefined;
|
||||
clientIds?: string[] | undefined;
|
||||
chapters?: string[] | undefined;
|
||||
eids?: string[] | undefined;
|
||||
countryCodes?: string[] | undefined;
|
||||
}) {
|
||||
const period = createTimelineDateRange(input);
|
||||
const filters = createTimelineFilters(input);
|
||||
|
||||
return {
|
||||
period,
|
||||
filters,
|
||||
timelineInput: {
|
||||
...filters,
|
||||
startDate: period.startDate,
|
||||
endDate: period.endDate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function summarizeTimelineEntries(readModel: {
|
||||
allocations: Array<{ projectId: string | null; resourceId: string | null }>;
|
||||
demands: Array<{ projectId: string | null }>;
|
||||
|
||||
Reference in New Issue
Block a user