Merge branch 'develop'
This commit is contained in:
commit
b00f8c838e
@ -8,7 +8,6 @@ import { useCalendar } from "@/contexts/CalendarContext";
|
||||
import { useCalendarEvents } from "@/hooks/calendar/useCalendarEvents";
|
||||
import {
|
||||
calendarService,
|
||||
formatDateKey,
|
||||
type CalendarEvent,
|
||||
type CalendarEventDraft,
|
||||
toCalendarEventDraft,
|
||||
@ -17,6 +16,11 @@ import TaskLinkPill from "@/components/tasks/TaskLinkPill";
|
||||
import CalendarNotificationSettingsModal from "@/components/CalendarNotificationSettingsModal";
|
||||
import { ViewportAwareMenu } from "@/components/shared/ViewportAwareMenu";
|
||||
import { DEFAULT_EVENT_COLOR, resolveEventColor } from "@/lib/calendar-event-colors";
|
||||
import {
|
||||
calendarEventOverlapsRange,
|
||||
formatLocalDateKey,
|
||||
getCalendarEventDateKeys,
|
||||
} from "@/lib/calendar/eventRange";
|
||||
import {
|
||||
buildCalendarOptions,
|
||||
canOpenParticipantsModal,
|
||||
@ -247,18 +251,8 @@ export const CalendarCard = memo(function CalendarCard({
|
||||
const eventDays = useMemo(() => {
|
||||
const days = new Set<string>();
|
||||
events.forEach((ev) => {
|
||||
if (!ev.start) return;
|
||||
const eventStart = new Date(ev.start);
|
||||
if (Number.isNaN(eventStart.getTime())) return;
|
||||
|
||||
const eventEnd = ev.end ? new Date(ev.end) : eventStart;
|
||||
eventStart.setHours(0, 0, 0, 0);
|
||||
eventEnd.setHours(0, 0, 0, 0);
|
||||
|
||||
const currentDay = new Date(eventStart);
|
||||
while (currentDay <= eventEnd) {
|
||||
days.add(formatDateKey(currentDay));
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
for (const dateKey of getCalendarEventDateKeys(ev)) {
|
||||
days.add(dateKey);
|
||||
}
|
||||
});
|
||||
return days;
|
||||
@ -277,7 +271,7 @@ export const CalendarCard = memo(function CalendarCard({
|
||||
const date = new Date(gridStart);
|
||||
date.setDate(gridStart.getDate() + i);
|
||||
|
||||
const key = formatDateKey(date);
|
||||
const key = formatLocalDateKey(date);
|
||||
return {
|
||||
key,
|
||||
day: date.getDate(),
|
||||
@ -300,12 +294,7 @@ export const CalendarCard = memo(function CalendarCard({
|
||||
weekEndExclusive.setDate(weekEndExclusive.getDate() + 7);
|
||||
|
||||
return [...events]
|
||||
.filter((ev) => ev.start)
|
||||
.filter((ev) => {
|
||||
const start = new Date(ev.start || "");
|
||||
if (Number.isNaN(start.getTime())) return false;
|
||||
return start >= weekStart && start < weekEndExclusive;
|
||||
})
|
||||
.filter((ev) => calendarEventOverlapsRange(ev, weekStart, weekEndExclusive))
|
||||
.sort((a, b) => new Date(a.start || "").getTime() - new Date(b.start || "").getTime())
|
||||
.slice(0, 12);
|
||||
}, [events]);
|
||||
@ -315,16 +304,10 @@ export const CalendarCard = memo(function CalendarCard({
|
||||
const monthStart = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
|
||||
monthStart.setHours(0, 0, 0, 0);
|
||||
|
||||
const monthEnd = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0);
|
||||
monthEnd.setHours(23, 59, 59, 999);
|
||||
const monthEndExclusive = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 1);
|
||||
|
||||
return [...events]
|
||||
.filter((ev) => ev.start)
|
||||
.filter((ev) => {
|
||||
const start = new Date(ev.start || "");
|
||||
if (Number.isNaN(start.getTime())) return false;
|
||||
return start >= monthStart && start <= monthEnd;
|
||||
})
|
||||
.filter((ev) => calendarEventOverlapsRange(ev, monthStart, monthEndExclusive))
|
||||
.sort((a, b) => new Date(a.start || "").getTime() - new Date(b.start || "").getTime())
|
||||
.slice(0, 30);
|
||||
}, [events, monthDate]);
|
||||
|
||||
75
frontend/src/lib/calendar/eventRange.test.js
Normal file
75
frontend/src/lib/calendar/eventRange.test.js
Normal file
@ -0,0 +1,75 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
calendarEventOverlapsRange,
|
||||
getCalendarEventDateKeys,
|
||||
} from "./eventRange.ts";
|
||||
|
||||
test("event crossing the week start is included in the current week", () => {
|
||||
const event = {
|
||||
start: new Date(2026, 7, 20, 10, 0),
|
||||
end: new Date(2026, 8, 1, 10, 0),
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
calendarEventOverlapsRange(
|
||||
event,
|
||||
new Date(2026, 7, 31, 0, 0),
|
||||
new Date(2026, 8, 7, 0, 0),
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("event crossing the month start is included in the selected month", () => {
|
||||
const event = {
|
||||
start: new Date(2026, 7, 20, 10, 0),
|
||||
end: new Date(2026, 8, 1, 10, 0),
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
calendarEventOverlapsRange(
|
||||
event,
|
||||
new Date(2026, 8, 1, 0, 0),
|
||||
new Date(2026, 9, 1, 0, 0),
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("event ending exactly at the range start does not overlap the range", () => {
|
||||
const event = {
|
||||
start: new Date(2026, 7, 20, 10, 0),
|
||||
end: new Date(2026, 8, 1, 0, 0),
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
calendarEventOverlapsRange(
|
||||
event,
|
||||
new Date(2026, 8, 1, 0, 0),
|
||||
new Date(2026, 9, 1, 0, 0),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("calendar day markers exclude an end boundary at midnight", () => {
|
||||
assert.deepEqual(
|
||||
getCalendarEventDateKeys({
|
||||
start: new Date(2026, 7, 31, 10, 0),
|
||||
end: new Date(2026, 8, 1, 0, 0),
|
||||
}),
|
||||
["2026-08-31"],
|
||||
);
|
||||
});
|
||||
|
||||
test("calendar day markers include a day when the event continues after midnight", () => {
|
||||
assert.deepEqual(
|
||||
getCalendarEventDateKeys({
|
||||
start: new Date(2026, 7, 31, 10, 0),
|
||||
end: new Date(2026, 8, 1, 10, 0),
|
||||
}),
|
||||
["2026-08-31", "2026-09-01"],
|
||||
);
|
||||
});
|
||||
70
frontend/src/lib/calendar/eventRange.ts
Normal file
70
frontend/src/lib/calendar/eventRange.ts
Normal file
@ -0,0 +1,70 @@
|
||||
export type CalendarEventInterval = {
|
||||
start?: string | Date | null;
|
||||
end?: string | Date | null;
|
||||
};
|
||||
|
||||
const pad = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
export const formatLocalDateKey = (date: Date): string =>
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
|
||||
const parseEventDate = (value: string | Date | null | undefined): Date | null => {
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
};
|
||||
|
||||
/**
|
||||
* Проверяет пересечение события с полуоткрытым диапазоном [start, end).
|
||||
* Для событий без корректного окончания используется дата начала как момент времени.
|
||||
*/
|
||||
export function calendarEventOverlapsRange(
|
||||
event: CalendarEventInterval,
|
||||
rangeStart: Date,
|
||||
rangeEndExclusive: Date,
|
||||
): boolean {
|
||||
const eventStart = parseEventDate(event.start);
|
||||
if (
|
||||
!eventStart
|
||||
|| Number.isNaN(rangeStart.getTime())
|
||||
|| Number.isNaN(rangeEndExclusive.getTime())
|
||||
|| rangeEndExclusive <= rangeStart
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const eventEnd = parseEventDate(event.end);
|
||||
if (!eventEnd || eventEnd <= eventStart) {
|
||||
return eventStart >= rangeStart && eventStart < rangeEndExclusive;
|
||||
}
|
||||
|
||||
return eventStart < rangeEndExclusive && eventEnd > rangeStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает локальные календарные дни, затронутые событием.
|
||||
* Окончание события считается исключительной границей: событие до 00:00
|
||||
* следующего дня не создаёт отметку на следующем дне.
|
||||
*/
|
||||
export function getCalendarEventDateKeys(event: CalendarEventInterval): string[] {
|
||||
const eventStart = parseEventDate(event.start);
|
||||
if (!eventStart) return [];
|
||||
|
||||
const parsedEnd = parseEventDate(event.end);
|
||||
const inclusiveEnd = parsedEnd && parsedEnd > eventStart
|
||||
? new Date(parsedEnd.getTime() - 1)
|
||||
: eventStart;
|
||||
|
||||
const currentDay = new Date(eventStart);
|
||||
currentDay.setHours(0, 0, 0, 0);
|
||||
|
||||
const lastDay = new Date(inclusiveEnd);
|
||||
lastDay.setHours(0, 0, 0, 0);
|
||||
|
||||
const keys: string[] = [];
|
||||
while (currentDay <= lastDay) {
|
||||
keys.push(formatLocalDateKey(currentDay));
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user