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:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user