Files
chrono/src/components/AgendaView.vue
T
Sebastian b663efbb3e feat: use module store
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-02-17 19:11:12 -05:00

246 lines
6.9 KiB
Vue

<template>
<div class="agenda-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 AGENDA_VIEW_SPANS"
:key="span"
size="x-small"
variant="text"
@click="setSpan(span)"
:color="selectedSpan === span ? 'primary' : undefined"
>
{{ span.toUpperCase() }}
</v-btn>
</div>
</div>
<v-list class="agenda-list">
<template v-if="Object.keys(groupedEvents).length === 0">
<v-list-item>
<v-list-item-title class="text-center text-medium-emphasis">No events in this period</v-list-item-title>
</v-list-item>
</template>
<template v-for="(dayEvents, date) in groupedEvents" :key="date">
<v-list-subheader>{{ formatAgendaDate(date) }}</v-list-subheader>
<v-list-item
v-for="entity in dayEvents"
:key="entity.identifier"
@click="$emit('event-click', entity)"
>
<template #prepend>
<v-icon :color="getEventColor(entity)">mdi-circle</v-icon>
</template>
<v-list-item-title>{{ entity.properties?.label || 'Untitled' }}</v-list-item-title>
<v-list-item-subtitle>
{{ getEventProperties(entity).timeless ? 'All day' : `${formatEventDateTime(getEventProperties(entity).startsOn)} - ${formatEventDateTime(getEventProperties(entity).endsOn)}` }}
</v-list-item-subtitle>
</v-list-item>
</template>
</v-list>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { AGENDA_VIEW_SPANS, spanToDays, type AgendaViewSpan } from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
import { EventObject } from '@ChronoManager/models/event';
type CalendarEntity = EntityObject;
type CalendarCollection = CollectionObject;
const props = defineProps<{
events: CalendarEntity[];
calendars: CalendarCollection[];
currentDate?: Date;
initialSpan?: AgendaViewSpan;
}>();
const selectedSpan = ref<AgendaViewSpan>(props.initialSpan ?? '1w');
const localDate = ref(new Date(props.currentDate ?? new Date()));
const emit = defineEmits<{
'event-click': [event: CalendarEntity];
'update:span': [span: AgendaViewSpan];
}>();
// 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) => {
if (newSpan && newSpan !== selectedSpan.value) {
selectedSpan.value = newSpan;
}
});
function setSpan(span: AgendaViewSpan) {
selectedSpan.value = span;
emit('update:span', span);
}
function getSpanDays(): number {
return spanToDays(selectedSpan.value);
}
function previousPeriod() {
const date = new Date(localDate.value);
date.setDate(date.getDate() - getSpanDays());
localDate.value = date;
}
function nextPeriod() {
const date = new Date(localDate.value);
date.setDate(date.getDate() + getSpanDays());
localDate.value = date;
}
function goToToday() {
localDate.value = new Date();
}
const dateRange = computed(() => {
const start = new Date(localDate.value);
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(end.getDate() + getSpanDays() - 1);
end.setHours(23, 59, 59, 999);
return { start, end };
});
const dateRangeLabel = computed(() => {
const { start, end } = dateRange.value;
const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
if (getSpanDays() === 1) {
return start.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
}
if (start.getMonth() === end.getMonth()) {
return `${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${end.getDate()}, ${start.getFullYear()}`;
}
return `${start.toLocaleDateString('en-US', formatOptions)} - ${end.toLocaleDateString('en-US', formatOptions)}, ${end.getFullYear()}`;
});
const groupedEvents = computed(() => {
const grouped: Record<string, CalendarEntity[]> = {};
const { start, end } = dateRange.value;
const filtered = props.events.filter((e): e is CalendarEntity => {
const startsOn = getEventProperties(e).startsOn;
if (typeof startsOn !== 'string') return false;
const eventStart = new Date(startsOn);
return eventStart >= start && eventStart <= end;
});
const sorted = filtered.sort((a, b) =>
new Date(getEventProperties(a).startsOn || 0).getTime() - new Date(getEventProperties(b).startsOn || 0).getTime()
);
sorted.forEach(entity => {
const dateKey = new Date(getEventProperties(entity).startsOn || 0).toDateString();
if (!grouped[dateKey]) {
grouped[dateKey] = [];
}
grouped[dateKey].push(entity);
});
return grouped;
});
function getEventColor(entity: CalendarEntity): string {
const event = getEventProperties(entity);
if (event.color) return event.color;
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.properties?.color || '#1976D2';
}
function getEventProperties(entity: CalendarEntity): EventObject {
return entity.properties as EventObject;
}
function formatTime(date: Date): string {
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
}
function formatEventDateTime(value: string | null): string {
return value ? formatTime(new Date(value)) : '';
}
function formatAgendaDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
</script>
<style scoped>
.agenda-view {
display: flex;
flex-direction: column;
height: 100%;
}
.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;
}
.agenda-list {
flex: 1;
overflow-y: auto;
background-color: transparent;
}
.agenda-list :deep(.v-list-item) {
margin: 4px 8px;
border-radius: 4px;
background-color: transparent;
}
.agenda-list :deep(.v-list-item:hover) {
background-color: rgba(var(--v-theme-primary), 0.04);
}
</style>