feat(plan): resolveGrid for batch cell resolution
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
||||||
import prisma from "../config/database";
|
import prisma from "../config/database";
|
||||||
import { resolveCell } from "../services/plan.service";
|
import { resolveCell, resolveGrid } from "../services/plan.service";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Test fixtures
|
// Test fixtures
|
||||||
@@ -119,3 +119,54 @@ describe("plan.service.resolveCell", () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// resolveGrid
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.resolveGrid", () => {
|
||||||
|
it("returns a 2D map keyed by userId then date", async () => {
|
||||||
|
await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: new Date("2099-06-01"),
|
||||||
|
date_to: new Date("2099-06-03"),
|
||||||
|
category: "work",
|
||||||
|
note: `${N}A`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const cells = await resolveGrid([adminUserId], "2099-06-01", "2099-06-05");
|
||||||
|
expect(cells[adminUserId]["2099-06-01"]?.note).toBe(`${N}A`);
|
||||||
|
expect(cells[adminUserId]["2099-06-02"]?.note).toBe(`${N}A`);
|
||||||
|
expect(cells[adminUserId]["2099-06-03"]?.note).toBe(`${N}A`);
|
||||||
|
expect(cells[adminUserId]["2099-06-04"]).toBeNull();
|
||||||
|
expect(cells[adminUserId]["2099-06-05"]).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies override on a covered day", async () => {
|
||||||
|
await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: new Date("2099-06-01"),
|
||||||
|
date_to: new Date("2099-06-03"),
|
||||||
|
category: "work",
|
||||||
|
note: `${N}A`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.work_plan_overrides.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: new Date("2099-06-02"),
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}B`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const cells = await resolveGrid([adminUserId], "2099-06-01", "2099-06-03");
|
||||||
|
expect(cells[adminUserId]["2099-06-01"]?.note).toBe(`${N}A`);
|
||||||
|
expect(cells[adminUserId]["2099-06-02"]?.note).toBe(`${N}B`);
|
||||||
|
expect(cells[adminUserId]["2099-06-03"]?.note).toBe(`${N}A`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -96,3 +96,102 @@ export async function resolveCell(
|
|||||||
rangeTo: entry.date_to.toISOString().slice(0, 10),
|
rangeTo: entry.date_to.toISOString().slice(0, 10),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the effective plan cell for every (userId, date) in the
|
||||||
|
* given range. Returns a 2D map: cells[userId][dateStr] = ResolvedCell | null.
|
||||||
|
*
|
||||||
|
* Implementation: load all entries and overrides for the range in two
|
||||||
|
* queries, then iterate the dates and resolve each cell in O(1) per
|
||||||
|
* (user, date). This avoids N+1 calls to resolveCell.
|
||||||
|
*/
|
||||||
|
export async function resolveGrid(
|
||||||
|
userIds: number[],
|
||||||
|
dateFromStr: string,
|
||||||
|
dateToStr: string,
|
||||||
|
): Promise<Record<number, Record<string, ResolvedCell | null>>> {
|
||||||
|
const dateFrom = new Date(dateFromStr);
|
||||||
|
const dateTo = new Date(dateToStr);
|
||||||
|
|
||||||
|
const [entries, overrides] = await Promise.all([
|
||||||
|
prisma.work_plan_entries.findMany({
|
||||||
|
where: {
|
||||||
|
user_id: { in: userIds },
|
||||||
|
is_deleted: false,
|
||||||
|
date_from: { lte: dateTo },
|
||||||
|
date_to: { gte: dateFrom },
|
||||||
|
},
|
||||||
|
orderBy: { created_at: "desc" },
|
||||||
|
}),
|
||||||
|
prisma.work_plan_overrides.findMany({
|
||||||
|
where: {
|
||||||
|
user_id: { in: userIds },
|
||||||
|
is_deleted: false,
|
||||||
|
shift_date: { gte: dateFrom, lte: dateTo },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Build a date list (inclusive)
|
||||||
|
const dates: string[] = [];
|
||||||
|
for (
|
||||||
|
let d = new Date(dateFrom);
|
||||||
|
d <= dateTo;
|
||||||
|
d.setUTCDate(d.getUTCDate() + 1)
|
||||||
|
) {
|
||||||
|
dates.push(d.toISOString().slice(0, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<number, Record<string, ResolvedCell | null>> = {};
|
||||||
|
for (const uid of userIds) {
|
||||||
|
result[uid] = {};
|
||||||
|
for (const dateStr of dates) {
|
||||||
|
// Override first
|
||||||
|
const override = overrides.find(
|
||||||
|
(o) =>
|
||||||
|
o.user_id === uid &&
|
||||||
|
o.shift_date.toISOString().slice(0, 10) === dateStr,
|
||||||
|
);
|
||||||
|
if (override) {
|
||||||
|
result[uid][dateStr] = {
|
||||||
|
source: "override",
|
||||||
|
entryId: null,
|
||||||
|
overrideId: override.id,
|
||||||
|
user_id: override.user_id,
|
||||||
|
shift_date: dateStr,
|
||||||
|
project_id: override.project_id,
|
||||||
|
category: override.category,
|
||||||
|
note: override.note,
|
||||||
|
rangeFrom: null,
|
||||||
|
rangeTo: null,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// First (latest by created_at) matching entry
|
||||||
|
const entry = entries.find(
|
||||||
|
(e) =>
|
||||||
|
e.user_id === uid &&
|
||||||
|
e.date_from <= new Date(dateStr) &&
|
||||||
|
e.date_to >= new Date(dateStr),
|
||||||
|
);
|
||||||
|
if (entry) {
|
||||||
|
result[uid][dateStr] = {
|
||||||
|
source: "entry",
|
||||||
|
entryId: entry.id,
|
||||||
|
overrideId: null,
|
||||||
|
user_id: entry.user_id,
|
||||||
|
shift_date: dateStr,
|
||||||
|
project_id: entry.project_id,
|
||||||
|
category: entry.category,
|
||||||
|
note: entry.note,
|
||||||
|
rangeFrom: entry.date_from.toISOString().slice(0, 10),
|
||||||
|
rangeTo: entry.date_to.toISOString().slice(0, 10),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
result[uid][dateStr] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user