fix(plan): allow empty notes, use local time for past-date gate

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.
This commit is contained in:
BOHA
2026-06-05 13:42:21 +02:00
parent 9c110abf46
commit f31797fb5b
4 changed files with 109 additions and 23 deletions

View File

@@ -250,9 +250,12 @@ export async function listPlanUsers(): Promise<PlanUser[]> {
* Guard helper: returns { error, status } if `dateStr` is in the past
* and `force` is not set. Returns null if the date is editable.
*
* The comparison uses ISO date string equality. For past-date guard purposes
* any date strictly before today (in the local timezone per the project's
* TZ=Europe/Prague setting) is locked.
* The comparison uses local Czech time (the project's TZ setting). The
* previous implementation used `toISOString().slice(0, 10)`, which is
* UTC — that could be off-by-one near day boundaries. The fix mirrors
* the `Date.prototype.toJSON` override in `src/config/env.ts` which
* also uses local-time getters, so the past-date gate is consistent
* with how dates are formatted throughout the rest of the codebase.
*/
export function assertNotPastDate(
dateStr: string,
@@ -260,8 +263,8 @@ export function assertNotPastDate(
): { error: string; status: number } | null {
if (force) return null;
const today = new Date();
const todayStr = today.toISOString().slice(0, 10);
const d = new Date();
const todayStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
if (dateStr < todayStr) {
return {
error:
@@ -328,9 +331,12 @@ export async function createEntry(
if (input.date_to < input.date_from) {
return { error: "Datum do musí být stejné nebo po datumu od", status: 400 };
}
// Past-date lock on the from-date (the earliest editable point)
const lock = assertNotPastDate(input.date_from, force);
if (lock) return lock;
// Past-date lock on both endpoints — a range that starts today but
// extends into the past would still lock past days in the grid.
const lockFrom = assertNotPastDate(input.date_from, force);
if (lockFrom) return lockFrom;
const lockTo = assertNotPastDate(input.date_to, force);
if (lockTo) return lockTo;
const created = await prisma.work_plan_entries.create({
data: {
@@ -365,11 +371,15 @@ export async function updateEntry(
return { error: "Záznam nenalezen", status: 404 };
}
// Past-date lock on the existing or new from-date
// Past-date lock on both endpoints (using either the new value if
// provided, or the existing value as a fallback).
const fromStr =
input.date_from ?? existing.date_from.toISOString().slice(0, 10);
const lock = assertNotPastDate(fromStr, force);
if (lock) return lock;
const toStr = input.date_to ?? existing.date_to.toISOString().slice(0, 10);
const lockFrom = assertNotPastDate(fromStr, force);
if (lockFrom) return lockFrom;
const lockTo = assertNotPastDate(toStr, force);
if (lockTo) return lockTo;
if (input.date_from && input.date_to && input.date_to < input.date_from) {
return { error: "Datum do musí být stejné nebo po datumu od", status: 400 };