feat: visible date range
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -53,6 +53,7 @@ import { AGENDA_VIEW_SPANS, spanToDays, type AgendaViewSpan } from '@/types/span
|
|||||||
import type { EntityObject } from '@ChronoManager/models/entity';
|
import type { EntityObject } from '@ChronoManager/models/entity';
|
||||||
import type { CollectionObject } from '@ChronoManager/models/collection';
|
import type { CollectionObject } from '@ChronoManager/models/collection';
|
||||||
import { EventObject } from '@ChronoManager/models/event';
|
import { EventObject } from '@ChronoManager/models/event';
|
||||||
|
import { daysVisibleRange, shiftLocalDays } from '@/utils/dateRanges';
|
||||||
|
|
||||||
type CalendarEntity = EntityObject;
|
type CalendarEntity = EntityObject;
|
||||||
type CalendarCollection = CollectionObject;
|
type CalendarCollection = CollectionObject;
|
||||||
@@ -60,25 +61,18 @@ type CalendarCollection = CollectionObject;
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
events: CalendarEntity[];
|
events: CalendarEntity[];
|
||||||
calendars: CalendarCollection[];
|
calendars: CalendarCollection[];
|
||||||
currentDate?: Date;
|
currentDate: Date;
|
||||||
initialSpan?: AgendaViewSpan;
|
initialSpan?: AgendaViewSpan;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const selectedSpan = ref<AgendaViewSpan>(props.initialSpan ?? '1w');
|
const selectedSpan = ref<AgendaViewSpan>(props.initialSpan ?? '1w');
|
||||||
const localDate = ref(new Date(props.currentDate ?? new Date()));
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'event-click': [event: CalendarEntity];
|
'event-click': [event: CalendarEntity];
|
||||||
'update:span': [span: AgendaViewSpan];
|
'update:span': [span: AgendaViewSpan];
|
||||||
|
'update:current-date': [date: Date];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Watch for external date changes (e.g., from mini calendar)
|
|
||||||
watch(() => props.currentDate, (newDate) => {
|
|
||||||
if (newDate) {
|
|
||||||
localDate.value = new Date(newDate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => props.initialSpan, (newSpan) => {
|
watch(() => props.initialSpan, (newSpan) => {
|
||||||
if (newSpan && newSpan !== selectedSpan.value) {
|
if (newSpan && newSpan !== selectedSpan.value) {
|
||||||
selectedSpan.value = newSpan;
|
selectedSpan.value = newSpan;
|
||||||
@@ -95,34 +89,29 @@ function getSpanDays(): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function previousPeriod() {
|
function previousPeriod() {
|
||||||
const date = new Date(localDate.value);
|
emit('update:current-date', shiftLocalDays(props.currentDate, -getSpanDays()));
|
||||||
date.setDate(date.getDate() - getSpanDays());
|
|
||||||
localDate.value = date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextPeriod() {
|
function nextPeriod() {
|
||||||
const date = new Date(localDate.value);
|
emit('update:current-date', shiftLocalDays(props.currentDate, getSpanDays()));
|
||||||
date.setDate(date.getDate() + getSpanDays());
|
|
||||||
localDate.value = date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToToday() {
|
function goToToday() {
|
||||||
localDate.value = new Date();
|
emit('update:current-date', new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateRange = computed(() => {
|
const dateRange = computed(() => {
|
||||||
const start = new Date(localDate.value);
|
const range = daysVisibleRange(props.currentDate, getSpanDays());
|
||||||
start.setHours(0, 0, 0, 0);
|
return {
|
||||||
|
start: new Date(range.startMs),
|
||||||
const end = new Date(start);
|
end: new Date(range.endMs),
|
||||||
end.setDate(end.getDate() + getSpanDays() - 1);
|
};
|
||||||
end.setHours(23, 59, 59, 999);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const dateRangeLabel = computed(() => {
|
const dateRangeLabel = computed(() => {
|
||||||
const { start, end } = dateRange.value;
|
const { start, end: exclusiveEnd } = dateRange.value;
|
||||||
|
const end = new Date(exclusiveEnd);
|
||||||
|
end.setDate(end.getDate() - 1);
|
||||||
const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
|
const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
|
||||||
|
|
||||||
if (getSpanDays() === 1) {
|
if (getSpanDays() === 1) {
|
||||||
@@ -144,7 +133,7 @@ const groupedEvents = computed(() => {
|
|||||||
const startsOn = getEventProperties(e).startsOn;
|
const startsOn = getEventProperties(e).startsOn;
|
||||||
if (typeof startsOn !== 'string') return false;
|
if (typeof startsOn !== 'string') return false;
|
||||||
const eventStart = new Date(startsOn);
|
const eventStart = new Date(startsOn);
|
||||||
return eventStart >= start && eventStart <= end;
|
return eventStart >= start && eventStart < end;
|
||||||
});
|
});
|
||||||
|
|
||||||
const sorted = filtered.sort((a, b) =>
|
const sorted = filtered.sort((a, b) =>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
@date-click="$emit('date-click', $event)"
|
@date-click="$emit('date-click', $event)"
|
||||||
@event-hover="handleEventHover"
|
@event-hover="handleEventHover"
|
||||||
@event-hover-end="hidePopup"
|
@event-hover-end="hidePopup"
|
||||||
|
@update:current-date="$emit('update:current-date', $event)"
|
||||||
/>
|
/>
|
||||||
<DaysView
|
<DaysView
|
||||||
v-else-if="view === 'days'"
|
v-else-if="view === 'days'"
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
@event-hover="handleEventHover"
|
@event-hover="handleEventHover"
|
||||||
@event-hover-end="hidePopup"
|
@event-hover-end="hidePopup"
|
||||||
@update:span="$emit('update:days-span', $event)"
|
@update:span="$emit('update:days-span', $event)"
|
||||||
|
@update:current-date="$emit('update:current-date', $event)"
|
||||||
/>
|
/>
|
||||||
<AgendaView
|
<AgendaView
|
||||||
v-else-if="view === 'agenda'"
|
v-else-if="view === 'agenda'"
|
||||||
@@ -36,21 +38,24 @@
|
|||||||
:initial-span="initialAgendaViewSpan"
|
:initial-span="initialAgendaViewSpan"
|
||||||
@event-click="$emit('event-click', $event)"
|
@event-click="$emit('event-click', $event)"
|
||||||
@update:span="$emit('update:agenda-span', $event)"
|
@update:span="$emit('update:agenda-span', $event)"
|
||||||
|
@update:current-date="$emit('update:current-date', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import MonthView from './MonthView.vue';
|
import MonthView from './MonthView.vue';
|
||||||
import DaysView from './DaysView.vue';
|
import DaysView from './DaysView.vue';
|
||||||
import AgendaView from './AgendaView.vue';
|
import AgendaView from './AgendaView.vue';
|
||||||
import EventViewerPopup from './EventViewerPopup.vue';
|
import EventViewerPopup from './EventViewerPopup.vue';
|
||||||
import type { AgendaViewSpan, DaysViewSpan } from '@/types/spans';
|
import { spanToDays, type AgendaViewSpan, type DaysViewSpan } from '@/types/spans';
|
||||||
import type { EntityObject } from '@ChronoManager/models/entity';
|
import type { EntityObject } from '@ChronoManager/models/entity';
|
||||||
import type { CollectionObject } from '@ChronoManager/models/collection';
|
import type { CollectionObject } from '@ChronoManager/models/collection';
|
||||||
|
import type { VisibleDateRange } from '@/types';
|
||||||
|
import { daysVisibleRange, monthGridVisibleRange } from '@/utils/dateRanges';
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
view: 'days' | 'month' | 'agenda';
|
view: 'days' | 'month' | 'agenda';
|
||||||
currentDate: Date;
|
currentDate: Date;
|
||||||
events: EntityObject[];
|
events: EntityObject[];
|
||||||
@@ -59,13 +64,31 @@ defineProps<{
|
|||||||
initialAgendaViewSpan?: AgendaViewSpan;
|
initialAgendaViewSpan?: AgendaViewSpan;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
const emit = defineEmits<{
|
||||||
'event-click': [event: EntityObject];
|
'event-click': [event: EntityObject];
|
||||||
'date-click': [date: Date];
|
'date-click': [date: Date];
|
||||||
'update:days-span': [span: DaysViewSpan];
|
'update:days-span': [span: DaysViewSpan];
|
||||||
'update:agenda-span': [span: AgendaViewSpan];
|
'update:agenda-span': [span: AgendaViewSpan];
|
||||||
|
'update:current-date': [date: Date];
|
||||||
|
'update:visible-range': [range: VisibleDateRange];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const visibleRange = computed(() => {
|
||||||
|
if (props.view === 'month') {
|
||||||
|
return monthGridVisibleRange(props.currentDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = props.view === 'agenda'
|
||||||
|
? props.initialAgendaViewSpan ?? '1w'
|
||||||
|
: props.initialDaysSpan ?? '7d';
|
||||||
|
|
||||||
|
return daysVisibleRange(props.currentDate, spanToDays(span));
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(visibleRange, range => {
|
||||||
|
emit('update:visible-range', range);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
// Popup state
|
// Popup state
|
||||||
const hoveredEvent = ref<EntityObject | null>(null);
|
const hoveredEvent = ref<EntityObject | null>(null);
|
||||||
const popupPosition = ref({ x: 0, y: 0 });
|
const popupPosition = ref({ x: 0, y: 0 });
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ import {
|
|||||||
getTimedEventsForDate,
|
getTimedEventsForDate,
|
||||||
type MultiDaySegment,
|
type MultiDaySegment,
|
||||||
} from '@/utils/calendarHelpers';
|
} from '@/utils/calendarHelpers';
|
||||||
|
import { shiftLocalDays } from '@/utils/dateRanges';
|
||||||
import { DAYS_VIEW_SPANS, spanToDays, type DaysViewSpan } from '@/types/spans';
|
import { DAYS_VIEW_SPANS, spanToDays, type DaysViewSpan } from '@/types/spans';
|
||||||
import type { EntityObject } from '@ChronoManager/models/entity';
|
import type { EntityObject } from '@ChronoManager/models/entity';
|
||||||
import type { CollectionObject } from '@ChronoManager/models/collection';
|
import type { CollectionObject } from '@ChronoManager/models/collection';
|
||||||
@@ -126,7 +127,6 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const selectedSpan = ref<DaysViewSpan>(props.initialSpan ?? '7d');
|
const selectedSpan = ref<DaysViewSpan>(props.initialSpan ?? '7d');
|
||||||
const daysCount = computed(() => spanToDays(selectedSpan.value));
|
const daysCount = computed(() => spanToDays(selectedSpan.value));
|
||||||
const localDate = ref(new Date(props.currentDate));
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'event-click': [event: CalendarEntity];
|
'event-click': [event: CalendarEntity];
|
||||||
@@ -134,13 +134,9 @@ const emit = defineEmits<{
|
|||||||
'event-hover-end': [];
|
'event-hover-end': [];
|
||||||
'date-click': [date: Date];
|
'date-click': [date: Date];
|
||||||
'update:span': [span: DaysViewSpan];
|
'update:span': [span: DaysViewSpan];
|
||||||
|
'update:current-date': [date: Date];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Watch for external date changes (e.g., from mini calendar)
|
|
||||||
watch(() => props.currentDate, (newDate) => {
|
|
||||||
localDate.value = new Date(newDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => props.initialSpan, (newSpan) => {
|
watch(() => props.initialSpan, (newSpan) => {
|
||||||
if (newSpan && newSpan !== selectedSpan.value) {
|
if (newSpan && newSpan !== selectedSpan.value) {
|
||||||
selectedSpan.value = newSpan;
|
selectedSpan.value = newSpan;
|
||||||
@@ -153,19 +149,15 @@ function setSpan(span: DaysViewSpan) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function previousPeriod() {
|
function previousPeriod() {
|
||||||
const date = new Date(localDate.value);
|
emit('update:current-date', shiftLocalDays(props.currentDate, -daysCount.value));
|
||||||
date.setDate(date.getDate() - daysCount.value);
|
|
||||||
localDate.value = date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextPeriod() {
|
function nextPeriod() {
|
||||||
const date = new Date(localDate.value);
|
emit('update:current-date', shiftLocalDays(props.currentDate, daysCount.value));
|
||||||
date.setDate(date.getDate() + daysCount.value);
|
|
||||||
localDate.value = date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToToday() {
|
function goToToday() {
|
||||||
localDate.value = new Date();
|
emit('update:current-date', new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateRangeLabel = computed(() => {
|
const dateRangeLabel = computed(() => {
|
||||||
@@ -189,7 +181,7 @@ const dateRangeLabel = computed(() => {
|
|||||||
|
|
||||||
const visibleDates = computed(() => {
|
const visibleDates = computed(() => {
|
||||||
const dates: Date[] = [];
|
const dates: Date[] = [];
|
||||||
const start = new Date(localDate.value);
|
const start = new Date(props.currentDate);
|
||||||
|
|
||||||
for (let i = 0; i < daysCount.value; i++) {
|
for (let i = 0; i < daysCount.value; i++) {
|
||||||
dates.push(new Date(start));
|
dates.push(new Date(start));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onMounted, onUnmounted } from 'vue';
|
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||||
import {
|
import {
|
||||||
startOfDay,
|
startOfDay,
|
||||||
daysDiff,
|
daysDiff,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getSingleDayEventsForDate,
|
getSingleDayEventsForDate,
|
||||||
type MultiDaySegment,
|
type MultiDaySegment,
|
||||||
} from '@/utils/calendarHelpers';
|
} from '@/utils/calendarHelpers';
|
||||||
|
import { shiftLocalMonths } from '@/utils/dateRanges';
|
||||||
|
|
||||||
const EVENT_HEIGHT = 22; // Height of each event row in pixels
|
const EVENT_HEIGHT = 22; // Height of each event row in pixels
|
||||||
const MAX_VISIBLE_EVENTS = 3; // Maximum event rows to show before "+N more"
|
const MAX_VISIBLE_EVENTS = 3; // Maximum event rows to show before "+N more"
|
||||||
@@ -24,42 +25,31 @@ const props = defineProps<{
|
|||||||
calendars: any[];
|
calendars: any[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
const emit = defineEmits<{
|
||||||
'event-click': [event: any];
|
'event-click': [event: any];
|
||||||
'date-click': [date: Date];
|
'date-click': [date: Date];
|
||||||
'event-hover': [data: { event: MouseEvent; entity: any }];
|
'event-hover': [data: { event: MouseEvent; entity: any }];
|
||||||
'event-hover-end': [];
|
'event-hover-end': [];
|
||||||
|
'update:current-date': [date: Date];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
|
||||||
// Local date for navigation
|
|
||||||
const localDate = ref(new Date(props.currentDate));
|
|
||||||
|
|
||||||
// Watch for external date changes (e.g., from mini calendar)
|
|
||||||
watch(() => props.currentDate, (newDate) => {
|
|
||||||
localDate.value = new Date(newDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigation functions
|
// Navigation functions
|
||||||
function previousMonth() {
|
function previousMonth() {
|
||||||
const date = new Date(localDate.value);
|
emit('update:current-date', shiftLocalMonths(props.currentDate, -1));
|
||||||
date.setMonth(date.getMonth() - 1);
|
|
||||||
localDate.value = date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextMonth() {
|
function nextMonth() {
|
||||||
const date = new Date(localDate.value);
|
emit('update:current-date', shiftLocalMonths(props.currentDate, 1));
|
||||||
date.setMonth(date.getMonth() + 1);
|
|
||||||
localDate.value = date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToToday() {
|
function goToToday() {
|
||||||
localDate.value = new Date();
|
emit('update:current-date', new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthLabel = computed(() => {
|
const monthLabel = computed(() => {
|
||||||
return localDate.value.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
return props.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track container size for dynamic calculations
|
// Track container size for dynamic calculations
|
||||||
@@ -83,8 +73,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
// Calculate how many weeks are needed for this month
|
// Calculate how many weeks are needed for this month
|
||||||
const weeksNeeded = computed(() => {
|
const weeksNeeded = computed(() => {
|
||||||
const year = localDate.value.getFullYear();
|
const year = props.currentDate.getFullYear();
|
||||||
const month = localDate.value.getMonth();
|
const month = props.currentDate.getMonth();
|
||||||
const firstDay = new Date(year, month, 1);
|
const firstDay = new Date(year, month, 1);
|
||||||
const lastDay = new Date(year, month + 1, 0);
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
|
||||||
@@ -97,8 +87,8 @@ const weeksNeeded = computed(() => {
|
|||||||
|
|
||||||
// Build weeks with multiday event segments
|
// Build weeks with multiday event segments
|
||||||
const weeks = computed<WeekData[]>(() => {
|
const weeks = computed<WeekData[]>(() => {
|
||||||
const year = localDate.value.getFullYear();
|
const year = props.currentDate.getFullYear();
|
||||||
const month = localDate.value.getMonth();
|
const month = props.currentDate.getMonth();
|
||||||
const firstDay = new Date(year, month, 1);
|
const firstDay = new Date(year, month, 1);
|
||||||
const gridStart = new Date(firstDay);
|
const gridStart = new Date(firstDay);
|
||||||
gridStart.setDate(gridStart.getDate() - gridStart.getDay());
|
gridStart.setDate(gridStart.getDate() - gridStart.getDay());
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const {
|
|||||||
const {
|
const {
|
||||||
initialize,
|
initialize,
|
||||||
setViewMode,
|
setViewMode,
|
||||||
|
setCurrentDate,
|
||||||
selectCalendar,
|
selectCalendar,
|
||||||
openEditCalendar,
|
openEditCalendar,
|
||||||
selectTaskList,
|
selectTaskList,
|
||||||
@@ -89,7 +90,11 @@ const {
|
|||||||
closeImport,
|
closeImport,
|
||||||
} = chronoUiStore;
|
} = chronoUiStore;
|
||||||
|
|
||||||
const { toggleCalendarVisibility, toggleTaskComplete } = chronoOperationsStore;
|
const {
|
||||||
|
setVisibleRange,
|
||||||
|
toggleCalendarVisibility,
|
||||||
|
toggleTaskComplete,
|
||||||
|
} = chronoOperationsStore;
|
||||||
|
|
||||||
function setCalendarView(view: CalendarViewType) {
|
function setCalendarView(view: CalendarViewType) {
|
||||||
chronoSettingsStore.calendarView = view;
|
chronoSettingsStore.calendarView = view;
|
||||||
@@ -278,6 +283,8 @@ watch(() => route.fullPath, () => {
|
|||||||
:initial-agenda-view-span="agendaViewSpan"
|
:initial-agenda-view-span="agendaViewSpan"
|
||||||
@event-click="editEvent"
|
@event-click="editEvent"
|
||||||
@date-click="createEventFromDate"
|
@date-click="createEventFromDate"
|
||||||
|
@update:current-date="setCurrentDate"
|
||||||
|
@update:visible-range="setVisibleRange"
|
||||||
@update:days-span="setDaysViewSpan"
|
@update:days-span="setDaysViewSpan"
|
||||||
@update:agenda-span="setAgendaViewSpan"
|
@update:agenda-span="setAgendaViewSpan"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref, shallowRef } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { CollectionObject } from '@ChronoManager/models/collection'
|
import { CollectionObject } from '@ChronoManager/models/collection'
|
||||||
import { EntityObject } from '@ChronoManager/models/entity'
|
import { EntityObject } from '@ChronoManager/models/entity'
|
||||||
@@ -9,6 +9,7 @@ import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore'
|
|||||||
import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore'
|
import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore'
|
||||||
import { useServicesStore } from '@ChronoManager/stores/servicesStore'
|
import { useServicesStore } from '@ChronoManager/stores/servicesStore'
|
||||||
import { useImportStore } from '@ChronoManager/stores/importStore'
|
import { useImportStore } from '@ChronoManager/stores/importStore'
|
||||||
|
import type { VisibleDateRange } from '@/types'
|
||||||
|
|
||||||
type ChronoEntityProperties = EventObject | TaskObject
|
type ChronoEntityProperties = EventObject | TaskObject
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export const useChronoOperationsStore = defineStore('chronoOperationsStore', ()
|
|||||||
const importStore = useImportStore()
|
const importStore = useImportStore()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const visibleRange = shallowRef<VisibleDateRange | null>(null)
|
||||||
|
|
||||||
const collections = computed(() => collectionsStore.collections)
|
const collections = computed(() => collectionsStore.collections)
|
||||||
const entities = computed(() => entitiesStore.entities)
|
const entities = computed(() => entitiesStore.entities)
|
||||||
@@ -43,6 +45,17 @@ export const useChronoOperationsStore = defineStore('chronoOperationsStore', ()
|
|||||||
})
|
})
|
||||||
const filteredTasks = computed(() => tasks.value)
|
const filteredTasks = computed(() => tasks.value)
|
||||||
|
|
||||||
|
function setVisibleRange(range: VisibleDateRange) {
|
||||||
|
if (
|
||||||
|
visibleRange.value?.startMs === range.startMs
|
||||||
|
&& visibleRange.value.endMs === range.endMs
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleRange.value = { ...range }
|
||||||
|
}
|
||||||
|
|
||||||
function prepareEntityProperties(entity: EntityObject): ChronoEntityProperties {
|
function prepareEntityProperties(entity: EntityObject): ChronoEntityProperties {
|
||||||
const properties = entity.properties as ChronoEntityProperties
|
const properties = entity.properties as ChronoEntityProperties
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
@@ -154,6 +167,7 @@ export const useChronoOperationsStore = defineStore('chronoOperationsStore', ()
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
|
visibleRange,
|
||||||
collections,
|
collections,
|
||||||
entities,
|
entities,
|
||||||
calendars,
|
calendars,
|
||||||
@@ -162,6 +176,7 @@ export const useChronoOperationsStore = defineStore('chronoOperationsStore', ()
|
|||||||
tasks,
|
tasks,
|
||||||
filteredEvents,
|
filteredEvents,
|
||||||
filteredTasks,
|
filteredTasks,
|
||||||
|
setVisibleRange,
|
||||||
saveEntity,
|
saveEntity,
|
||||||
deleteEntity,
|
deleteEntity,
|
||||||
toggleCalendarVisibility,
|
toggleCalendarVisibility,
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export const useChronoUiStore = defineStore('chronoUiStore', () => {
|
|||||||
viewMode.value = mode
|
viewMode.value = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCurrentDate(date: Date) {
|
||||||
|
currentDate.value = new Date(date)
|
||||||
|
}
|
||||||
|
|
||||||
function selectCalendar(calendar: CollectionObject) {
|
function selectCalendar(calendar: CollectionObject) {
|
||||||
selectedCollection.value = calendar
|
selectedCollection.value = calendar
|
||||||
}
|
}
|
||||||
@@ -214,6 +218,7 @@ export const useChronoUiStore = defineStore('chronoUiStore', () => {
|
|||||||
collectionEditorType,
|
collectionEditorType,
|
||||||
isTaskView,
|
isTaskView,
|
||||||
setViewMode,
|
setViewMode,
|
||||||
|
setCurrentDate,
|
||||||
selectCalendar,
|
selectCalendar,
|
||||||
selectTaskList,
|
selectTaskList,
|
||||||
openCreateCalendar,
|
openCreateCalendar,
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
// Local UI-specific types
|
// Local UI-specific types
|
||||||
export type CalendarView = 'days' | 'month' | 'agenda';
|
export type CalendarView = 'days' | 'month' | 'agenda';
|
||||||
|
|
||||||
|
/** Half-open local calendar range: [startMs, endMs). */
|
||||||
|
export interface VisibleDateRange {
|
||||||
|
startMs: number;
|
||||||
|
endMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ViewState {
|
export interface ViewState {
|
||||||
currentView: CalendarView;
|
currentView: CalendarView;
|
||||||
currentDate: Date;
|
currentDate: Date;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { VisibleDateRange } from '../types'
|
||||||
|
|
||||||
|
export function startOfLocalDay(date: Date): Date {
|
||||||
|
const start = new Date(date)
|
||||||
|
start.setHours(0, 0, 0, 0)
|
||||||
|
return start
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shiftLocalDays(date: Date, days: number): Date {
|
||||||
|
const shifted = new Date(date)
|
||||||
|
shifted.setDate(shifted.getDate() + days)
|
||||||
|
return shifted
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shiftLocalMonths(date: Date, months: number): Date {
|
||||||
|
const shifted = new Date(date)
|
||||||
|
const targetDay = shifted.getDate()
|
||||||
|
|
||||||
|
shifted.setDate(1)
|
||||||
|
shifted.setMonth(shifted.getMonth() + months)
|
||||||
|
|
||||||
|
const lastDayOfTargetMonth = new Date(
|
||||||
|
shifted.getFullYear(),
|
||||||
|
shifted.getMonth() + 1,
|
||||||
|
0,
|
||||||
|
).getDate()
|
||||||
|
shifted.setDate(Math.min(targetDay, lastDayOfTargetMonth))
|
||||||
|
|
||||||
|
return shifted
|
||||||
|
}
|
||||||
|
|
||||||
|
export function daysVisibleRange(date: Date, days: number): VisibleDateRange {
|
||||||
|
const start = startOfLocalDay(date)
|
||||||
|
const end = shiftLocalDays(start, Math.max(1, days))
|
||||||
|
|
||||||
|
return {
|
||||||
|
startMs: start.getTime(),
|
||||||
|
endMs: end.getTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function monthGridVisibleRange(date: Date): VisibleDateRange {
|
||||||
|
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1)
|
||||||
|
const gridStart = new Date(firstDay)
|
||||||
|
gridStart.setDate(gridStart.getDate() - gridStart.getDay())
|
||||||
|
|
||||||
|
const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0)
|
||||||
|
const weeks = Math.ceil((firstDay.getDay() + lastDay.getDate()) / 7)
|
||||||
|
const gridEnd = shiftLocalDays(gridStart, weeks * 7)
|
||||||
|
|
||||||
|
return {
|
||||||
|
startMs: gridStart.getTime(),
|
||||||
|
endMs: gridEnd.getTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
daysVisibleRange,
|
||||||
|
monthGridVisibleRange,
|
||||||
|
shiftLocalDays,
|
||||||
|
shiftLocalMonths,
|
||||||
|
} from '../../../src/utils/dateRanges'
|
||||||
|
|
||||||
|
describe('calendar visible ranges', () => {
|
||||||
|
it('uses a half-open range for a day span', () => {
|
||||||
|
const range = daysVisibleRange(new Date(2026, 5, 15, 13, 45), 7)
|
||||||
|
const start = new Date(range.startMs)
|
||||||
|
const end = new Date(range.endMs)
|
||||||
|
|
||||||
|
expect([start.getFullYear(), start.getMonth(), start.getDate(), start.getHours()])
|
||||||
|
.toEqual([2026, 5, 15, 0])
|
||||||
|
expect([end.getFullYear(), end.getMonth(), end.getDate(), end.getHours()])
|
||||||
|
.toEqual([2026, 5, 22, 0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses calendar-day arithmetic across a daylight-saving boundary', () => {
|
||||||
|
const range = daysVisibleRange(new Date(2026, 2, 8, 12), 1)
|
||||||
|
const start = new Date(range.startMs)
|
||||||
|
const end = new Date(range.endMs)
|
||||||
|
|
||||||
|
expect(start.getHours()).toBe(0)
|
||||||
|
expect(end.getHours()).toBe(0)
|
||||||
|
expect(end.getDate()).toBe(start.getDate() + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('includes every leading and trailing day rendered by a month grid', () => {
|
||||||
|
const range = monthGridVisibleRange(new Date(2026, 5, 15))
|
||||||
|
const start = new Date(range.startMs)
|
||||||
|
const end = new Date(range.endMs)
|
||||||
|
|
||||||
|
expect([start.getFullYear(), start.getMonth(), start.getDate()]).toEqual([2026, 4, 31])
|
||||||
|
expect([end.getFullYear(), end.getMonth(), end.getDate()]).toEqual([2026, 6, 5])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calculates a leap-year February grid', () => {
|
||||||
|
const range = monthGridVisibleRange(new Date(2024, 1, 29))
|
||||||
|
const start = new Date(range.startMs)
|
||||||
|
const end = new Date(range.endMs)
|
||||||
|
|
||||||
|
expect([start.getFullYear(), start.getMonth(), start.getDate()]).toEqual([2024, 0, 28])
|
||||||
|
expect([end.getFullYear(), end.getMonth(), end.getDate()]).toEqual([2024, 2, 3])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('calendar navigation', () => {
|
||||||
|
it('clamps month navigation instead of skipping a shorter month', () => {
|
||||||
|
const shifted = shiftLocalMonths(new Date(2026, 0, 31, 9), 1)
|
||||||
|
|
||||||
|
expect([shifted.getFullYear(), shifted.getMonth(), shifted.getDate(), shifted.getHours()])
|
||||||
|
.toEqual([2026, 1, 28, 9])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses calendar-day navigation', () => {
|
||||||
|
const shifted = shiftLocalDays(new Date(2026, 5, 30, 9), 1)
|
||||||
|
|
||||||
|
expect([shifted.getFullYear(), shifted.getMonth(), shifted.getDate(), shifted.getHours()])
|
||||||
|
.toEqual([2026, 6, 1, 9])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user