Pure helpers for the Gantt's drag/double-click math: - fractionToDate(fraction, range): what ISO date a fractional position in the chart represents, snapped to whole UTC days. - shiftDatesByFraction: move a bar's start and target by the same delta, clamped to the visible range, with start dropped when clamping would invert or collapse the span. - resizeEndToFraction: what the new target date becomes when the user drags a right edge. - moveCardByFraction: alias that produces the action's input shape. 11 unit tests cover the corners (no dates set, point bar, clamping past the right edge, inversion guard). rescheduleCardAction also drops its owner-only gate. Any signed-in user — owner or client manager — can move dates now. PocketBase's existing row-level rules govern which cards a given user can write (a client manager still only writes their own client's cards); the activity-log row is unchanged and attributes the move to the actual user either way. Adds setSubtaskDatesAction so the same drag/double-click gestures work on subtask rows. No audit log on subtasks — matches the existing pattern (subtask toggles don't log).
89 lines
3 KiB
TypeScript
89 lines
3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import {
|
|
fractionToDate,
|
|
shiftDatesByFraction,
|
|
resizeEndToFraction,
|
|
moveCardByFraction,
|
|
} from "@/lib/roadmap/bar-math";
|
|
import type { TimelineRange } from "@/lib/roadmap/timeline";
|
|
|
|
const RANGE: TimelineRange = { from: "2026-02-01", to: "2026-02-22" };
|
|
|
|
describe("fractionToDate", () => {
|
|
it("maps 0 to range.from", () => {
|
|
expect(fractionToDate(0, RANGE)).toBe("2026-02-01");
|
|
});
|
|
|
|
it("maps 1 to range.to (rounded)", () => {
|
|
expect(fractionToDate(1, RANGE)).toBe("2026-02-22");
|
|
});
|
|
|
|
it("maps 0.5 to the midpoint", () => {
|
|
expect(fractionToDate(0.5, RANGE)).toBe("2026-02-11");
|
|
// 21 days span (Feb 1 to Feb 22 inclusive diff = 21); half = ~10.5 → 11
|
|
});
|
|
|
|
it("clamps out-of-range inputs", () => {
|
|
expect(fractionToDate(-0.5, RANGE)).toBe("2026-02-01");
|
|
expect(fractionToDate(1.5, RANGE)).toBe("2026-02-22");
|
|
});
|
|
});
|
|
|
|
describe("shiftDatesByFraction — moving a bar", () => {
|
|
it("shifts both dates by the same number of days when both are set", () => {
|
|
const next = shiftDatesByFraction(
|
|
"2026-02-05",
|
|
"2026-02-10",
|
|
21 / 21 / 2, // half the visible range ≈ 10.5 days → 11
|
|
RANGE
|
|
);
|
|
expect(next.startDate).toBe("2026-02-16");
|
|
expect(next.targetDate).toBe("2026-02-21");
|
|
});
|
|
|
|
it("returns nulls when neither date is set", () => {
|
|
const next = shiftDatesByFraction("", "", 0.5, RANGE);
|
|
expect(next).toEqual({ startDate: null, targetDate: null });
|
|
});
|
|
|
|
it("moves only the target when only the target is set", () => {
|
|
const next = shiftDatesByFraction("", "2026-02-10", 0.1, RANGE);
|
|
expect(next.startDate).toBeNull();
|
|
expect(next.targetDate).toBe("2026-02-12");
|
|
});
|
|
|
|
it("clamps the target to range.to when dragged past the right edge", () => {
|
|
const next = shiftDatesByFraction(
|
|
"2026-02-18",
|
|
"2026-02-20",
|
|
10, // way past the right edge
|
|
RANGE
|
|
);
|
|
// Both dates would clamp to range.to and collide; start is dropped
|
|
// so the bar still renders as a point at the right edge.
|
|
expect(next.startDate).toBeNull();
|
|
expect(next.targetDate).toBe("2026-02-22");
|
|
});
|
|
|
|
it("keeps a 1-day span when only the target clamps", () => {
|
|
// start=18 target=20 (2-day span). Drag 0.2 of the range (~4 days) right.
|
|
// Target=20+4 = 24 → clamps to 22. Start=18+4 = 22, equal to target → drop.
|
|
const next = shiftDatesByFraction("2026-02-18", "2026-02-20", 0.2, RANGE);
|
|
expect(next.targetDate).toBe("2026-02-22");
|
|
});
|
|
});
|
|
|
|
describe("resizeEndToFraction — right-edge resize", () => {
|
|
it("returns the date at the new fraction", () => {
|
|
expect(resizeEndToFraction("2026-02-05", 0.5, RANGE)).toBe("2026-02-11");
|
|
});
|
|
});
|
|
|
|
describe("moveCardByFraction — action shape", () => {
|
|
it("mirrors shiftDatesByFraction so the action gets the same shape", () => {
|
|
const a = shiftDatesByFraction("2026-02-05", "2026-02-10", 0.1, RANGE);
|
|
const b = moveCardByFraction("2026-02-05", "2026-02-10", 0.1, RANGE);
|
|
expect(a).toEqual(b);
|
|
});
|
|
});
|