227 lines
6.7 KiB
Vue
227 lines
6.7 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 size="x-small" variant="text" @click="setSpan('1d')" :color="selectedSpan === '1d' ? 'primary' : undefined">1D</v-btn>
|
|
<v-btn size="x-small" variant="text" @click="setSpan('3d')" :color="selectedSpan === '3d' ? 'primary' : undefined">3D</v-btn>
|
|
<v-btn size="x-small" variant="text" @click="setSpan('1w')" :color="selectedSpan === '1w' ? 'primary' : undefined">1W</v-btn>
|
|
<v-btn size="x-small" variant="text" @click="setSpan('2w')" :color="selectedSpan === '2w' ? 'primary' : undefined">2W</v-btn>
|
|
<v-btn size="x-small" variant="text" @click="setSpan('3w')" :color="selectedSpan === '3w' ? 'primary' : undefined">3W</v-btn>
|
|
<v-btn size="x-small" variant="text" @click="setSpan('1m')" :color="selectedSpan === '1m' ? 'primary' : undefined">1M</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>
|
|
{{ entity.properties?.timeless ? 'All day' : `${entity.properties?.startsOn ? formatTime(new Date(entity.properties.startsOn)) : ''} - ${entity.properties?.endsOn ? formatTime(new Date(entity.properties.endsOn)) : ''}` }}
|
|
</v-list-item-subtitle>
|
|
</v-list-item>
|
|
</template>
|
|
</v-list>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue';
|
|
|
|
type SpanType = '1d' | '3d' | '1w' | '2w' | '3w' | '1m';
|
|
|
|
const props = defineProps<{
|
|
events: any[];
|
|
calendars: any[];
|
|
currentDate?: Date;
|
|
initialSpan?: SpanType;
|
|
}>();
|
|
|
|
defineEmits<{
|
|
'event-click': [event: any];
|
|
}>();
|
|
|
|
const selectedSpan = ref<SpanType>(props.initialSpan ?? '1w');
|
|
const localDate = ref(new Date(props.currentDate ?? new Date()));
|
|
|
|
// Watch for external date changes (e.g., from mini calendar)
|
|
watch(() => props.currentDate, (newDate) => {
|
|
if (newDate) {
|
|
localDate.value = new Date(newDate);
|
|
}
|
|
});
|
|
|
|
function setSpan(span: SpanType) {
|
|
selectedSpan.value = span;
|
|
}
|
|
|
|
function getSpanDays(): number {
|
|
switch (selectedSpan.value) {
|
|
case '1d': return 1;
|
|
case '3d': return 3;
|
|
case '1w': return 7;
|
|
case '2w': return 14;
|
|
case '3w': return 21;
|
|
case '1m': return 30;
|
|
default: return 7;
|
|
}
|
|
}
|
|
|
|
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, any[]> = {};
|
|
const { start, end } = dateRange.value;
|
|
|
|
const filtered = props.events.filter(e => {
|
|
if (!e.properties?.startsOn) return false;
|
|
const eventStart = new Date(e.properties.startsOn);
|
|
return eventStart >= start && eventStart <= end;
|
|
});
|
|
|
|
const sorted = filtered.sort((a, b) =>
|
|
new Date(a.properties.startsOn).getTime() - new Date(b.properties.startsOn).getTime()
|
|
);
|
|
|
|
sorted.forEach(entity => {
|
|
const dateKey = new Date(entity.properties.startsOn).toDateString();
|
|
if (!grouped[dateKey]) {
|
|
grouped[dateKey] = [];
|
|
}
|
|
grouped[dateKey].push(entity);
|
|
});
|
|
|
|
return grouped;
|
|
});
|
|
|
|
function getEventColor(entity: any): string {
|
|
if (entity.properties?.color) return entity.properties.color;
|
|
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
|
|
return calendar?.properties?.color || '#1976D2';
|
|
}
|
|
|
|
function formatTime(date: Date): string {
|
|
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
|
}
|
|
|
|
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>
|