mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
* fix(issues): store start_date/due_date as DATE, not timestamp (MUL-2925) These fields are calendar days (the pickers offer no time-of-day), but were stored as TIMESTAMPTZ. A client serializing local midnight via toISOString() folded its timezone into the instant, so the day shifted by the local offset (GH #3618). Migrate the columns to DATE and parse/serialize date-only "YYYY-MM-DD". ParseCalendarDate still accepts legacy RFC3339 (truncated to the UTC day) so older clients keep working. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): render start_date/due_date as timezone-stable calendar days (MUL-2925) Pickers now emit date-only "YYYY-MM-DD" (local calendar day) instead of toISOString(), and every read formats via the shared @multica/core/issues/date helpers with timeZone:"UTC" so the day never shifts with the viewer's offset. The Gantt's existing UTC bucketing is now correct. Covers web/desktop pickers, quick-set menu, list/board/detail/activity, and the mobile due-date picker. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address date-only review — loud-fail ambiguous dates, finish display sweep (MUL-2925) Review follow-ups on #3692: - ParseCalendarDate no longer silently truncates a legacy non-midnight RFC3339 to the wrong UTC day; it accepts only YYYY-MM-DD or an exact UTC-midnight instant and rejects ambiguous ones loudly. Adds util unit tests. - migration 112 pins the TIMESTAMPTZ->DATE conversion to UTC explicitly via AT TIME ZONE 'UTC' (was session-timezone dependent); down migration too. - Convert remaining date-change display sites to formatDateOnly: inbox detail label (web) and mobile activity + inbox labels (were new Date()+local format). - CLI --start-date/--due-date help now says YYYY-MM-DD, not RFC3339. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
toDateOnly,
|
|
todayDateOnly,
|
|
addDaysDateOnly,
|
|
dateOnlyToUTCDate,
|
|
dateOnlyToLocalDate,
|
|
formatDateOnly,
|
|
isPastDateOnly,
|
|
} from "./date";
|
|
|
|
describe("issue date-only helpers", () => {
|
|
it("serializes a picked local day to YYYY-MM-DD with the local calendar", () => {
|
|
// A calendar picker hands back local midnight of the clicked day.
|
|
expect(toDateOnly(new Date(2026, 2, 1))).toBe("2026-03-01");
|
|
expect(toDateOnly(new Date(2026, 0, 5))).toBe("2026-01-05"); // zero-padded
|
|
});
|
|
|
|
it("formats a date-only string timezone-safely (no day shift)", () => {
|
|
// The bug: a calendar day must render as the same day in every timezone.
|
|
expect(
|
|
formatDateOnly("2026-03-01", { month: "short", day: "numeric" }, "en-US"),
|
|
).toBe("Mar 1");
|
|
expect(formatDateOnly("2026-03-01", undefined, "en-US")).toBe("Mar 1");
|
|
expect(formatDateOnly(null)).toBe("");
|
|
expect(formatDateOnly("")).toBe("");
|
|
});
|
|
|
|
it("round-trips a picked day back to the same displayed day", () => {
|
|
const picked = new Date(2026, 2, 1); // user clicks March 1 locally
|
|
const stored = toDateOnly(picked);
|
|
expect(stored).toBe("2026-03-01");
|
|
expect(formatDateOnly(stored, { month: "short", day: "numeric" }, "en-US")).toBe(
|
|
"Mar 1",
|
|
);
|
|
});
|
|
|
|
it("anchors a date-only value at UTC midnight", () => {
|
|
expect(dateOnlyToUTCDate("2026-03-01")?.toISOString()).toBe(
|
|
"2026-03-01T00:00:00.000Z",
|
|
);
|
|
expect(dateOnlyToUTCDate(null)).toBeNull();
|
|
});
|
|
|
|
it("tolerates a legacy RFC3339 instant by reading its UTC day", () => {
|
|
// Old clients stored local-midnight-as-UTC; read the stored UTC calendar day.
|
|
expect(dateOnlyToUTCDate("2026-02-28T16:00:00Z")?.toISOString()).toBe(
|
|
"2026-02-28T00:00:00.000Z",
|
|
);
|
|
});
|
|
|
|
it("builds a local-midnight Date for the picker's selected day", () => {
|
|
const d = dateOnlyToLocalDate("2026-03-01");
|
|
expect(d?.getFullYear()).toBe(2026);
|
|
expect(d?.getMonth()).toBe(2);
|
|
expect(d?.getDate()).toBe(1);
|
|
expect(dateOnlyToLocalDate(null)).toBeUndefined();
|
|
});
|
|
|
|
it("detects past calendar days relative to today", () => {
|
|
expect(isPastDateOnly(addDaysDateOnly(-1))).toBe(true);
|
|
expect(isPastDateOnly(todayDateOnly())).toBe(false);
|
|
expect(isPastDateOnly(addDaysDateOnly(1))).toBe(false);
|
|
expect(isPastDateOnly(null)).toBe(false);
|
|
});
|
|
});
|