feat(plan): resolveCell/resolveGrid return arrays; dashboard shows up to 3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,10 +89,9 @@ afterAll(async () => {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
describe("plan.service.resolveCell", () => {
|
describe("plan.service.resolveCell", () => {
|
||||||
it("returns null when nothing covers the date", async () => {
|
it("returns [] when nothing covers the date", async () => {
|
||||||
// No entry, no override for (adminUserId, "2099-01-01")
|
|
||||||
const result = await resolveCell(adminUserId, "2099-01-01");
|
const result = await resolveCell(adminUserId, "2099-01-01");
|
||||||
expect(result).toBeNull();
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the entry that covers the date", async () => {
|
it("returns the entry that covers the date", async () => {
|
||||||
@@ -107,17 +106,14 @@ describe("plan.service.resolveCell", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = await resolveCell(adminUserId, "2099-06-05");
|
const result = await resolveCell(adminUserId, "2099-06-05");
|
||||||
expect(result).not.toBeNull();
|
expect(result.length).toBe(1);
|
||||||
expect(result?.source).toBe("entry");
|
expect(result[0].source).toBe("entry");
|
||||||
expect(result?.note).toBe(`${N}PLC upgrade`);
|
expect(result[0].note).toBe(`${N}PLC upgrade`);
|
||||||
expect(result?.category).toBe("work");
|
expect(result[0].rangeFrom).toBe("2099-06-01");
|
||||||
expect(result?.rangeFrom).toBe("2099-06-01");
|
expect(result[0].rangeTo).toBe("2099-06-10");
|
||||||
expect(result?.rangeTo).toBe("2099-06-10");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the override for that day, not the entry", async () => {
|
it("returns overrides (entries hidden) when an override exists", async () => {
|
||||||
// An entry covers 2099-06-01..2099-06-10, but an override on 2099-06-05
|
|
||||||
// takes precedence.
|
|
||||||
await prisma.work_plan_entries.create({
|
await prisma.work_plan_entries.create({
|
||||||
data: {
|
data: {
|
||||||
user_id: adminUserId,
|
user_id: adminUserId,
|
||||||
@@ -138,10 +134,51 @@ describe("plan.service.resolveCell", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = await resolveCell(adminUserId, "2099-06-05");
|
const result = await resolveCell(adminUserId, "2099-06-05");
|
||||||
expect(result).not.toBeNull();
|
expect(result.length).toBe(1);
|
||||||
expect(result?.source).toBe("override");
|
expect(result[0].source).toBe("override");
|
||||||
expect(result?.note).toBe(`${N}Volno po noční`);
|
expect(result[0].note).toBe(`${N}Volno po noční`);
|
||||||
expect(result?.category).toBe("leave");
|
});
|
||||||
|
|
||||||
|
it("returns multiple additive entries newest-first", async () => {
|
||||||
|
await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: new Date("2099-06-01"),
|
||||||
|
date_to: new Date("2099-06-10"),
|
||||||
|
category: "work",
|
||||||
|
note: `${N}first`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: new Date("2099-06-05"),
|
||||||
|
date_to: new Date("2099-06-05"),
|
||||||
|
category: "work",
|
||||||
|
note: `${N}second`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await resolveCell(adminUserId, "2099-06-05");
|
||||||
|
expect(result.length).toBe(2);
|
||||||
|
expect(result[0].note).toBe(`${N}second`); // newest first
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caps the returned records at MAX_RECORDS_PER_CELL", async () => {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
await prisma.work_plan_overrides.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: new Date("2099-06-06"),
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}cap${i}`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const result = await resolveCell(adminUserId, "2099-06-06");
|
||||||
|
expect(result.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores soft-deleted entries and overrides", async () => {
|
it("ignores soft-deleted entries and overrides", async () => {
|
||||||
@@ -157,7 +194,7 @@ describe("plan.service.resolveCell", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = await resolveCell(adminUserId, "2099-06-05");
|
const result = await resolveCell(adminUserId, "2099-06-05");
|
||||||
expect(result).toBeNull();
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,11 +215,11 @@ describe("plan.service.resolveGrid", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const cells = await resolveGrid([adminUserId], "2099-06-01", "2099-06-05");
|
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-01"][0]?.note).toBe(`${N}A`);
|
||||||
expect(cells[adminUserId]["2099-06-02"]?.note).toBe(`${N}A`);
|
expect(cells[adminUserId]["2099-06-02"][0]?.note).toBe(`${N}A`);
|
||||||
expect(cells[adminUserId]["2099-06-03"]?.note).toBe(`${N}A`);
|
expect(cells[adminUserId]["2099-06-03"][0]?.note).toBe(`${N}A`);
|
||||||
expect(cells[adminUserId]["2099-06-04"]).toBeNull();
|
expect(cells[adminUserId]["2099-06-04"]).toEqual([]);
|
||||||
expect(cells[adminUserId]["2099-06-05"]).toBeNull();
|
expect(cells[adminUserId]["2099-06-05"]).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies override on a covered day", async () => {
|
it("applies override on a covered day", async () => {
|
||||||
@@ -206,9 +243,9 @@ describe("plan.service.resolveGrid", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const cells = await resolveGrid([adminUserId], "2099-06-01", "2099-06-03");
|
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-01"][0]?.note).toBe(`${N}A`);
|
||||||
expect(cells[adminUserId]["2099-06-02"]?.note).toBe(`${N}B`);
|
expect(cells[adminUserId]["2099-06-02"][0]?.note).toBe(`${N}B`);
|
||||||
expect(cells[adminUserId]["2099-06-03"]?.note).toBe(`${N}A`);
|
expect(cells[adminUserId]["2099-06-03"][0]?.note).toBe(`${N}A`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -857,7 +894,7 @@ describe("HTTP /api/admin/plan", () => {
|
|||||||
const body = res.json();
|
const body = res.json();
|
||||||
expect(body.data.users).toBeDefined();
|
expect(body.data.users).toBeDefined();
|
||||||
expect(body.data.cells[adminUserId]).toBeDefined();
|
expect(body.data.cells[adminUserId]).toBeDefined();
|
||||||
expect(body.data.cells[adminUserId]["2097-06-01"].note).toBe(
|
expect(body.data.cells[adminUserId]["2097-06-01"][0].note).toBe(
|
||||||
`${N}grid test`,
|
`${N}grid test`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -529,7 +529,8 @@ export default function PlanGrid({
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
{users.map((u) => {
|
{users.map((u) => {
|
||||||
const cell = data.cells[u.id]?.[date] ?? null;
|
const cellArr = data.cells[u.id]?.[date] ?? [];
|
||||||
|
const cell = cellArr[0] ?? null;
|
||||||
const past = isPastDate(date, today);
|
const past = isPastDate(date, today);
|
||||||
// Past-day cells are read-only in the UI: an empty past
|
// Past-day cells are read-only in the UI: an empty past
|
||||||
// cell is non-interactive (no create modal, no "+" hint),
|
// cell is non-interactive (no create modal, no "+" hint),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Typography from "@mui/material/Typography";
|
|||||||
import { Card } from "../../ui";
|
import { Card } from "../../ui";
|
||||||
import { formatDate } from "../../utils/formatters";
|
import { formatDate } from "../../utils/formatters";
|
||||||
|
|
||||||
/** The logged-in user's resolved work plan for today (see GET /api/admin/dashboard). */
|
/** One resolved plan record for today (see GET /api/admin/dashboard). */
|
||||||
export interface TodayPlan {
|
export interface TodayPlan {
|
||||||
shift_date: string;
|
shift_date: string;
|
||||||
category: string;
|
category: string;
|
||||||
@@ -23,33 +23,12 @@ function projectLabel(p: TodayPlan): string {
|
|||||||
return parts.length > 0 ? parts.join(" — ") : "Bez projektu";
|
return parts.length > 0 ? parts.join(" — ") : "Bez projektu";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function PlanRow({ plan }: { plan: TodayPlan }) {
|
||||||
* "Vaše dnešní zařazení" — shows the logged-in user's work-plan entry for today
|
const color = plan.category_color || "var(--mui-palette-primary-main)";
|
||||||
* (category + project/site + note, with a range badge when the day is part of a
|
|
||||||
* multi-day range). `plan === null` = the user has plan access but nothing is
|
|
||||||
* scheduled today (muted empty state). Rendered near the clock-in on the
|
|
||||||
* dashboard; the caller gates it on the plan permission.
|
|
||||||
*/
|
|
||||||
export default function DashTodayPlan({ plan }: { plan: TodayPlan | null }) {
|
|
||||||
const color = plan?.category_color || "var(--mui-palette-primary-main)";
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ mb: 3 }}>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
sx={{ mb: 1.5, fontWeight: 600, color: "text.secondary" }}
|
|
||||||
>
|
|
||||||
Vaše dnešní zařazení
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{plan ? (
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 1,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -80,26 +59,51 @@ export default function DashTodayPlan({ plan }: { plan: TodayPlan | null }) {
|
|||||||
{plan.category_label}
|
{plan.category_label}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{plan.rangeFrom &&
|
{plan.rangeFrom && plan.rangeTo && plan.rangeFrom !== plan.rangeTo && (
|
||||||
plan.rangeTo &&
|
|
||||||
plan.rangeFrom !== plan.rangeTo && (
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
součást rozsahu {formatDate(plan.rangeFrom)} –{" "}
|
součást rozsahu {formatDate(plan.rangeFrom)} –{" "}
|
||||||
{formatDate(plan.rangeTo)}
|
{formatDate(plan.rangeTo)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="h6" sx={{ lineHeight: 1.25 }}>
|
<Typography variant="h6" sx={{ lineHeight: 1.25 }}>
|
||||||
{projectLabel(plan)}
|
{projectLabel(plan)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{plan.note && (
|
{plan.note && (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{plan.note}
|
{plan.note}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "Vaše dnešní zařazení" — the logged-in user's resolved plan record(s) for
|
||||||
|
* today (up to 3). Empty array = nothing scheduled. */
|
||||||
|
export default function DashTodayPlan({ plans }: { plans: TodayPlan[] }) {
|
||||||
|
return (
|
||||||
|
<Card sx={{ mb: 3 }}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{ mb: 1.5, fontWeight: 600, color: "text.secondary" }}
|
||||||
|
>
|
||||||
|
Vaše dnešní zařazení
|
||||||
|
</Typography>
|
||||||
|
{plans.length > 0 ? (
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
|
{plans.map((p, i) => (
|
||||||
|
<Box
|
||||||
|
key={i}
|
||||||
|
sx={{
|
||||||
|
pt: i === 0 ? 0 : 2,
|
||||||
|
borderTop: i === 0 ? 0 : 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlanRow plan={p} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Pro dnešek nemáte naplánováno.
|
Pro dnešek nemáte naplánováno.
|
||||||
|
|||||||
@@ -150,16 +150,16 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
key: readonly unknown[],
|
key: readonly unknown[],
|
||||||
userId: number,
|
userId: number,
|
||||||
dates: string[],
|
dates: string[],
|
||||||
mutator: (prev: ResolvedCell | null) => ResolvedCell | null,
|
mutator: (prev: ResolvedCell[]) => ResolvedCell[],
|
||||||
): Record<string, ResolvedCell | null> | null {
|
): Record<string, ResolvedCell[]> | null {
|
||||||
const prev = snapshotGrid(key);
|
const prev = snapshotGrid(key);
|
||||||
if (!prev) return null;
|
if (!prev) return null;
|
||||||
const userPrev = prev.cells[userId] ?? {};
|
const userPrev = prev.cells[userId] ?? {};
|
||||||
const rolled: Record<string, ResolvedCell | null> = {};
|
const rolled: Record<string, ResolvedCell[]> = {};
|
||||||
const userNext: Record<string, ResolvedCell | null> = { ...userPrev };
|
const userNext: Record<string, ResolvedCell[]> = { ...userPrev };
|
||||||
for (const date of dates) {
|
for (const date of dates) {
|
||||||
rolled[date] = userPrev[date] ?? null;
|
rolled[date] = userPrev[date] ?? [];
|
||||||
userNext[date] = mutator(rolled[date]);
|
userNext[date] = mutator(rolled[date]).slice(0, 3);
|
||||||
}
|
}
|
||||||
qc.setQueryData(key, {
|
qc.setQueryData(key, {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -244,7 +244,7 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
// the ResolvedCell mirrors the request body.
|
// the ResolvedCell mirrors the request body.
|
||||||
const days = eachDay(body.date_from, body.date_to);
|
const days = eachDay(body.date_from, body.date_to);
|
||||||
const id = data && typeof data.id === "number" ? data.id : null;
|
const id = data && typeof data.id === "number" ? data.id : null;
|
||||||
const rolled = patchCells(currentGridKey, body.user_id, days, () =>
|
const rolled = patchCells(currentGridKey, body.user_id, days, (prev) => [
|
||||||
makeEntryCell({
|
makeEntryCell({
|
||||||
userId: body.user_id,
|
userId: body.user_id,
|
||||||
date: days[0],
|
date: days[0],
|
||||||
@@ -255,7 +255,8 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
rangeTo: body.date_to,
|
rangeTo: body.date_to,
|
||||||
entryId: id,
|
entryId: id,
|
||||||
}),
|
}),
|
||||||
);
|
...prev,
|
||||||
|
]);
|
||||||
// Stash the rollback on the mutation object so PlanWork can call
|
// Stash the rollback on the mutation object so PlanWork can call
|
||||||
// it from onError.
|
// it from onError.
|
||||||
(createEntry as any)._rolled = rolled;
|
(createEntry as any)._rolled = rolled;
|
||||||
@@ -294,8 +295,8 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
let ownerUserId: number | null = null;
|
let ownerUserId: number | null = null;
|
||||||
if (grid) {
|
if (grid) {
|
||||||
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
for (const cell of Object.values(byDate)) {
|
for (const cells of Object.values(byDate)) {
|
||||||
if (cell && cell.entryId === id) {
|
if (cells.some((c) => c.entryId === id)) {
|
||||||
ownerUserId = Number(uidStr);
|
ownerUserId = Number(uidStr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -304,20 +305,27 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ownerUserId !== null) {
|
if (ownerUserId !== null) {
|
||||||
const rolled = patchCells(currentGridKey, ownerUserId, days, (prev) =>
|
const rolled = patchCells(
|
||||||
makeEntryCell({
|
currentGridKey,
|
||||||
|
ownerUserId,
|
||||||
|
days,
|
||||||
|
(prev) => {
|
||||||
|
const existing = prev.find((c) => c.entryId === id) ?? null;
|
||||||
|
const updated = makeEntryCell({
|
||||||
userId: ownerUserId!,
|
userId: ownerUserId!,
|
||||||
date: days[0],
|
date: days[0],
|
||||||
projectId:
|
projectId:
|
||||||
body.project_id === undefined
|
body.project_id === undefined
|
||||||
? (prev?.project_id ?? null)
|
? (existing?.project_id ?? null)
|
||||||
: body.project_id,
|
: body.project_id,
|
||||||
category: body.category ?? prev?.category ?? "work",
|
category: body.category ?? existing?.category ?? "work",
|
||||||
note: body.note ?? prev?.note ?? "",
|
note: body.note ?? existing?.note ?? "",
|
||||||
rangeFrom: body.date_from,
|
rangeFrom: body.date_from,
|
||||||
rangeTo: body.date_to,
|
rangeTo: body.date_to,
|
||||||
entryId: id,
|
entryId: id,
|
||||||
}),
|
});
|
||||||
|
return [updated, ...prev.filter((c) => c.entryId !== id)];
|
||||||
|
},
|
||||||
);
|
);
|
||||||
(updateEntry as any)._rolled = rolled;
|
(updateEntry as any)._rolled = rolled;
|
||||||
}
|
}
|
||||||
@@ -338,26 +346,29 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
||||||
if (grid) {
|
if (grid) {
|
||||||
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
for (const [, cell] of Object.entries(byDate)) {
|
let range: { from: string; to: string } | null = null;
|
||||||
if (
|
for (const cells of Object.values(byDate)) {
|
||||||
cell &&
|
const hit = cells.find(
|
||||||
cell.entryId === vars.id &&
|
(c) => c.entryId === vars.id && c.rangeFrom && c.rangeTo,
|
||||||
cell.rangeFrom &&
|
);
|
||||||
cell.rangeTo
|
if (hit) {
|
||||||
) {
|
range = { from: hit.rangeFrom!, to: hit.rangeTo! };
|
||||||
const days = eachDay(cell.rangeFrom, cell.rangeTo);
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (range) {
|
||||||
|
const days = eachDay(range.from, range.to);
|
||||||
const rolled = patchCells(
|
const rolled = patchCells(
|
||||||
currentGridKey,
|
currentGridKey,
|
||||||
Number(uidStr),
|
Number(uidStr),
|
||||||
days,
|
days,
|
||||||
() => null,
|
(prev) => prev.filter((c) => c.entryId !== vars.id),
|
||||||
);
|
);
|
||||||
(deleteEntry as any)._rolled = rolled;
|
(deleteEntry as any)._rolled = rolled;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
invalidate();
|
invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -375,7 +386,7 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
currentGridKey,
|
currentGridKey,
|
||||||
vars.user_id,
|
vars.user_id,
|
||||||
[vars.shift_date],
|
[vars.shift_date],
|
||||||
() =>
|
(prev) => [
|
||||||
makeOverrideCell({
|
makeOverrideCell({
|
||||||
userId: vars.user_id,
|
userId: vars.user_id,
|
||||||
date: vars.shift_date,
|
date: vars.shift_date,
|
||||||
@@ -384,6 +395,8 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
note: vars.note,
|
note: vars.note,
|
||||||
overrideId: id,
|
overrideId: id,
|
||||||
}),
|
}),
|
||||||
|
...prev,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
(createOverride as any)._rolled = rolled;
|
(createOverride as any)._rolled = rolled;
|
||||||
invalidate();
|
invalidate();
|
||||||
@@ -411,21 +424,30 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
||||||
if (grid) {
|
if (grid) {
|
||||||
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
for (const [date, cell] of Object.entries(byDate)) {
|
for (const [date, cells] of Object.entries(byDate)) {
|
||||||
if (cell && cell.overrideId === vars.id) {
|
if (cells.some((c) => c.overrideId === vars.id)) {
|
||||||
const rolled = patchCells(
|
const rolled = patchCells(
|
||||||
currentGridKey,
|
currentGridKey,
|
||||||
Number(uidStr),
|
Number(uidStr),
|
||||||
[date],
|
[date],
|
||||||
(prev) =>
|
(prev) => {
|
||||||
makeOverrideCell({
|
const existing =
|
||||||
|
prev.find((c) => c.overrideId === vars.id) ?? null;
|
||||||
|
const updated = makeOverrideCell({
|
||||||
userId: Number(uidStr),
|
userId: Number(uidStr),
|
||||||
date,
|
date,
|
||||||
projectId: vars.body.project_id ?? prev?.project_id ?? null,
|
projectId:
|
||||||
category: vars.body.category ?? prev?.category ?? "work",
|
vars.body.project_id ?? existing?.project_id ?? null,
|
||||||
note: vars.body.note ?? prev?.note ?? "",
|
category:
|
||||||
|
vars.body.category ?? existing?.category ?? "work",
|
||||||
|
note: vars.body.note ?? existing?.note ?? "",
|
||||||
overrideId: vars.id,
|
overrideId: vars.id,
|
||||||
}),
|
});
|
||||||
|
return [
|
||||||
|
updated,
|
||||||
|
...prev.filter((c) => c.overrideId !== vars.id),
|
||||||
|
];
|
||||||
|
},
|
||||||
);
|
);
|
||||||
(updateOverride as any)._rolled = rolled;
|
(updateOverride as any)._rolled = rolled;
|
||||||
break;
|
break;
|
||||||
@@ -446,13 +468,13 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
||||||
if (grid) {
|
if (grid) {
|
||||||
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
for (const [date, cell] of Object.entries(byDate)) {
|
for (const [date, cells] of Object.entries(byDate)) {
|
||||||
if (cell && cell.overrideId === vars.id) {
|
if (cells.some((c) => c.overrideId === vars.id)) {
|
||||||
const rolled = patchCells(
|
const rolled = patchCells(
|
||||||
currentGridKey,
|
currentGridKey,
|
||||||
Number(uidStr),
|
Number(uidStr),
|
||||||
[date],
|
[date],
|
||||||
() => null,
|
(prev) => prev.filter((c) => c.overrideId !== vars.id),
|
||||||
);
|
);
|
||||||
(deleteOverride as any)._rolled = rolled;
|
(deleteOverride as any)._rolled = rolled;
|
||||||
break;
|
break;
|
||||||
@@ -472,15 +494,15 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
// directly without the "weak type" mismatch a `{ _rolled? }` param causes.
|
// directly without the "weak type" mismatch a `{ _rolled? }` param causes.
|
||||||
function rollbackMutation(mutation: unknown, userId: number) {
|
function rollbackMutation(mutation: unknown, userId: number) {
|
||||||
const stash = mutation as {
|
const stash = mutation as {
|
||||||
_rolled?: Record<string, ResolvedCell | null> | null;
|
_rolled?: Record<string, ResolvedCell[]> | null;
|
||||||
};
|
};
|
||||||
if (!stash._rolled) return;
|
if (!stash._rolled) return;
|
||||||
const prev = qc.getQueryData<GridData>(currentGridKey as any);
|
const prev = qc.getQueryData<GridData>(currentGridKey as any);
|
||||||
if (!prev) return;
|
if (!prev) return;
|
||||||
const userPrev = prev.cells[userId] ?? {};
|
const userPrev = prev.cells[userId] ?? {};
|
||||||
const userNext = { ...userPrev };
|
const userNext = { ...userPrev };
|
||||||
for (const [date, cell] of Object.entries(stash._rolled)) {
|
for (const [date, cells] of Object.entries(stash._rolled)) {
|
||||||
userNext[date] = cell;
|
userNext[date] = cells;
|
||||||
}
|
}
|
||||||
qc.setQueryData(currentGridKey, {
|
qc.setQueryData(currentGridKey, {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -518,10 +540,18 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCells(
|
||||||
|
grid: GridData | undefined,
|
||||||
|
userId: number,
|
||||||
|
date: string,
|
||||||
|
): ResolvedCell[] {
|
||||||
|
return grid?.cells?.[userId]?.[date] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
export function getCell(
|
export function getCell(
|
||||||
grid: GridData | undefined,
|
grid: GridData | undefined,
|
||||||
userId: number,
|
userId: number,
|
||||||
date: string,
|
date: string,
|
||||||
): ResolvedCell | null {
|
): ResolvedCell | null {
|
||||||
return grid?.cells?.[userId]?.[date] ?? null;
|
return getCells(grid, userId, date)[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export interface GridData {
|
|||||||
date_from: string;
|
date_from: string;
|
||||||
date_to: string;
|
date_to: string;
|
||||||
users: PlanUser[];
|
users: PlanUser[];
|
||||||
cells: Record<number, Record<string, ResolvedCell | null>>;
|
cells: Record<number, Record<string, ResolvedCell[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const planKeys = {
|
export const planKeys = {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ interface DashData {
|
|||||||
pending_orders?: number;
|
pending_orders?: number;
|
||||||
unpaid_invoices?: number;
|
unpaid_invoices?: number;
|
||||||
pending_leave_requests?: number;
|
pending_leave_requests?: number;
|
||||||
today_plan?: TodayPlan | null;
|
today_plan?: TodayPlan[];
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ export default function Dashboard() {
|
|||||||
{!dashLoading &&
|
{!dashLoading &&
|
||||||
(hasPermission("attendance.record") ||
|
(hasPermission("attendance.record") ||
|
||||||
hasPermission("attendance.manage")) && (
|
hasPermission("attendance.manage")) && (
|
||||||
<DashTodayPlan plan={dashData?.today_plan ?? null} />
|
<DashTodayPlan plans={dashData?.today_plan ?? []} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Quick actions */}
|
{/* Quick actions */}
|
||||||
|
|||||||
@@ -327,8 +327,9 @@ export default function PlanWork() {
|
|||||||
let firstDate: string | null = null;
|
let firstDate: string | null = null;
|
||||||
if (grid) {
|
if (grid) {
|
||||||
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
for (const [date, cell] of Object.entries(byDate)) {
|
for (const [date, cells] of Object.entries(byDate)) {
|
||||||
if (cell && cell.entryId === id) {
|
const hit = cells.find((c) => c.entryId === id);
|
||||||
|
if (hit) {
|
||||||
userId = Number(uidStr);
|
userId = Number(uidStr);
|
||||||
firstDate = body.date_from ?? date;
|
firstDate = body.date_from ?? date;
|
||||||
break;
|
break;
|
||||||
@@ -357,10 +358,11 @@ export default function PlanWork() {
|
|||||||
let firstDate: string | null = null;
|
let firstDate: string | null = null;
|
||||||
if (grid) {
|
if (grid) {
|
||||||
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
for (const [date, cell] of Object.entries(byDate)) {
|
for (const [date, cells] of Object.entries(byDate)) {
|
||||||
if (cell && cell.entryId === id) {
|
const hit = cells.find((c) => c.entryId === id);
|
||||||
|
if (hit) {
|
||||||
userId = Number(uidStr);
|
userId = Number(uidStr);
|
||||||
firstDate = cell.rangeFrom ?? date;
|
firstDate = hit.rangeFrom ?? date;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,8 +404,9 @@ export default function PlanWork() {
|
|||||||
let date: string | null = null;
|
let date: string | null = null;
|
||||||
if (grid) {
|
if (grid) {
|
||||||
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
for (const [d, cell] of Object.entries(byDate)) {
|
for (const [d, cells] of Object.entries(byDate)) {
|
||||||
if (cell && cell.overrideId === id) {
|
const hit = cells.find((c) => c.overrideId === id);
|
||||||
|
if (hit) {
|
||||||
userId = Number(uidStr);
|
userId = Number(uidStr);
|
||||||
date = d;
|
date = d;
|
||||||
break;
|
break;
|
||||||
@@ -431,8 +434,9 @@ export default function PlanWork() {
|
|||||||
let date: string | null = null;
|
let date: string | null = null;
|
||||||
if (grid) {
|
if (grid) {
|
||||||
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
for (const [d, cell] of Object.entries(byDate)) {
|
for (const [d, cells] of Object.entries(byDate)) {
|
||||||
if (cell && cell.overrideId === id) {
|
const hit = cells.find((c) => c.overrideId === id);
|
||||||
|
if (hit) {
|
||||||
userId = Number(uidStr);
|
userId = Number(uidStr);
|
||||||
date = d;
|
date = d;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -44,26 +44,27 @@ export default async function dashboardRoutes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Today's work plan (personal) — powers the "Vaše dnešní zařazení" card.
|
// Today's work plan (personal) — powers the "Vaše dnešní zařazení" card.
|
||||||
// resolveCell applies override-beats-range precedence; null = the user has
|
// resolveCell returns 0–3 records (override-beats-range layering); an empty
|
||||||
// plan access but nothing is scheduled today. The category key is enriched
|
// array means the user has plan access but nothing is scheduled today. Each
|
||||||
// with its label + colour so the widget needs no extra query. todayStr uses
|
// record's category key is enriched with its label + colour so the widget
|
||||||
// local (Europe/Prague) calendar date — the plan's @db.Date day.
|
// needs no extra query. todayStr uses local (Europe/Prague) calendar date —
|
||||||
|
// the plan's @db.Date day.
|
||||||
if (has("attendance.record") || has("attendance.manage")) {
|
if (has("attendance.record") || has("attendance.manage")) {
|
||||||
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||||
const cell = await resolveCell(userId, todayStr);
|
const cells = await resolveCell(userId, todayStr);
|
||||||
if (cell) {
|
result.today_plan = await Promise.all(
|
||||||
|
cells.map(async (cell) => {
|
||||||
const cat = await prisma.plan_categories.findUnique({
|
const cat = await prisma.plan_categories.findUnique({
|
||||||
where: { key: cell.category },
|
where: { key: cell.category },
|
||||||
select: { label: true, color: true },
|
select: { label: true, color: true },
|
||||||
});
|
});
|
||||||
result.today_plan = {
|
return {
|
||||||
...cell,
|
...cell,
|
||||||
category_label: cat?.label ?? cell.category,
|
category_label: cat?.label ?? cell.category,
|
||||||
category_color: cat?.color ?? null,
|
category_color: cat?.color ?? null,
|
||||||
};
|
};
|
||||||
} else {
|
}),
|
||||||
result.today_plan = null;
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attendance admin — only for attendance.manage
|
// Attendance admin — only for attendance.manage
|
||||||
|
|||||||
@@ -108,12 +108,14 @@ export interface ResolvedCell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the effective plan cell for (userId, dateStr).
|
* Compute the effective plan records for (userId, dateStr) as an array of
|
||||||
|
* 0–MAX_RECORDS_PER_CELL records.
|
||||||
*
|
*
|
||||||
* Precedence: override > entry > null.
|
* Layering: if any override exists for the day, the overrides are returned
|
||||||
* Soft-deleted rows are ignored (is_deleted = false filter).
|
* (newest-first) and the range entries are hidden; otherwise the range
|
||||||
* If multiple entries cover the same day, the latest by created_at wins
|
* entries covering the day are returned (newest-first). Soft-deleted rows
|
||||||
* and a warning is logged.
|
* are ignored (is_deleted = false filter). The result is capped at
|
||||||
|
* MAX_RECORDS_PER_CELL.
|
||||||
*
|
*
|
||||||
* dateStr must be a YYYY-MM-DD string. The function builds a JS Date
|
* dateStr must be a YYYY-MM-DD string. The function builds a JS Date
|
||||||
* from it and Prisma handles timezone conversion against the MySQL
|
* from it and Prisma handles timezone conversion against the MySQL
|
||||||
@@ -122,31 +124,31 @@ export interface ResolvedCell {
|
|||||||
export async function resolveCell(
|
export async function resolveCell(
|
||||||
userId: number,
|
userId: number,
|
||||||
dateStr: string,
|
dateStr: string,
|
||||||
): Promise<ResolvedCell | null> {
|
): Promise<ResolvedCell[]> {
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
|
|
||||||
// 1. Single-day override for this exact date
|
const overrides = await prisma.work_plan_overrides.findMany({
|
||||||
const override = await prisma.work_plan_overrides.findFirst({
|
|
||||||
where: { user_id: userId, shift_date: date, is_deleted: false },
|
where: { user_id: userId, shift_date: date, is_deleted: false },
|
||||||
|
orderBy: { created_at: "desc" },
|
||||||
|
take: MAX_RECORDS_PER_CELL,
|
||||||
include: { projects: projectSelect },
|
include: { projects: projectSelect },
|
||||||
});
|
});
|
||||||
if (override) {
|
if (overrides.length > 0) {
|
||||||
return {
|
return overrides.map((o) => ({
|
||||||
source: "override",
|
source: "override" as const,
|
||||||
entryId: null,
|
entryId: null,
|
||||||
overrideId: override.id,
|
overrideId: o.id,
|
||||||
user_id: override.user_id,
|
user_id: o.user_id,
|
||||||
shift_date: dateStr,
|
shift_date: dateStr,
|
||||||
project_id: override.project_id,
|
project_id: o.project_id,
|
||||||
...projectFields(override.projects),
|
...projectFields(o.projects),
|
||||||
category: override.category,
|
category: o.category,
|
||||||
note: override.note,
|
note: o.note,
|
||||||
rangeFrom: null,
|
rangeFrom: null,
|
||||||
rangeTo: null,
|
rangeTo: null,
|
||||||
};
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Range entry that covers the day
|
|
||||||
const entries = await prisma.work_plan_entries.findMany({
|
const entries = await prisma.work_plan_entries.findMany({
|
||||||
where: {
|
where: {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
@@ -155,24 +157,11 @@ export async function resolveCell(
|
|||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
},
|
},
|
||||||
orderBy: { created_at: "desc" },
|
orderBy: { created_at: "desc" },
|
||||||
|
take: MAX_RECORDS_PER_CELL,
|
||||||
include: { projects: projectSelect },
|
include: { projects: projectSelect },
|
||||||
});
|
});
|
||||||
|
return entries.map((entry) => ({
|
||||||
if (entries.length === 0) return null;
|
source: "entry" as const,
|
||||||
|
|
||||||
if (entries.length > 1) {
|
|
||||||
// Multiple entries overlap. Use the latest. A persistent audit-log
|
|
||||||
// entry would be ideal here, but logAudit() currently requires a
|
|
||||||
// FastifyRequest — services have no request. Use console.warn as a
|
|
||||||
// stand-in until a service-context audit logger is introduced.
|
|
||||||
console.warn(
|
|
||||||
`[plan.service] multiple entries cover user ${userId} on ${dateStr}; using the latest (entry id ${entries[0].id})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = entries[0];
|
|
||||||
return {
|
|
||||||
source: "entry",
|
|
||||||
entryId: entry.id,
|
entryId: entry.id,
|
||||||
overrideId: null,
|
overrideId: null,
|
||||||
user_id: entry.user_id,
|
user_id: entry.user_id,
|
||||||
@@ -183,12 +172,14 @@ export async function resolveCell(
|
|||||||
note: entry.note,
|
note: entry.note,
|
||||||
rangeFrom: entry.date_from.toISOString().slice(0, 10),
|
rangeFrom: entry.date_from.toISOString().slice(0, 10),
|
||||||
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
|
* Compute the effective plan records for every (userId, date) in the
|
||||||
* given range. Returns a 2D map: cells[userId][dateStr] = ResolvedCell | null.
|
* given range. Returns a 2D map: cells[userId][dateStr] = ResolvedCell[]
|
||||||
|
* (0–MAX_RECORDS_PER_CELL records, same layering as resolveCell; empty
|
||||||
|
* days are []).
|
||||||
*
|
*
|
||||||
* Implementation: load all entries and overrides for the range in two
|
* Implementation: load all entries and overrides for the range in two
|
||||||
* queries, then iterate the dates and resolve each cell in O(1) per
|
* queries, then iterate the dates and resolve each cell in O(1) per
|
||||||
@@ -198,7 +189,7 @@ export async function resolveGrid(
|
|||||||
userIds: number[],
|
userIds: number[],
|
||||||
dateFromStr: string,
|
dateFromStr: string,
|
||||||
dateToStr: string,
|
dateToStr: string,
|
||||||
): Promise<Record<number, Record<string, ResolvedCell | null>>> {
|
): Promise<Record<number, Record<string, ResolvedCell[]>>> {
|
||||||
const dateFrom = new Date(dateFromStr);
|
const dateFrom = new Date(dateFromStr);
|
||||||
const dateTo = new Date(dateToStr);
|
const dateTo = new Date(dateToStr);
|
||||||
|
|
||||||
@@ -219,11 +210,11 @@ export async function resolveGrid(
|
|||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
shift_date: { gte: dateFrom, lte: dateTo },
|
shift_date: { gte: dateFrom, lte: dateTo },
|
||||||
},
|
},
|
||||||
|
orderBy: { created_at: "desc" },
|
||||||
include: { projects: projectSelect },
|
include: { projects: projectSelect },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Build a date list (inclusive)
|
|
||||||
const dates: string[] = [];
|
const dates: string[] = [];
|
||||||
for (
|
for (
|
||||||
let d = new Date(dateFrom);
|
let d = new Date(dateFrom);
|
||||||
@@ -233,56 +224,52 @@ export async function resolveGrid(
|
|||||||
dates.push(d.toISOString().slice(0, 10));
|
dates.push(d.toISOString().slice(0, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: Record<number, Record<string, ResolvedCell | null>> = {};
|
const result: Record<number, Record<string, ResolvedCell[]>> = {};
|
||||||
for (const uid of userIds) {
|
for (const uid of userIds) {
|
||||||
result[uid] = {};
|
result[uid] = {};
|
||||||
for (const dateStr of dates) {
|
for (const dateStr of dates) {
|
||||||
// Override first
|
const day = new Date(dateStr);
|
||||||
const override = overrides.find(
|
const dayOverrides = overrides
|
||||||
|
.filter(
|
||||||
(o) =>
|
(o) =>
|
||||||
o.user_id === uid &&
|
o.user_id === uid &&
|
||||||
o.shift_date.toISOString().slice(0, 10) === dateStr,
|
o.shift_date.toISOString().slice(0, 10) === dateStr,
|
||||||
);
|
)
|
||||||
if (override) {
|
.slice(0, MAX_RECORDS_PER_CELL);
|
||||||
result[uid][dateStr] = {
|
if (dayOverrides.length > 0) {
|
||||||
source: "override",
|
result[uid][dateStr] = dayOverrides.map((o) => ({
|
||||||
|
source: "override" as const,
|
||||||
entryId: null,
|
entryId: null,
|
||||||
overrideId: override.id,
|
overrideId: o.id,
|
||||||
user_id: override.user_id,
|
user_id: o.user_id,
|
||||||
shift_date: dateStr,
|
shift_date: dateStr,
|
||||||
project_id: override.project_id,
|
project_id: o.project_id,
|
||||||
...projectFields(override.projects),
|
...projectFields(o.projects),
|
||||||
category: override.category,
|
category: o.category,
|
||||||
note: override.note,
|
note: o.note,
|
||||||
rangeFrom: null,
|
rangeFrom: null,
|
||||||
rangeTo: null,
|
rangeTo: null,
|
||||||
};
|
}));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// First (latest by created_at) matching entry
|
const dayEntries = entries
|
||||||
const entry = entries.find(
|
.filter(
|
||||||
(e) =>
|
(e) => e.user_id === uid && e.date_from <= day && e.date_to >= day,
|
||||||
e.user_id === uid &&
|
)
|
||||||
e.date_from <= new Date(dateStr) &&
|
.slice(0, MAX_RECORDS_PER_CELL);
|
||||||
e.date_to >= new Date(dateStr),
|
result[uid][dateStr] = dayEntries.map((e) => ({
|
||||||
);
|
source: "entry" as const,
|
||||||
if (entry) {
|
entryId: e.id,
|
||||||
result[uid][dateStr] = {
|
|
||||||
source: "entry",
|
|
||||||
entryId: entry.id,
|
|
||||||
overrideId: null,
|
overrideId: null,
|
||||||
user_id: entry.user_id,
|
user_id: e.user_id,
|
||||||
shift_date: dateStr,
|
shift_date: dateStr,
|
||||||
project_id: entry.project_id,
|
project_id: e.project_id,
|
||||||
...projectFields(entry.projects),
|
...projectFields(e.projects),
|
||||||
category: entry.category,
|
category: e.category,
|
||||||
note: entry.note,
|
note: e.note,
|
||||||
rangeFrom: entry.date_from.toISOString().slice(0, 10),
|
rangeFrom: e.date_from.toISOString().slice(0, 10),
|
||||||
rangeTo: entry.date_to.toISOString().slice(0, 10),
|
rangeTo: e.date_to.toISOString().slice(0, 10),
|
||||||
};
|
}));
|
||||||
} else {
|
|
||||||
result[uid][dateStr] = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user