refactor: code clean up
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
+142
-233
@@ -1,26 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { layoutMultiDayLanes, type MultiDaySegment } from '@/utils/multiDayLanes';
|
||||
import { shiftLocalDays, startOfLocalDay } from '@/utils/date';
|
||||
import { DAYS_VIEW_SPANS, spanToDays, type DaysViewSpan } from '@/types/spans';
|
||||
import type { EntityObject } from '@ChronoManager/models/entity';
|
||||
import type { CollectionObject } from '@ChronoManager/models/collection';
|
||||
import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
|
||||
import type { CalendarInstance } from '@/types/instance';
|
||||
import ViewControls from './ViewControls.vue';
|
||||
import { formatEpochTime, formatHour, formatWeekDay } from '@/utils/format';
|
||||
|
||||
const ALL_DAY_EVENT_HEIGHT = 24;
|
||||
|
||||
const props = defineProps<{
|
||||
currentDate: Date;
|
||||
calendars: CollectionObject[];
|
||||
initialSpan?: DaysViewSpan;
|
||||
}>();
|
||||
|
||||
const instancesStore = useChronoInstancesStore();
|
||||
|
||||
const selectedSpan = ref<DaysViewSpan>(props.initialSpan ?? '7d');
|
||||
const daysCount = computed(() => spanToDays(selectedSpan.value));
|
||||
|
||||
const emit = defineEmits<{
|
||||
'event-click': [event: EntityObject];
|
||||
'event-hover': [data: { event: MouseEvent; entity: EntityObject }];
|
||||
'event-hover-end': [];
|
||||
'date-click': [date: Date];
|
||||
'update:span': [span: DaysViewSpan];
|
||||
'update:current-date': [date: Date];
|
||||
}>();
|
||||
|
||||
watch(() => props.initialSpan, (newSpan) => {
|
||||
if (newSpan && newSpan !== selectedSpan.value) {
|
||||
selectedSpan.value = newSpan;
|
||||
}
|
||||
});
|
||||
|
||||
function setSpan(span: DaysViewSpan) {
|
||||
selectedSpan.value = span;
|
||||
emit('update:span', span);
|
||||
}
|
||||
|
||||
const visibleDates = computed(() => {
|
||||
const dates: Date[] = [];
|
||||
const start = new Date(props.currentDate);
|
||||
|
||||
for (let i = 0; i < daysCount.value; i++) {
|
||||
dates.push(new Date(start));
|
||||
start.setDate(start.getDate() + 1);
|
||||
}
|
||||
|
||||
return dates;
|
||||
});
|
||||
|
||||
// Compute all-day/multi-day event segments
|
||||
const allDaySegments = computed(() => {
|
||||
if (visibleDates.value.length === 0) {
|
||||
return { segments: [], laneCount: 0 };
|
||||
}
|
||||
|
||||
const rangeStart = startOfLocalDay(visibleDates.value[0]);
|
||||
const rangeEnd = startOfLocalDay(visibleDates.value[visibleDates.value.length - 1]);
|
||||
|
||||
const getColumnIndex = (date: Date): number => {
|
||||
const targetDay = startOfLocalDay(date);
|
||||
return visibleDates.value.findIndex(d => startOfLocalDay(d).getTime() === targetDay.getTime());
|
||||
};
|
||||
|
||||
const instances = instancesStore.instancesForRange({
|
||||
start: rangeStart.getTime(),
|
||||
end: shiftLocalDays(rangeEnd, 1).getTime(),
|
||||
});
|
||||
|
||||
return layoutMultiDayLanes(instances, rangeStart, rangeEnd, daysCount.value, getColumnIndex);
|
||||
});
|
||||
|
||||
function isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
}
|
||||
|
||||
function getTimedEvents(date: Date): CalendarInstance[] {
|
||||
return instancesStore.instancesForDay(date).filter(instance => !instance.timeless);
|
||||
}
|
||||
|
||||
function getEventColor(instance: CalendarInstance): string {
|
||||
if (instance.color) return instance.color;
|
||||
const calendar = props.calendars.find(cal => cal.identifier === instance.entity.collection);
|
||||
return calendar?.properties?.color || '#1976D2';
|
||||
}
|
||||
|
||||
function getAllDayEventStyle(segment: MultiDaySegment) {
|
||||
return {
|
||||
backgroundColor: getEventColor(segment.instance),
|
||||
left: `calc(${segment.startCol} / ${daysCount.value} * 100% + 4px)`,
|
||||
width: `calc(${segment.span} / ${daysCount.value} * 100% - 8px)`,
|
||||
top: `${segment.lane * ALL_DAY_EVENT_HEIGHT}px`,
|
||||
};
|
||||
}
|
||||
|
||||
function getEventStyle(instance: CalendarInstance) {
|
||||
const startTime = new Date(instance.start);
|
||||
const start = startTime.getHours() * 60 + startTime.getMinutes();
|
||||
const duration = instance.duration / (1000 * 60);
|
||||
|
||||
return {
|
||||
top: `${(start / 1440) * 100}%`,
|
||||
height: `${(duration / 1440) * 100}%`,
|
||||
backgroundColor: getEventColor(instance),
|
||||
};
|
||||
}
|
||||
|
||||
function handleDayClick(event: MouseEvent, day: Date) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const clickY = event.clientY - rect.top;
|
||||
|
||||
// Calculate the hour based on click position (60px per hour)
|
||||
const totalMinutes = Math.floor((clickY / 60) * 60);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = Math.round((totalMinutes % 60) / 15) * 15; // Round to nearest 15 min
|
||||
|
||||
// Create a new date with the clicked time
|
||||
const clickedDate = new Date(day);
|
||||
clickedDate.setHours(hours, minutes, 0, 0);
|
||||
|
||||
emit('date-click', clickedDate);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="days-view">
|
||||
<div class="view-controls">
|
||||
<div class="view-navigation">
|
||||
<v-btn size="x-small" variant="text" icon="mdi-chevron-left" @click="previousPeriod" />
|
||||
<v-btn size="x-small" variant="tonal" @click="goToToday">Today</v-btn>
|
||||
<v-btn size="x-small" variant="text" icon="mdi-chevron-right" @click="nextPeriod" />
|
||||
<span class="current-period">{{ dateRangeLabel }}</span>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<div class="view-selector">
|
||||
<v-btn
|
||||
v-for="span in DAYS_VIEW_SPANS"
|
||||
:key="span"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="setSpan(span)"
|
||||
:color="selectedSpan === span ? 'primary' : undefined"
|
||||
>
|
||||
{{ span.toUpperCase() }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<ViewControls
|
||||
mode="days"
|
||||
:current-date="currentDate"
|
||||
:spans="DAYS_VIEW_SPANS"
|
||||
:selected-span="selectedSpan"
|
||||
@update:current-date="emit('update:current-date', $event)"
|
||||
@update:span="setSpan"
|
||||
/>
|
||||
<div class="days-grid">
|
||||
<!-- Fixed Headers Row -->
|
||||
<div class="day-headers">
|
||||
@@ -83,7 +203,7 @@
|
||||
>
|
||||
<template v-if="daysCount <= 3">
|
||||
<div class="event-time">
|
||||
{{ formatInstanceTime(instance.startMs) }} - {{ formatInstanceTime(instance.endMs) }}
|
||||
{{ formatEpochTime(instance.start) }} - {{ formatEpochTime(instance.end) }}
|
||||
</div>
|
||||
<div class="event-title">{{ instance.label || 'Untitled' }}</div>
|
||||
</template>
|
||||
@@ -99,189 +219,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { layoutMultiDayLanes, type MultiDaySegment } from '@/utils/multiDayLanes';
|
||||
import { shiftLocalDays, startOfLocalDay } from '@/utils/date';
|
||||
import { DAYS_VIEW_SPANS, spanToDays, type DaysViewSpan } from '@/types/spans';
|
||||
import type { EntityObject } from '@ChronoManager/models/entity';
|
||||
import type { CollectionObject } from '@ChronoManager/models/collection';
|
||||
import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
|
||||
import type { CalendarInstance } from '@/types/instance';
|
||||
|
||||
const ALL_DAY_EVENT_HEIGHT = 24;
|
||||
|
||||
type CalendarEntity = EntityObject;
|
||||
type CalendarCollection = CollectionObject;
|
||||
|
||||
const props = defineProps<{
|
||||
currentDate: Date;
|
||||
calendars: CalendarCollection[];
|
||||
initialSpan?: DaysViewSpan;
|
||||
}>();
|
||||
|
||||
const instancesStore = useChronoInstancesStore();
|
||||
|
||||
const selectedSpan = ref<DaysViewSpan>(props.initialSpan ?? '7d');
|
||||
const daysCount = computed(() => spanToDays(selectedSpan.value));
|
||||
|
||||
const emit = defineEmits<{
|
||||
'event-click': [event: CalendarEntity];
|
||||
'event-hover': [data: { event: MouseEvent; entity: CalendarEntity }];
|
||||
'event-hover-end': [];
|
||||
'date-click': [date: Date];
|
||||
'update:span': [span: DaysViewSpan];
|
||||
'update:current-date': [date: Date];
|
||||
}>();
|
||||
|
||||
watch(() => props.initialSpan, (newSpan) => {
|
||||
if (newSpan && newSpan !== selectedSpan.value) {
|
||||
selectedSpan.value = newSpan;
|
||||
}
|
||||
});
|
||||
|
||||
function setSpan(span: DaysViewSpan) {
|
||||
selectedSpan.value = span;
|
||||
emit('update:span', span);
|
||||
}
|
||||
|
||||
function previousPeriod() {
|
||||
emit('update:current-date', shiftLocalDays(props.currentDate, -daysCount.value));
|
||||
}
|
||||
|
||||
function nextPeriod() {
|
||||
emit('update:current-date', shiftLocalDays(props.currentDate, daysCount.value));
|
||||
}
|
||||
|
||||
function goToToday() {
|
||||
emit('update:current-date', new Date());
|
||||
}
|
||||
|
||||
const dateRangeLabel = computed(() => {
|
||||
if (visibleDates.value.length === 0) return '';
|
||||
|
||||
const first = visibleDates.value[0];
|
||||
const last = visibleDates.value[visibleDates.value.length - 1];
|
||||
|
||||
const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
|
||||
|
||||
if (daysCount.value === 1) {
|
||||
return first.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
if (first.getMonth() === last.getMonth()) {
|
||||
return `${first.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${last.getDate()}, ${first.getFullYear()}`;
|
||||
}
|
||||
|
||||
return `${first.toLocaleDateString('en-US', formatOptions)} - ${last.toLocaleDateString('en-US', formatOptions)}, ${last.getFullYear()}`;
|
||||
});
|
||||
|
||||
const visibleDates = computed(() => {
|
||||
const dates: Date[] = [];
|
||||
const start = new Date(props.currentDate);
|
||||
|
||||
for (let i = 0; i < daysCount.value; i++) {
|
||||
dates.push(new Date(start));
|
||||
start.setDate(start.getDate() + 1);
|
||||
}
|
||||
|
||||
return dates;
|
||||
});
|
||||
|
||||
// Compute all-day/multi-day event segments
|
||||
const allDaySegments = computed(() => {
|
||||
if (visibleDates.value.length === 0) {
|
||||
return { segments: [], laneCount: 0 };
|
||||
}
|
||||
|
||||
const rangeStart = startOfLocalDay(visibleDates.value[0]);
|
||||
const rangeEnd = startOfLocalDay(visibleDates.value[visibleDates.value.length - 1]);
|
||||
|
||||
const getColumnIndex = (date: Date): number => {
|
||||
const targetDay = startOfLocalDay(date);
|
||||
return visibleDates.value.findIndex(d => startOfLocalDay(d).getTime() === targetDay.getTime());
|
||||
};
|
||||
|
||||
const instances = instancesStore.instancesForRange({
|
||||
startMs: rangeStart.getTime(),
|
||||
endMs: shiftLocalDays(rangeEnd, 1).getTime(),
|
||||
});
|
||||
|
||||
return layoutMultiDayLanes(instances, rangeStart, rangeEnd, daysCount.value, getColumnIndex);
|
||||
});
|
||||
|
||||
function isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
}
|
||||
|
||||
function getTimedEvents(date: Date): CalendarInstance[] {
|
||||
return instancesStore.instancesForDay(date).filter(instance => !instance.timeless);
|
||||
}
|
||||
|
||||
function getEventColor(instance: CalendarInstance): string {
|
||||
if (instance.color) return instance.color;
|
||||
const calendar = props.calendars.find(cal => cal.identifier === instance.entity.collection);
|
||||
return calendar?.properties?.color || '#1976D2';
|
||||
}
|
||||
|
||||
function getAllDayEventStyle(segment: MultiDaySegment) {
|
||||
return {
|
||||
backgroundColor: getEventColor(segment.instance),
|
||||
left: `calc(${segment.startCol} / ${daysCount.value} * 100% + 4px)`,
|
||||
width: `calc(${segment.span} / ${daysCount.value} * 100% - 8px)`,
|
||||
top: `${segment.lane * ALL_DAY_EVENT_HEIGHT}px`,
|
||||
};
|
||||
}
|
||||
|
||||
function getEventStyle(instance: CalendarInstance) {
|
||||
const startTime = new Date(instance.startMs);
|
||||
const start = startTime.getHours() * 60 + startTime.getMinutes();
|
||||
const duration = instance.durationMs / (1000 * 60);
|
||||
|
||||
return {
|
||||
top: `${(start / 1440) * 100}%`,
|
||||
height: `${(duration / 1440) * 100}%`,
|
||||
backgroundColor: getEventColor(instance),
|
||||
};
|
||||
}
|
||||
|
||||
function formatHour(hour: number): string {
|
||||
const ampm = hour >= 12 ? 'PM' : 'AM';
|
||||
const displayHour = hour % 12 || 12;
|
||||
return `${displayHour} ${ampm}`;
|
||||
}
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function formatInstanceTime(timestamp: number): string {
|
||||
return formatTime(new Date(timestamp));
|
||||
}
|
||||
|
||||
function formatWeekDay(date: Date): string {
|
||||
return date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
}
|
||||
|
||||
function handleDayClick(event: MouseEvent, day: Date) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const clickY = event.clientY - rect.top;
|
||||
|
||||
// Calculate the hour based on click position (60px per hour)
|
||||
const totalMinutes = Math.floor((clickY / 60) * 60);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = Math.round((totalMinutes % 60) / 15) * 15; // Round to nearest 15 min
|
||||
|
||||
// Create a new date with the clicked time
|
||||
const clickedDate = new Date(day);
|
||||
clickedDate.setHours(hours, minutes, 0, 0);
|
||||
|
||||
emit('date-click', clickedDate);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.days-view {
|
||||
display: flex;
|
||||
@@ -290,34 +227,6 @@ function handleDayClick(event: MouseEvent, day: Date) {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.view-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid rgb(var(--v-border-color));
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.view-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.current-period {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.view-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.days-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user