From f31797fb5b36270c6521499e583dea07db4649f1 Mon Sep 17 00:00:00 2001 From: BOHA Date: Fri, 5 Jun 2026 13:42:21 +0200 Subject: [PATCH] fix(plan): allow empty notes, use local time for past-date gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related fixes from manual smoke testing: - Zod schema: drop min(1) on note field across all four schemas — note is optional in real usage, and the modal initialized it to empty. - assertNotPastDate: replace toISOString() (UTC) with local-time getters to match the rest of the codebase. Fixes off-by-one near day boundaries. - createEntry / updateEntry: also check the date_to endpoint for past dates, so a range can't extend backwards into the past even when its start is today or future. Defense in depth. Modal: drop `required` attribute and asterisk from the note field. Tests: add 5 new cases covering empty note, past date HTTP rejection, and the new to-endpoint check in updateEntry. --- src/__tests__/plan.test.ts | 83 ++++++++++++++++++++++++++ src/admin/components/PlanCellModal.tsx | 3 +- src/schemas/plan.schema.ts | 14 ++--- src/services/plan.service.ts | 32 ++++++---- 4 files changed, 109 insertions(+), 23 deletions(-) diff --git a/src/__tests__/plan.test.ts b/src/__tests__/plan.test.ts index a284756..44e46d7 100644 --- a/src/__tests__/plan.test.ts +++ b/src/__tests__/plan.test.ts @@ -305,6 +305,42 @@ describe("plan.service.createEntry", () => { ); expect("error" in result).toBe(true); }); + + it("allows an empty note (text is optional)", async () => { + const result = await createEntry( + { + user_id: adminUserId, + date_from: "2099-07-15", + date_to: "2099-07-15", + category: "work", + note: "", + }, + adminUserId, + false, + ); + expect("data" in result).toBe(true); + }); + + it("rejects a range whose date_to is in the past, even if date_from is today", async () => { + // The range check `date_to >= date_from` fires before the past-date + // check, so a backwards range returns 400 (correct UX). The to-endpoint + // past-date check matters when updating an existing entry — see the + // matching test in updateEntry below. Here we just confirm that a + // range whose start is in the past is rejected as 403. + const result = await createEntry( + { + user_id: adminUserId, + date_from: "2000-01-01", + date_to: "2000-01-05", + category: "work", + note: `${N}past-both`, + }, + adminUserId, + false, + ); + expect("error" in result).toBe(true); + if ("error" in result) expect(result.status).toBe(403); + }); }); // --------------------------------------------------------------------------- @@ -338,6 +374,31 @@ describe("plan.service.updateEntry", () => { expect((updated.oldData as any).note).toBe(`${N}original`); } }); + + it("rejects an update that pushes date_to into the past", async () => { + // Future entry, but the user tries to extend its end backwards into + // the past. The to-endpoint past-date check should fire. + const created = await createEntry( + { + user_id: adminUserId, + date_from: "2099-08-10", + date_to: "2099-08-15", + category: "work", + note: `${N}to-shorten`, + }, + adminUserId, + false, + ); + if (!("data" in created)) throw new Error("setup failed"); + const updated = await updateEntry( + created.data.id, + { date_to: "2000-01-05" }, + adminUserId, + false, + ); + expect("error" in updated).toBe(true); + if ("error" in updated) expect(updated.status).toBe(403); + }); }); // --------------------------------------------------------------------------- @@ -787,6 +848,28 @@ describe("HTTP /api/admin/plan", () => { expect(body.data.note).toBe(`${N}http create`); }); + it("POST /entries accepts an empty note (note is optional)", async () => { + const res = await authPost("/api/admin/plan/entries", adminToken, { + user_id: adminUserId, + date_from: "2097-08-05", + date_to: "2097-08-05", + category: "work", + note: "", + }); + expect(res.statusCode).toBe(201); + }); + + it("POST /entries rejects a past date without ?force=1", async () => { + const res = await authPost("/api/admin/plan/entries", adminToken, { + user_id: adminUserId, + date_from: "2000-02-01", + date_to: "2000-02-01", + category: "work", + note: `${N}past http`, + }); + expect(res.statusCode).toBe(403); + }); + it("POST /overrides with force=1 allows past date", async () => { const res = await authPost( "/api/admin/plan/overrides?force=1", diff --git a/src/admin/components/PlanCellModal.tsx b/src/admin/components/PlanCellModal.tsx index aeca740..b29661f 100644 --- a/src/admin/components/PlanCellModal.tsx +++ b/src/admin/components/PlanCellModal.tsx @@ -286,13 +286,12 @@ function EditForm(props: Props) { ))} - +