feat: use module store #3
@@ -9,12 +9,16 @@
|
||||
</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>
|
||||
<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">
|
||||
@@ -35,7 +39,7 @@
|
||||
</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)) : ''}` }}
|
||||
{{ getEventProperties(entity).timeless ? 'All day' : `${formatEventDateTime(getEventProperties(entity).startsOn)} - ${formatEventDateTime(getEventProperties(entity).endsOn)}` }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@@ -45,23 +49,29 @@
|
||||
|
||||
<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 SpanType = '1d' | '3d' | '1w' | '2w' | '3w' | '1m';
|
||||
type CalendarEntity = EntityObject;
|
||||
type CalendarCollection = CollectionObject;
|
||||
|
||||
const props = defineProps<{
|
||||
events: any[];
|
||||
calendars: any[];
|
||||
events: CalendarEntity[];
|
||||
calendars: CalendarCollection[];
|
||||
currentDate?: Date;
|
||||
initialSpan?: SpanType;
|
||||
initialSpan?: AgendaViewSpan;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
'event-click': [event: any];
|
||||
}>();
|
||||
|
||||
const selectedSpan = ref<SpanType>(props.initialSpan ?? '1w');
|
||||
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) {
|
||||
@@ -69,20 +79,19 @@ watch(() => props.currentDate, (newDate) => {
|
||||
}
|
||||
});
|
||||
|
||||
function setSpan(span: SpanType) {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
return spanToDays(selectedSpan.value);
|
||||
}
|
||||
|
||||
function previousPeriod() {
|
||||
@@ -128,21 +137,22 @@ const dateRangeLabel = computed(() => {
|
||||
});
|
||||
|
||||
const groupedEvents = computed(() => {
|
||||
const grouped: Record<string, any[]> = {};
|
||||
const grouped: Record<string, CalendarEntity[]> = {};
|
||||
const { start, end } = dateRange.value;
|
||||
|
||||
const filtered = props.events.filter(e => {
|
||||
if (!e.properties?.startsOn) return false;
|
||||
const eventStart = new Date(e.properties.startsOn);
|
||||
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(a.properties.startsOn).getTime() - new Date(b.properties.startsOn).getTime()
|
||||
new Date(getEventProperties(a).startsOn || 0).getTime() - new Date(getEventProperties(b).startsOn || 0).getTime()
|
||||
);
|
||||
|
||||
sorted.forEach(entity => {
|
||||
const dateKey = new Date(entity.properties.startsOn).toDateString();
|
||||
const dateKey = new Date(getEventProperties(entity).startsOn || 0).toDateString();
|
||||
if (!grouped[dateKey]) {
|
||||
grouped[dateKey] = [];
|
||||
}
|
||||
@@ -152,16 +162,25 @@ const groupedEvents = computed(() => {
|
||||
return grouped;
|
||||
});
|
||||
|
||||
function getEventColor(entity: any): string {
|
||||
if (entity.properties?.color) return entity.properties.color;
|
||||
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', {
|
||||
|
||||
@@ -21,16 +21,21 @@
|
||||
:current-date="currentDate"
|
||||
:events="events"
|
||||
:calendars="calendars"
|
||||
:initial-span="initialDaysSpan"
|
||||
@event-click="$emit('event-click', $event)"
|
||||
@date-click="$emit('date-click', $event)"
|
||||
@event-hover="handleEventHover"
|
||||
@event-hover-end="hidePopup"
|
||||
@update:span="$emit('update:days-span', $event)"
|
||||
/>
|
||||
<AgendaView
|
||||
v-else-if="view === 'agenda'"
|
||||
:events="events"
|
||||
:calendars="calendars"
|
||||
:current-date="currentDate"
|
||||
:initial-span="initialAgendaViewSpan"
|
||||
@event-click="$emit('event-click', $event)"
|
||||
@update:span="$emit('update:agenda-span', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -41,26 +46,33 @@ import MonthView from './MonthView.vue';
|
||||
import DaysView from './DaysView.vue';
|
||||
import AgendaView from './AgendaView.vue';
|
||||
import EventViewerPopup from './EventViewerPopup.vue';
|
||||
import type { AgendaViewSpan, DaysViewSpan } from '@/types/spans';
|
||||
import type { EntityObject } from '@ChronoManager/models/entity';
|
||||
import type { CollectionObject } from '@ChronoManager/models/collection';
|
||||
|
||||
defineProps<{
|
||||
view: 'days' | 'month' | 'agenda';
|
||||
currentDate: Date;
|
||||
events: any[];
|
||||
calendars: any[];
|
||||
events: EntityObject[];
|
||||
calendars: CollectionObject[];
|
||||
initialDaysSpan?: DaysViewSpan;
|
||||
initialAgendaViewSpan?: AgendaViewSpan;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
'event-click': [event: any];
|
||||
'event-click': [event: EntityObject];
|
||||
'date-click': [date: Date];
|
||||
'update:days-span': [span: DaysViewSpan];
|
||||
'update:agenda-span': [span: AgendaViewSpan];
|
||||
}>();
|
||||
|
||||
// Popup state
|
||||
const hoveredEvent = ref<any>(null);
|
||||
const hoveredEvent = ref<EntityObject | null>(null);
|
||||
const popupPosition = ref({ x: 0, y: 0 });
|
||||
const showPopup = ref(false);
|
||||
let hideTimeout: NodeJS.Timeout | null = null;
|
||||
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function handleEventHover(data: { event: MouseEvent; entity: any }) {
|
||||
function handleEventHover(data: { event: MouseEvent; entity: EntityObject }) {
|
||||
if (hideTimeout) {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = null;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore'
|
||||
import { computed } from 'vue'
|
||||
import { CollectionObject } from '@ChronoManager/models/collection';
|
||||
|
||||
// Store
|
||||
const collectionsStore = useCollectionsStore()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
collections: CollectionObject[]
|
||||
selectedCollection?: CollectionObject | null
|
||||
type?: 'calendar' | 'tasklist'
|
||||
}>()
|
||||
@@ -19,15 +16,11 @@ const emit = defineEmits<{
|
||||
'toggle-visibility': [collection: CollectionObject]
|
||||
}>()
|
||||
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const collections = ref<CollectionObject[]>([])
|
||||
|
||||
// Computed
|
||||
const filteredCollections = computed(() => {
|
||||
if (!props.type) return collections.value
|
||||
if (!props.type) return props.collections
|
||||
|
||||
return collections.value.filter(collection => {
|
||||
return props.collections.filter(collection => {
|
||||
if (props.type === 'calendar') {
|
||||
return collection.properties.contents?.event
|
||||
} else if (props.type === 'tasklist') {
|
||||
@@ -49,17 +42,6 @@ const displayIcon = computed(() => {
|
||||
return 'mdi-folder'
|
||||
})
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
collections.value = Object.values(await collectionsStore.list())
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to load collections:', error)
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
// Functions
|
||||
const onCollectionSelect = (collection: CollectionObject) => {
|
||||
console.log('[Chrono] - Collection selected', collection)
|
||||
@@ -71,22 +53,8 @@ const onCollectionEdit = (collection: CollectionObject) => {
|
||||
}
|
||||
|
||||
const onToggleVisibility = (collection: CollectionObject) => {
|
||||
collection.properties.visibility = collection.properties.visibility === false
|
||||
emit('toggle-visibility', collection)
|
||||
}
|
||||
|
||||
// Expose refresh method
|
||||
defineExpose({
|
||||
async refresh() {
|
||||
loading.value = true
|
||||
try {
|
||||
collections.value = Object.values(await collectionsStore.list())
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to load collections:', error)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -101,9 +69,7 @@ defineExpose({
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<div class="collection-selector-content">
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
||||
|
||||
<v-list v-else density="compact" nav class="pa-0">
|
||||
<v-list density="compact" nav class="pa-0">
|
||||
<v-list-item
|
||||
v-for="collection in filteredCollections"
|
||||
:key="collection.identifier"
|
||||
@@ -142,7 +108,7 @@ defineExpose({
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-alert v-if="!loading && filteredCollections.length === 0" type="info" variant="tonal" density="compact" class="mt-2">
|
||||
<v-alert v-if="filteredCollections.length === 0" type="info" variant="tonal" density="compact" class="mt-2">
|
||||
No {{ displayTitle.toLowerCase() }} found
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,16 @@
|
||||
</div>
|
||||
<v-spacer />
|
||||
<div class="view-selector">
|
||||
<v-btn size="x-small" variant="text" @click="setDays(1)" :color="daysCount === 1 ? 'primary' : undefined">1D</v-btn>
|
||||
<v-btn size="x-small" variant="text" @click="setDays(3)" :color="daysCount === 3 ? 'primary' : undefined">3D</v-btn>
|
||||
<v-btn size="x-small" variant="text" @click="setDays(5)" :color="daysCount === 5 ? 'primary' : undefined">5D</v-btn>
|
||||
<v-btn size="x-small" variant="text" @click="setDays(7)" :color="daysCount === 7 ? 'primary' : undefined">7D</v-btn>
|
||||
<v-btn size="x-small" variant="text" @click="setDays(9)" :color="daysCount === 9 ? 'primary' : undefined">9D</v-btn>
|
||||
<v-btn size="x-small" variant="text" @click="setDays(11)" :color="daysCount === 11 ? 'primary' : undefined">11D</v-btn>
|
||||
<v-btn size="x-small" variant="text" @click="setDays(14)" :color="daysCount === 14 ? 'primary' : undefined">14D</v-btn>
|
||||
<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>
|
||||
<div class="days-grid">
|
||||
@@ -80,12 +83,12 @@
|
||||
>
|
||||
<template v-if="daysCount <= 3">
|
||||
<div class="event-time">
|
||||
{{ entity.properties?.startsOn ? formatTime(new Date(entity.properties.startsOn)) : '' }} - {{ entity.properties?.endsOn ? formatTime(new Date(entity.properties.endsOn)) : '' }}
|
||||
{{ formatEventDateTime(getEventProperties(entity).startsOn) }} - {{ formatEventDateTime(getEventProperties(entity).endsOn) }}
|
||||
</div>
|
||||
<div class="event-title">{{ entity.properties?.label || 'Untitled' }}</div>
|
||||
<div class="event-title">{{ getEventProperties(entity).label || 'Untitled' }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="event-title-compact">{{ entity.properties?.label || 'Untitled' }}</div>
|
||||
<div class="event-title-compact">{{ getEventProperties(entity).label || 'Untitled' }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,26 +107,49 @@ import {
|
||||
getTimedEventsForDate,
|
||||
type MultiDaySegment,
|
||||
} from '@/utils/calendarHelpers';
|
||||
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 { EventObject } from '@ChronoManager/models/event';
|
||||
|
||||
const ALL_DAY_EVENT_HEIGHT = 24;
|
||||
|
||||
type CalendarEntity = EntityObject;
|
||||
type CalendarCollection = CollectionObject;
|
||||
|
||||
const props = defineProps<{
|
||||
currentDate: Date;
|
||||
events: any[];
|
||||
calendars: any[];
|
||||
initialDays?: number;
|
||||
events: CalendarEntity[];
|
||||
calendars: CalendarCollection[];
|
||||
initialSpan?: DaysViewSpan;
|
||||
}>();
|
||||
|
||||
const daysCount = ref(props.initialDays ?? 7);
|
||||
const selectedSpan = ref<DaysViewSpan>(props.initialSpan ?? '7d');
|
||||
const daysCount = computed(() => spanToDays(selectedSpan.value));
|
||||
const localDate = ref(new Date(props.currentDate));
|
||||
|
||||
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];
|
||||
}>();
|
||||
|
||||
// Watch for external date changes (e.g., from mini calendar)
|
||||
watch(() => props.currentDate, (newDate) => {
|
||||
localDate.value = new Date(newDate);
|
||||
});
|
||||
|
||||
function setDays(count: number) {
|
||||
daysCount.value = count;
|
||||
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() {
|
||||
@@ -195,12 +221,17 @@ function isToday(date: Date): boolean {
|
||||
return date.toDateString() === today.toDateString();
|
||||
}
|
||||
|
||||
function getTimedEvents(date: Date): any[] {
|
||||
return getTimedEventsForDate(props.events, date);
|
||||
function getTimedEvents(date: Date): CalendarEntity[] {
|
||||
return getTimedEventsForDate(props.events, date) as CalendarEntity[];
|
||||
}
|
||||
|
||||
function getEventColor(entity: any): string {
|
||||
if (entity.properties?.color) return entity.properties.color;
|
||||
function getEventProperties(entity: CalendarEntity): EventObject {
|
||||
return entity.properties as EventObject;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
@@ -214,12 +245,14 @@ function getAllDayEventStyle(segment: MultiDaySegment) {
|
||||
};
|
||||
}
|
||||
|
||||
function getEventStyle(entity: any) {
|
||||
if (!entity.properties?.startsOn || !entity.properties?.endsOn) {
|
||||
function getEventStyle(entity: CalendarEntity) {
|
||||
const event = getEventProperties(entity);
|
||||
|
||||
if (!event.startsOn || !event.endsOn) {
|
||||
return { display: 'none' };
|
||||
}
|
||||
const startTime = new Date(entity.properties.startsOn);
|
||||
const endTime = new Date(entity.properties.endsOn);
|
||||
const startTime = new Date(event.startsOn);
|
||||
const endTime = new Date(event.endsOn);
|
||||
const start = startTime.getHours() * 60 + startTime.getMinutes();
|
||||
const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60);
|
||||
|
||||
@@ -240,17 +273,14 @@ 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 formatWeekDay(date: Date): string {
|
||||
return date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
'event-click': [event: any];
|
||||
'event-hover': [data: { event: MouseEvent; entity: any }];
|
||||
'event-hover-end': [];
|
||||
'date-click': [date: Date];
|
||||
}>();
|
||||
|
||||
function handleDayClick(event: MouseEvent, day: Date) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { CollectionObject } from '@ChronoManager/models/collection'
|
||||
import type { EntityObject } from '@ChronoManager/models/entity'
|
||||
import type { EventObject } from '@ChronoManager/models/event'
|
||||
@@ -25,20 +25,22 @@ const emit = defineEmits<{
|
||||
'edit': []
|
||||
'cancel': []
|
||||
'close': []
|
||||
'save': [entity: any, collection?: any]
|
||||
'delete': [entity: any, collection?: any]
|
||||
'save': [entity: EntityObject, collection?: CollectionObject | null]
|
||||
'delete': [entity: EntityObject, collection?: CollectionObject | null]
|
||||
}>()
|
||||
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const selectedCollectionId = ref(props.entity?.collection || props.collection?.identifier || props.calendars?.[0]?.identifier || '')
|
||||
const initialEntity = props.entity as EntityObject | null
|
||||
const draftEntity = ref<EntityObject | null>(initialEntity?.clone ? initialEntity.clone() : null)
|
||||
const selectedCollectionId = ref<string | number>('')
|
||||
|
||||
// Computed
|
||||
const mode = computed(() => props.mode)
|
||||
const entity = computed(() => props.entity || null)
|
||||
const entityObject = computed(() => entity.value?.properties as EventObject ?? null)
|
||||
const entityFresh = computed(() => !entity.value?.identifier)
|
||||
const editableEntity = computed(() => draftEntity.value)
|
||||
const entityObject = computed(() => editableEntity.value?.properties as EventObject ?? null)
|
||||
const entityFresh = computed(() => !editableEntity.value?.identifier)
|
||||
|
||||
const calendarOptions = computed(() =>
|
||||
(props.calendars || []).map(cal => ({
|
||||
@@ -69,50 +71,77 @@ const startEdit = () => {
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
const entity = props.entity as EntityObject | null
|
||||
draftEntity.value = entity?.clone ? entity.clone() : null
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
const saveEntity = async () => {
|
||||
if (!editableEntity.value) {
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
const targetCollection = (props.calendars || []).find(cal => cal.identifier === selectedCollectionId.value)
|
||||
emit('save', entity.value!, targetCollection || props.collection)
|
||||
emit('save', editableEntity.value as EntityObject, targetCollection || props.collection)
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
// Location management
|
||||
const addLocationPhysical = () => {
|
||||
(entityObject.value as any)?.addLocationPhysical()
|
||||
entityObject.value?.addLocationPhysical()
|
||||
}
|
||||
|
||||
const removeLocationPhysical = (key: string) => {
|
||||
(entityObject.value as any)?.removeLocationPhysical(key)
|
||||
entityObject.value?.removeLocationPhysical(key)
|
||||
}
|
||||
|
||||
const addLocationVirtual = () => {
|
||||
(entityObject.value as any)?.addLocationVirtual()
|
||||
entityObject.value?.addLocationVirtual()
|
||||
}
|
||||
|
||||
const removeLocationVirtual = (key: string) => {
|
||||
(entityObject.value as any)?.removeLocationVirtual(key)
|
||||
entityObject.value?.removeLocationVirtual(key)
|
||||
}
|
||||
|
||||
// Participant management
|
||||
const addParticipant = () => {
|
||||
(entityObject.value as any)?.addParticipant()
|
||||
entityObject.value?.addParticipant()
|
||||
}
|
||||
|
||||
const removeParticipant = (key: string) => {
|
||||
(entityObject.value as any)?.removeParticipant(key)
|
||||
entityObject.value?.removeParticipant(key)
|
||||
}
|
||||
|
||||
// Notification management
|
||||
const addNotification = () => {
|
||||
(entityObject.value as any)?.addNotification()
|
||||
entityObject.value?.addNotification()
|
||||
}
|
||||
|
||||
const removeNotification = (key: string) => {
|
||||
(entityObject.value as any)?.removeNotification(key)
|
||||
entityObject.value?.removeNotification(key)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.entity,
|
||||
(newEntity) => {
|
||||
const entity = newEntity as EntityObject | null
|
||||
draftEntity.value = entity?.clone ? entity.clone() : null
|
||||
selectedCollectionId.value = entity?.collection || props.collection?.identifier || props.calendars?.[0]?.identifier || ''
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.mode,
|
||||
(newMode) => {
|
||||
const entity = props.entity as EntityObject | null
|
||||
if (newMode === 'edit' && entity?.clone) {
|
||||
draftEntity.value = entity.clone()
|
||||
selectedCollectionId.value = entity.collection || props.collection?.identifier || props.calendars?.[0]?.identifier || ''
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -123,7 +152,7 @@ const removeNotification = (key: string) => {
|
||||
{{ entityObject?.label || 'Nothing Selected' }}
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && entity">
|
||||
<div v-if="!loading && editableEntity">
|
||||
<v-btn
|
||||
v-if="mode === 'view' && canEdit"
|
||||
icon="mdi-pencil"
|
||||
@@ -138,7 +167,7 @@ const removeNotification = (key: string) => {
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="emit('delete', entity!, collection)"
|
||||
@click="emit('delete', editableEntity as EntityObject, collection)"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
@@ -154,14 +183,14 @@ const removeNotification = (key: string) => {
|
||||
<v-divider />
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
||||
|
||||
<div v-if="!entity" class="text-center pa-8">
|
||||
<div v-if="!editableEntity" class="text-center pa-8">
|
||||
<v-icon icon="mdi-calendar-blank" size="64" color="grey" class="mb-4" />
|
||||
<p class="text-h6 text-grey">Select an event to view details</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="pa-4">
|
||||
<v-select
|
||||
v-if="mode === 'edit' && !entity.identifier"
|
||||
v-if="mode === 'edit' && !editableEntity?.identifier"
|
||||
v-model="selectedCollectionId"
|
||||
:items="calendarOptions"
|
||||
label="Calendar"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-card v-if="entity" class="task-editor">
|
||||
<v-card v-if="editableEntity && taskProperties" class="task-editor">
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span>{{ entity.identifier ? entity.properties?.label || 'Untitled Task' : 'New Task' }}</span>
|
||||
<span>{{ editableEntity.identifier ? taskProperties.label || 'Untitled Task' : 'New Task' }}</span>
|
||||
<div class="d-flex gap-2">
|
||||
<v-btn
|
||||
v-if="mode === 'view' && canEdit"
|
||||
@@ -22,7 +22,7 @@
|
||||
<v-card-text class="task-editor-content">
|
||||
<v-form ref="formRef">
|
||||
<v-text-field
|
||||
v-model="entity.properties.label"
|
||||
v-model="taskProperties.label"
|
||||
label="Title"
|
||||
:rules="[v => !!v || 'Title is required']"
|
||||
:readonly="!isEditing"
|
||||
@@ -40,7 +40,7 @@
|
||||
></v-select>
|
||||
|
||||
<v-textarea
|
||||
v-model="entity.properties.description"
|
||||
v-model="taskProperties.description"
|
||||
label="Description"
|
||||
:readonly="!isEditing"
|
||||
:variant="isEditing ? 'outlined' : 'plain'"
|
||||
@@ -49,7 +49,7 @@
|
||||
></v-textarea>
|
||||
|
||||
<v-select
|
||||
v-model="entity.properties.priority"
|
||||
v-model="taskProperties.priority"
|
||||
:items="priorityOptions"
|
||||
label="Priority"
|
||||
:readonly="!isEditing"
|
||||
@@ -78,7 +78,7 @@
|
||||
></v-text-field>
|
||||
|
||||
<v-combobox
|
||||
v-model="entity.properties.categories"
|
||||
v-model="taskProperties.tags"
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<div class="d-flex gap-4">
|
||||
<v-text-field
|
||||
v-model.number="entity.properties.estimatedTime"
|
||||
v-model.number="taskProperties.progress"
|
||||
label="Estimated Time (minutes)"
|
||||
type="number"
|
||||
:readonly="!isEditing"
|
||||
@@ -98,7 +98,7 @@
|
||||
class="flex-1-1"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model.number="entity.properties.actualTime"
|
||||
v-model.number="taskProperties.priority"
|
||||
label="Actual Time (minutes)"
|
||||
type="number"
|
||||
:readonly="!isEditing"
|
||||
@@ -108,13 +108,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Metadata for view mode -->
|
||||
<div v-if="mode === 'view' && entity.properties.created" class="mt-4">
|
||||
<div v-if="mode === 'view' && taskProperties.created" class="mt-4">
|
||||
<v-divider class="mb-4"></v-divider>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
<div>Created: {{ new Date(entity.properties.created).toLocaleString() }}</div>
|
||||
<div v-if="entity.properties.modified">Modified: {{ new Date(entity.properties.modified).toLocaleString() }}</div>
|
||||
<div v-if="entity.properties.status === 'completed' && entity.properties.completedOn">
|
||||
Completed: {{ new Date(entity.properties.completedOn).toLocaleString() }}
|
||||
<div>Created: {{ new Date(taskProperties.created).toLocaleString() }}</div>
|
||||
<div v-if="taskProperties.modified">Modified: {{ new Date(taskProperties.modified).toLocaleString() }}</div>
|
||||
<div v-if="taskProperties.status === 'completed' && taskProperties.completedOn">
|
||||
Completed: {{ new Date(taskProperties.completedOn).toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="isExternalTask" class="mt-2">
|
||||
<v-chip size="small" color="info" variant="tonal">
|
||||
@@ -129,7 +129,7 @@
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
v-if="mode === 'edit' && entity.identifier && canDelete"
|
||||
v-if="mode === 'edit' && editableEntity.identifier && canDelete"
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="handleDelete"
|
||||
@@ -165,34 +165,43 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import type { CollectionObject } from '@ChronoManager/models/collection';
|
||||
import { EntityObject } from '@ChronoManager/models/entity';
|
||||
import { TaskObject } from '@ChronoManager/models/task';
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'view' | 'edit';
|
||||
entity: any | null;
|
||||
collection: any | null;
|
||||
lists: any[];
|
||||
entity: EntityObject | null;
|
||||
collection: CollectionObject | null;
|
||||
lists: CollectionObject[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [entity: any, collection?: any];
|
||||
delete: [entity: any, collection?: any];
|
||||
save: [entity: EntityObject, collection?: CollectionObject | null];
|
||||
delete: [entity: EntityObject, collection?: CollectionObject | null];
|
||||
edit: [];
|
||||
cancel: [];
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
function cloneEntity(entity: EntityObject | null): EntityObject | null {
|
||||
return entity?.clone ? entity.clone() : null;
|
||||
}
|
||||
|
||||
const formRef = ref();
|
||||
const selectedCollectionId = ref(props.entity?.collection || props.collection?.identifier || props.lists[0]?.identifier || '');
|
||||
const draftEntity = ref<EntityObject | null>(cloneEntity(props.entity));
|
||||
const selectedCollectionId = ref<string | number>('');
|
||||
const editableEntity = computed(() => draftEntity.value);
|
||||
const taskProperties = computed(() => editableEntity.value?.properties as TaskObject | null);
|
||||
|
||||
// Determine permissions based on entity metadata
|
||||
const isExternalTask = computed(() => {
|
||||
// Check if task was assigned by another user
|
||||
return props.entity?.properties?.external === true || props.entity?.properties?.assignedBy;
|
||||
return false;
|
||||
});
|
||||
|
||||
const canEdit = computed(() => {
|
||||
if (!props.entity) return false;
|
||||
if (props.entity.properties?.readonly === true) return false;
|
||||
if (!editableEntity.value) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -203,12 +212,12 @@ const canEditDates = computed(() => {
|
||||
|
||||
const canChangeCollection = computed(() => {
|
||||
// Can't move external tasks to different lists
|
||||
return !isExternalTask.value && !props.entity?.identifier;
|
||||
return !isExternalTask.value && !editableEntity.value?.identifier;
|
||||
});
|
||||
|
||||
const canDelete = computed(() => {
|
||||
// Can't delete external/readonly tasks
|
||||
return !isExternalTask.value && props.entity?.properties?.readonly !== true;
|
||||
return !isExternalTask.value;
|
||||
});
|
||||
|
||||
const isEditing = computed(() => props.mode === 'edit');
|
||||
@@ -218,21 +227,32 @@ const startDate = ref('');
|
||||
|
||||
// Watch for entity changes to update date fields
|
||||
watch(() => props.entity, (newEntity) => {
|
||||
if (newEntity?.properties?.dueOn) {
|
||||
dueDate.value = formatDate(new Date(newEntity.properties.dueOn));
|
||||
draftEntity.value = cloneEntity(newEntity);
|
||||
|
||||
const task = draftEntity.value?.properties as TaskObject | undefined;
|
||||
|
||||
if (task?.dueOn) {
|
||||
dueDate.value = formatDate(new Date(task.dueOn));
|
||||
} else {
|
||||
dueDate.value = '';
|
||||
}
|
||||
|
||||
if (newEntity?.properties?.startsOn) {
|
||||
startDate.value = formatDate(new Date(newEntity.properties.startsOn));
|
||||
if (task?.startsOn) {
|
||||
startDate.value = formatDate(new Date(task.startsOn));
|
||||
} else {
|
||||
startDate.value = '';
|
||||
}
|
||||
|
||||
selectedCollectionId.value = newEntity?.collection || props.collection?.identifier || props.lists[0]?.identifier || '';
|
||||
selectedCollectionId.value = draftEntity.value?.collection || props.collection?.identifier || props.lists[0]?.identifier || '';
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => props.mode, (newMode) => {
|
||||
if (newMode === 'edit' && props.entity) {
|
||||
draftEntity.value = cloneEntity(props.entity);
|
||||
selectedCollectionId.value = draftEntity.value?.collection || props.collection?.identifier || props.lists[0]?.identifier || '';
|
||||
}
|
||||
});
|
||||
|
||||
const listOptions = computed(() =>
|
||||
props.lists.map(list => ({
|
||||
title: list.properties.label || 'Unnamed List',
|
||||
@@ -247,14 +267,14 @@ const priorityOptions = [
|
||||
];
|
||||
|
||||
watch(dueDate, (newVal) => {
|
||||
if (props.entity && isEditing.value) {
|
||||
props.entity.properties.dueOn = newVal ? new Date(newVal).toISOString() : null;
|
||||
if (taskProperties.value && isEditing.value) {
|
||||
taskProperties.value.dueOn = newVal ? new Date(newVal).toISOString() as never : null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(startDate, (newVal) => {
|
||||
if (props.entity && isEditing.value) {
|
||||
props.entity.properties.startsOn = newVal ? new Date(newVal).toISOString() : null;
|
||||
if (taskProperties.value && isEditing.value) {
|
||||
taskProperties.value.startsOn = newVal ? new Date(newVal).toISOString() as never : null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -263,21 +283,24 @@ function formatDate(date: Date): string {
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!editableEntity.value) return;
|
||||
|
||||
const { valid } = await formRef.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
const targetCollection = props.lists.find(list => list.identifier === selectedCollectionId.value);
|
||||
emit('save', props.entity, targetCollection || props.collection);
|
||||
emit('save', editableEntity.value as EntityObject, targetCollection || props.collection);
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
draftEntity.value = cloneEntity(props.entity);
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (props.entity) {
|
||||
if (editableEntity.value) {
|
||||
const targetCollection = props.lists.find(list => list.identifier === selectedCollectionId.value);
|
||||
emit('delete', props.entity, targetCollection || props.collection);
|
||||
emit('delete', editableEntity.value as EntityObject, targetCollection || props.collection);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
96
src/composables/useChronoEntityActions.ts
Normal file
96
src/composables/useChronoEntityActions.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { CollectionObject } from '@ChronoManager/models/collection'
|
||||
import { EntityObject } from '@ChronoManager/models/entity'
|
||||
import { EventObject } from '@ChronoManager/models/event'
|
||||
import { TaskObject } from '@ChronoManager/models/task'
|
||||
import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore'
|
||||
import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore'
|
||||
|
||||
type ChronoEntityProperties = EventObject | TaskObject
|
||||
|
||||
export function useChronoEntityActions(
|
||||
entitiesStore: ReturnType<typeof useEntitiesStore>,
|
||||
collectionsStore: ReturnType<typeof useCollectionsStore>,
|
||||
) {
|
||||
function getEntityData(entity: EntityObject): Record<string, unknown> {
|
||||
const properties = entity.properties as ChronoEntityProperties
|
||||
const now = new Date().toISOString()
|
||||
|
||||
properties.modified = now as never
|
||||
if (!entity.identifier) {
|
||||
properties.created = now as never
|
||||
}
|
||||
|
||||
return properties.toJson() as unknown as Record<string, unknown>
|
||||
}
|
||||
|
||||
function resolveCollection(entity: EntityObject, collection?: CollectionObject | null): CollectionObject {
|
||||
if (collection instanceof CollectionObject) {
|
||||
return collection
|
||||
}
|
||||
|
||||
const foundCollection = collectionsStore.collections.find((item) => item.identifier === entity.collection)
|
||||
if (!foundCollection) {
|
||||
throw new Error('Collection not found for entity operation')
|
||||
}
|
||||
|
||||
return foundCollection
|
||||
}
|
||||
|
||||
async function saveEntity(entity: EntityObject, collection: CollectionObject): Promise<EntityObject> {
|
||||
const data = getEntityData(entity)
|
||||
|
||||
if (!entity.identifier) {
|
||||
return entitiesStore.create(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
return entitiesStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
entity.identifier,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
async function deleteEntity(entity: EntityObject, collection?: CollectionObject | null): Promise<void> {
|
||||
const resolvedCollection = resolveCollection(entity, collection)
|
||||
|
||||
await entitiesStore.delete(
|
||||
resolvedCollection.provider,
|
||||
resolvedCollection.service,
|
||||
resolvedCollection.identifier,
|
||||
entity.identifier,
|
||||
)
|
||||
}
|
||||
|
||||
async function toggleTaskCompletion(entity: EntityObject, collection?: CollectionObject | null): Promise<EntityObject> {
|
||||
const resolvedCollection = resolveCollection(entity, collection)
|
||||
const taskData = entity.properties as TaskObject
|
||||
const isCompleted = taskData.status === 'completed'
|
||||
|
||||
taskData.status = isCompleted ? 'needs-action' : 'completed'
|
||||
taskData.completedOn = isCompleted ? null : new Date().toISOString() as never
|
||||
taskData.progress = isCompleted ? null : 100
|
||||
taskData.modified = new Date().toISOString() as never
|
||||
|
||||
return entitiesStore.update(
|
||||
resolvedCollection.provider,
|
||||
resolvedCollection.service,
|
||||
resolvedCollection.identifier,
|
||||
entity.identifier,
|
||||
taskData.toJson(),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
saveEntity,
|
||||
deleteEntity,
|
||||
toggleTaskCompletion,
|
||||
resolveCollection,
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { computed, onMounted, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useDisplay } from 'vuetify';
|
||||
import { useModuleStore } from '@KTXC/stores/moduleStore';
|
||||
import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore';
|
||||
import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore';
|
||||
import { useServicesStore } from '@ChronoManager/stores/servicesStore';
|
||||
import { CollectionObject } from '@ChronoManager/models/collection';
|
||||
import { EntityObject } from '@ChronoManager/models/entity';
|
||||
import { EventObject } from '@ChronoManager/models/event';
|
||||
import { TaskObject } from '@ChronoManager/models/task';
|
||||
import { ServiceObject } from '@ChronoManager/models/service';
|
||||
import type { CalendarView as CalendarViewType } from '@/types';
|
||||
import { useChronoStore } from '@/stores/chronoStore';
|
||||
import CollectionList from '@/components/CollectionList.vue';
|
||||
import CollectionEditor from '@/components/CollectionEditor.vue';
|
||||
import CalendarView from '@/components/CalendarView.vue';
|
||||
@@ -29,423 +23,82 @@ const isChronoManagerAvailable = computed(() => {
|
||||
return moduleStore.has('chrono_manager') || moduleStore.has('ChronoManager')
|
||||
});
|
||||
|
||||
// Stores
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const entitiesStore = useEntitiesStore();
|
||||
const servicesStore = useServicesStore();
|
||||
const chronoStore = useChronoStore();
|
||||
const route = useRoute();
|
||||
|
||||
// View state
|
||||
const viewMode = ref<'calendar' | 'tasks'>('calendar');
|
||||
const calendarView = ref<CalendarViewType>('days');
|
||||
const currentDate = ref(new Date());
|
||||
const sidebarVisible = ref(true);
|
||||
const {
|
||||
calendarView,
|
||||
daysViewSpan,
|
||||
agendaViewSpan,
|
||||
currentDate,
|
||||
sidebarVisible,
|
||||
selectedCollection,
|
||||
showEventEditor,
|
||||
showTaskEditor,
|
||||
showCollectionEditor,
|
||||
selectedEntity,
|
||||
entityEditorMode,
|
||||
editingCollection,
|
||||
collectionEditorMode,
|
||||
collectionEditorType,
|
||||
calendars,
|
||||
taskLists,
|
||||
isTaskView,
|
||||
filteredEvents,
|
||||
filteredTasks,
|
||||
collections,
|
||||
} = storeToRefs(chronoStore);
|
||||
|
||||
// Data - using manager objects directly
|
||||
const collections = ref<CollectionObject[]>([]);
|
||||
const entities = ref<EntityObject[]>([]);
|
||||
const selectedCollection = ref<CollectionObject | null>(null);
|
||||
const {
|
||||
initialize,
|
||||
setViewMode,
|
||||
setCalendarView,
|
||||
setDaysViewSpan,
|
||||
setAgendaViewSpan,
|
||||
selectCalendar,
|
||||
openEditCalendar,
|
||||
toggleCalendarVisibility,
|
||||
selectTaskList,
|
||||
openEditTaskList,
|
||||
createEventFromDate,
|
||||
startEditingSelectedEntity,
|
||||
cancelEditingSelectedEntity,
|
||||
closeEntityEditor,
|
||||
editEvent,
|
||||
editTask,
|
||||
toggleTaskComplete,
|
||||
saveEvent,
|
||||
deleteEvent,
|
||||
saveTask,
|
||||
deleteTask,
|
||||
saveCollection,
|
||||
deleteCollection,
|
||||
openCreateCalendar,
|
||||
openCreateTaskList,
|
||||
} = chronoStore;
|
||||
|
||||
// Computed - filter collections and entities
|
||||
const calendars = computed(() => {
|
||||
return collections.value.filter(col => col.properties.contents?.event);
|
||||
});
|
||||
|
||||
const taskLists = computed(() => {
|
||||
return collections.value.filter(col => col.properties.contents?.task);
|
||||
});
|
||||
|
||||
const events = computed(() => {
|
||||
return entities.value.filter(entity => entity.properties && (entity.properties as any).type === 'event');
|
||||
});
|
||||
|
||||
const tasks = computed(() => {
|
||||
return entities.value.filter(entity => entity.properties && (entity.properties as any).type === 'task');
|
||||
});
|
||||
|
||||
// Dialog state
|
||||
const showEventEditor = ref(false);
|
||||
const showTaskEditor = ref(false);
|
||||
const showCollectionEditor = ref(false);
|
||||
const selectedEntity = ref<EntityObject | null>(null);
|
||||
const entityEditorMode = ref<'edit' | 'view'>('view');
|
||||
const editingCollection = ref<CollectionObject | null>(null);
|
||||
const collectionEditorMode = ref<'create' | 'edit'>('create');
|
||||
const collectionEditorType = ref<'calendar' | 'tasklist'>('calendar');
|
||||
|
||||
// Computed
|
||||
const isTaskView = computed(() => viewMode.value === 'tasks');
|
||||
|
||||
const filteredEvents = computed(() => {
|
||||
const visibleCalendarIds = calendars.value
|
||||
.filter(cal => cal.properties.visibility !== false)
|
||||
.map(cal => cal.identifier);
|
||||
|
||||
return events.value.filter(event =>
|
||||
visibleCalendarIds.includes(event.collection)
|
||||
);
|
||||
});
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
return tasks.value;
|
||||
});
|
||||
|
||||
// Methods
|
||||
function selectCalendar(calendar: CollectionObject) {
|
||||
selectedCollection.value = calendar;
|
||||
console.log('[Chrono] - Selected calendar:', calendar);
|
||||
function syncViewModeFromRoute() {
|
||||
const routePath = route.path;
|
||||
const mode = routePath.endsWith('/tasks') ? 'tasks' : 'calendar';
|
||||
setViewMode(mode);
|
||||
}
|
||||
|
||||
function createCalendar() {
|
||||
editingCollection.value = new CollectionObject();
|
||||
editingCollection.value.properties.contents = { event: true };
|
||||
collectionEditorMode.value = 'create';
|
||||
collectionEditorType.value = 'calendar';
|
||||
showCollectionEditor.value = true;
|
||||
function handleCalendarViewChange(view: 'days' | 'month' | 'agenda') {
|
||||
setCalendarView(view);
|
||||
}
|
||||
|
||||
function editCalendar(collection: CollectionObject) {
|
||||
editingCollection.value = collection;
|
||||
collectionEditorMode.value = 'edit';
|
||||
collectionEditorType.value = 'calendar';
|
||||
showCollectionEditor.value = true;
|
||||
}
|
||||
|
||||
async function toggleCalendarVisibility(collection: CollectionObject) {
|
||||
try {
|
||||
await collectionsStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
collection.properties
|
||||
);
|
||||
console.log('[Chrono] - Toggled calendar visibility:', collection);
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to toggle calendar visibility:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function selectTaskList(list: CollectionObject) {
|
||||
selectedCollection.value = list;
|
||||
console.log('[Chrono] - Selected task list:', list);
|
||||
}
|
||||
|
||||
function createTaskList() {
|
||||
editingCollection.value = new CollectionObject();
|
||||
editingCollection.value.properties.contents = { task: true };
|
||||
collectionEditorMode.value = 'create';
|
||||
collectionEditorType.value = 'tasklist';
|
||||
showCollectionEditor.value = true;
|
||||
}
|
||||
|
||||
function editTaskList(collection: CollectionObject) {
|
||||
editingCollection.value = collection;
|
||||
collectionEditorMode.value = 'edit';
|
||||
collectionEditorType.value = 'tasklist';
|
||||
showCollectionEditor.value = true;
|
||||
}
|
||||
|
||||
function createEvent() {
|
||||
// Select first calendar collection or use selected
|
||||
if (!selectedCollection.value && calendars.value.length > 0) {
|
||||
selectedCollection.value = calendars.value[0];
|
||||
}
|
||||
|
||||
if (!selectedCollection.value) {
|
||||
console.warn('[Chrono] - No calendar collection available');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedEntity.value = new EntityObject();
|
||||
selectedEntity.value.properties = new EventObject();
|
||||
entityEditorMode.value = 'edit';
|
||||
showEventEditor.value = true;
|
||||
}
|
||||
|
||||
function editEvent(entity: EntityObject) {
|
||||
selectedEntity.value = entity;
|
||||
entityEditorMode.value = 'view';
|
||||
showEventEditor.value = true;
|
||||
}
|
||||
|
||||
async function saveEvent(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = selectedCollection.value;
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const eventData = entity.properties as EventObject;
|
||||
eventData.modified = new Date().toISOString();
|
||||
|
||||
if (!entity.identifier) {
|
||||
eventData.created = new Date().toISOString();
|
||||
selectedEntity.value = await entitiesStore.create(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
eventData.toJson()
|
||||
);
|
||||
} else {
|
||||
selectedEntity.value = await entitiesStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
entity.identifier,
|
||||
eventData.toJson()
|
||||
);
|
||||
}
|
||||
|
||||
entityEditorMode.value = 'view';
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to save event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = collections.value.find(c => c.identifier === entity.collection);
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection found');
|
||||
return;
|
||||
}
|
||||
|
||||
await entitiesStore.delete(collection.provider, collection.service, collection.identifier, entity.identifier);
|
||||
selectedEntity.value = null;
|
||||
entityEditorMode.value = 'view';
|
||||
showEventEditor.value = false;
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to delete event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDateClick(date: Date) {
|
||||
// Select first calendar collection or use selected
|
||||
if (!selectedCollection.value && calendars.value.length > 0) {
|
||||
selectedCollection.value = calendars.value[0];
|
||||
}
|
||||
|
||||
if (!selectedCollection.value) {
|
||||
console.warn('[Chrono] - No calendar collection available');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedEntity.value = new EntityObject();
|
||||
selectedEntity.value.properties = new EventObject();
|
||||
const eventData = selectedEntity.value.properties as EventObject;
|
||||
eventData.startsOn = date.toISOString();
|
||||
eventData.endsOn = new Date(date.getTime() + 60 * 60 * 1000).toISOString();
|
||||
entityEditorMode.value = 'edit';
|
||||
showEventEditor.value = true;
|
||||
}
|
||||
|
||||
function handleEditorEdit() {
|
||||
console.log('[Chrono] - Editor editing started');
|
||||
entityEditorMode.value = 'edit';
|
||||
}
|
||||
|
||||
function handleEditorCancel() {
|
||||
console.log('[Chrono] - Editor editing cancelled');
|
||||
entityEditorMode.value = 'view';
|
||||
}
|
||||
|
||||
function handleEditorClose() {
|
||||
console.log('[Chrono] - Editor closed');
|
||||
selectedEntity.value = null;
|
||||
entityEditorMode.value = 'view';
|
||||
showEventEditor.value = false;
|
||||
showTaskEditor.value = false;
|
||||
}
|
||||
|
||||
function createTask() {
|
||||
// Select first task list collection or use selected
|
||||
if (!selectedCollection.value && taskLists.value.length > 0) {
|
||||
selectedCollection.value = taskLists.value[0];
|
||||
}
|
||||
|
||||
if (!selectedCollection.value) {
|
||||
console.warn('[Chrono] - No task list available');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedEntity.value = new EntityObject();
|
||||
selectedEntity.value.properties = new TaskObject();
|
||||
entityEditorMode.value = 'edit';
|
||||
showTaskEditor.value = true;
|
||||
}
|
||||
|
||||
function editTask(entity: EntityObject) {
|
||||
selectedEntity.value = entity;
|
||||
entityEditorMode.value = 'view';
|
||||
showTaskEditor.value = true;
|
||||
}
|
||||
|
||||
async function saveTask(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = selectedCollection.value;
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const taskData = entity.properties as TaskObject;
|
||||
taskData.modified = new Date().toISOString();
|
||||
|
||||
if (!entity.identifier) {
|
||||
taskData.created = new Date().toISOString();
|
||||
selectedEntity.value = await entitiesStore.create(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
taskData.toJson()
|
||||
);
|
||||
} else {
|
||||
selectedEntity.value = await entitiesStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
entity.identifier,
|
||||
taskData.toJson()
|
||||
);
|
||||
}
|
||||
|
||||
entityEditorMode.value = 'view';
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to save task:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = collections.value.find(c => c.identifier === entity.collection);
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection found');
|
||||
return;
|
||||
}
|
||||
|
||||
await entitiesStore.delete(collection.provider, collection.service, collection.identifier, entity.identifier);
|
||||
selectedEntity.value = null;
|
||||
entityEditorMode.value = 'view';
|
||||
showTaskEditor.value = false;
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to delete task:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTaskComplete(taskId: string | number) {
|
||||
try {
|
||||
const entity = entities.value.find(e => e.identifier === taskId);
|
||||
if (!entity || !entity.properties) return;
|
||||
|
||||
const collection = collections.value.find(c => c.identifier === entity.collection);
|
||||
if (!collection) return;
|
||||
|
||||
const taskData = entity.properties as TaskObject;
|
||||
const isCompleted = taskData.status === 'completed';
|
||||
|
||||
taskData.status = isCompleted ? 'needs-action' : 'completed';
|
||||
taskData.completedOn = isCompleted ? null : new Date().toISOString();
|
||||
taskData.progress = isCompleted ? null : 100;
|
||||
taskData.modified = new Date().toISOString();
|
||||
|
||||
await entitiesStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
entity.identifier,
|
||||
taskData.toJson()
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to toggle task completion:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCollection(collection: CollectionObject, service: ServiceObject) {
|
||||
try {
|
||||
if (collectionEditorMode.value === 'create') {
|
||||
await collectionsStore.create(
|
||||
service.provider,
|
||||
service.identifier || '',
|
||||
null,
|
||||
collection.properties
|
||||
);
|
||||
console.log('[Chrono] - Created collection:', collection);
|
||||
} else {
|
||||
await collectionsStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
collection.properties
|
||||
);
|
||||
console.log('[Chrono] - Modified collection:', collection);
|
||||
}
|
||||
// Reload collections
|
||||
collections.value = Object.values(await collectionsStore.list());
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to save collection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCollection(collection: CollectionObject) {
|
||||
try {
|
||||
await collectionsStore.delete(collection.provider, collection.service, collection.identifier);
|
||||
console.log('[Chrono] - Deleted collection:', collection);
|
||||
// Reload collections
|
||||
collections.value = Object.values(await collectionsStore.list());
|
||||
if (selectedCollection.value?.identifier === collection.identifier) {
|
||||
selectedCollection.value = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to delete collection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize data from stores
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load collections (calendars and task lists)
|
||||
collections.value = Object.values(await collectionsStore.list());
|
||||
|
||||
// Load entities (events and tasks) for available collection sources only
|
||||
const sources = collections.value.reduce<Record<string, Record<string, Record<string | number, true>>>>((acc, collection) => {
|
||||
if (!acc[collection.provider]) {
|
||||
acc[collection.provider] = {};
|
||||
}
|
||||
|
||||
const serviceKey = String(collection.service);
|
||||
if (!acc[collection.provider][serviceKey]) {
|
||||
acc[collection.provider][serviceKey] = {};
|
||||
}
|
||||
|
||||
acc[collection.provider][serviceKey][collection.identifier] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(sources).length > 0) {
|
||||
entities.value = Object.values(await entitiesStore.list(sources));
|
||||
} else {
|
||||
entities.value = [];
|
||||
}
|
||||
|
||||
console.log('[Chrono] - Loaded data from ChronoManager:', {
|
||||
collections: collections.value.length,
|
||||
calendars: calendars.value.length,
|
||||
events: events.value.length,
|
||||
tasks: tasks.value.length,
|
||||
taskLists: taskLists.value.length,
|
||||
});
|
||||
syncViewModeFromRoute();
|
||||
await initialize();
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to load data from ChronoManager:', error);
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => route.fullPath, () => {
|
||||
syncViewModeFromRoute();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -467,33 +120,16 @@ onMounted(async () => {
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!-- View Toggle -->
|
||||
<v-btn-toggle
|
||||
v-model="viewMode"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="mr-2"
|
||||
>
|
||||
<v-btn value="calendar" size="small">
|
||||
<v-icon>mdi-calendar</v-icon>
|
||||
<span class="ml-1 d-none d-sm-inline">Calendar</span>
|
||||
</v-btn>
|
||||
<v-btn value="tasks" size="small">
|
||||
<v-icon>mdi-checkbox-marked-outline</v-icon>
|
||||
<span class="ml-1 d-none d-sm-inline">Tasks</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<!-- View Switcher for Calendar -->
|
||||
<v-btn-toggle
|
||||
v-if="!isTaskView"
|
||||
v-model="calendarView"
|
||||
:model-value="calendarView"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
mandatory
|
||||
class="mr-2"
|
||||
@update:model-value="handleCalendarViewChange"
|
||||
>
|
||||
<v-btn value="days" size="small">Days</v-btn>
|
||||
<v-btn value="month" size="small">Month</v-btn>
|
||||
@@ -514,18 +150,20 @@ onMounted(async () => {
|
||||
<div class="pa-4">
|
||||
<CollectionList
|
||||
v-if="!isTaskView"
|
||||
:collections="collections"
|
||||
:selected-collection="selectedCollection"
|
||||
type="calendar"
|
||||
@select="selectCalendar"
|
||||
@edit="editCalendar"
|
||||
@edit="openEditCalendar"
|
||||
@toggle-visibility="toggleCalendarVisibility"
|
||||
/>
|
||||
<CollectionList
|
||||
v-else
|
||||
:collections="collections"
|
||||
:selected-collection="selectedCollection"
|
||||
type="tasklist"
|
||||
@select="selectTaskList"
|
||||
@edit="editTaskList"
|
||||
@edit="openEditTaskList"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
@@ -534,7 +172,7 @@ onMounted(async () => {
|
||||
color="primary"
|
||||
block
|
||||
class="mt-3"
|
||||
@click="createCalendar"
|
||||
@click="openCreateCalendar"
|
||||
>
|
||||
<v-icon start>mdi-plus</v-icon>
|
||||
New Calendar
|
||||
@@ -545,7 +183,7 @@ onMounted(async () => {
|
||||
color="primary"
|
||||
block
|
||||
class="mt-3"
|
||||
@click="createTaskList"
|
||||
@click="openCreateTaskList"
|
||||
>
|
||||
<v-icon start>mdi-plus</v-icon>
|
||||
New Task List
|
||||
@@ -591,8 +229,12 @@ onMounted(async () => {
|
||||
:current-date="currentDate"
|
||||
:events="filteredEvents"
|
||||
:calendars="calendars"
|
||||
:initial-days-span="daysViewSpan"
|
||||
:initial-agenda-view-span="agendaViewSpan"
|
||||
@event-click="editEvent"
|
||||
@date-click="handleDateClick"
|
||||
@date-click="createEventFromDate"
|
||||
@update:days-span="setDaysViewSpan"
|
||||
@update:agenda-span="setAgendaViewSpan"
|
||||
/>
|
||||
|
||||
<TaskView
|
||||
@@ -615,9 +257,9 @@ onMounted(async () => {
|
||||
:calendars="calendars"
|
||||
@save="saveEvent"
|
||||
@delete="deleteEvent"
|
||||
@edit="handleEditorEdit"
|
||||
@cancel="handleEditorCancel"
|
||||
@close="handleEditorClose"
|
||||
@edit="startEditingSelectedEntity"
|
||||
@cancel="cancelEditingSelectedEntity"
|
||||
@close="closeEntityEditor"
|
||||
/>
|
||||
</v-dialog>
|
||||
|
||||
@@ -631,9 +273,9 @@ onMounted(async () => {
|
||||
:lists="taskLists"
|
||||
@save="saveTask"
|
||||
@delete="deleteTask"
|
||||
@edit="handleEditorEdit"
|
||||
@cancel="handleEditorCancel"
|
||||
@close="handleEditorClose"
|
||||
@edit="startEditingSelectedEntity"
|
||||
@cancel="cancelEditingSelectedEntity"
|
||||
@close="closeEntityEditor"
|
||||
/>
|
||||
</v-dialog>
|
||||
|
||||
|
||||
424
src/stores/chronoStore.ts
Normal file
424
src/stores/chronoStore.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useUserStore } from '@KTXC/stores/userStore'
|
||||
import { CollectionObject } from '@ChronoManager/models/collection'
|
||||
import { EntityObject } from '@ChronoManager/models/entity'
|
||||
import { EventObject } from '@ChronoManager/models/event'
|
||||
import { TaskObject } from '@ChronoManager/models/task'
|
||||
import { ServiceObject } from '@ChronoManager/models/service'
|
||||
import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore'
|
||||
import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore'
|
||||
import type { SourceSelector } from '@ChronoManager/types'
|
||||
import type { CalendarView as CalendarViewType } from '@/types'
|
||||
import {
|
||||
AGENDA_VIEW_SPANS,
|
||||
DAYS_VIEW_SPANS,
|
||||
type AgendaViewSpan,
|
||||
type DaysViewSpan,
|
||||
} from '@/types/spans'
|
||||
import { useChronoEntityActions } from '@/composables/useChronoEntityActions'
|
||||
|
||||
type EntitySources = Record<string, Record<string, Record<string | number, true>>>
|
||||
|
||||
export const useChronoStore = defineStore('chronoStore', () => {
|
||||
const userStore = useUserStore()
|
||||
const collectionsStore = useCollectionsStore()
|
||||
const entitiesStore = useEntitiesStore()
|
||||
const entityActions = useChronoEntityActions(entitiesStore, collectionsStore)
|
||||
|
||||
const savedCalendarView = userStore.getSetting('chrono.calendarView')
|
||||
const savedDaysViewSpan = userStore.getSetting('chrono.daysViewSpan')
|
||||
const savedAgendaViewSpan = userStore.getSetting('chrono.agendaViewSpan')
|
||||
|
||||
const viewMode = ref<'calendar' | 'tasks'>('calendar')
|
||||
const calendarView = ref<CalendarViewType>(
|
||||
savedCalendarView === 'month' || savedCalendarView === 'agenda' || savedCalendarView === 'days'
|
||||
? savedCalendarView
|
||||
: 'days',
|
||||
)
|
||||
const daysViewSpan = ref<DaysViewSpan>(
|
||||
typeof savedDaysViewSpan === 'string' && DAYS_VIEW_SPANS.includes(savedDaysViewSpan as DaysViewSpan)
|
||||
? (savedDaysViewSpan as DaysViewSpan)
|
||||
: '7d',
|
||||
)
|
||||
|
||||
const agendaViewSpan = ref<AgendaViewSpan>(
|
||||
typeof savedAgendaViewSpan === 'string' && AGENDA_VIEW_SPANS.includes(savedAgendaViewSpan as AgendaViewSpan)
|
||||
? (savedAgendaViewSpan as AgendaViewSpan)
|
||||
: '1w',
|
||||
)
|
||||
const currentDate = ref(new Date())
|
||||
const sidebarVisible = ref(true)
|
||||
|
||||
const selectedCollection = shallowRef<CollectionObject | null>(null)
|
||||
const selectedEntity = shallowRef<EntityObject | null>(null)
|
||||
|
||||
const showEventEditor = ref(false)
|
||||
const showTaskEditor = ref(false)
|
||||
const showCollectionEditor = ref(false)
|
||||
|
||||
const entityEditorMode = ref<'edit' | 'view'>('view')
|
||||
const editingCollection = shallowRef<CollectionObject | null>(null)
|
||||
const collectionEditorMode = ref<'create' | 'edit'>('create')
|
||||
const collectionEditorType = ref<'calendar' | 'tasklist'>('calendar')
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const collections = computed(() => collectionsStore.collections)
|
||||
const entities = computed(() => entitiesStore.entities)
|
||||
|
||||
const calendars = computed(() => {
|
||||
return collections.value.filter(col => col.properties.contents?.event)
|
||||
})
|
||||
|
||||
const taskLists = computed(() => {
|
||||
return collections.value.filter(col => col.properties.contents?.task)
|
||||
})
|
||||
|
||||
const events = computed(() => {
|
||||
return entities.value.filter(entity => entity.properties instanceof EventObject)
|
||||
})
|
||||
|
||||
const tasks = computed(() => {
|
||||
return entities.value.filter(entity => entity.properties instanceof TaskObject)
|
||||
})
|
||||
|
||||
const isTaskView = computed(() => viewMode.value === 'tasks')
|
||||
|
||||
const filteredEvents = computed(() => {
|
||||
const visibleCalendarIds = calendars.value
|
||||
.filter(cal => cal.properties.visibility !== false)
|
||||
.map(cal => cal.identifier)
|
||||
|
||||
return events.value.filter(event => visibleCalendarIds.includes(event.collection))
|
||||
})
|
||||
|
||||
const filteredTasks = computed(() => tasks.value)
|
||||
|
||||
function buildSources(collectionItems: CollectionObject[]): EntitySources {
|
||||
return collectionItems.reduce<EntitySources>((acc, collection) => {
|
||||
if (!acc[collection.provider]) {
|
||||
acc[collection.provider] = {}
|
||||
}
|
||||
|
||||
const serviceKey = String(collection.service)
|
||||
if (!acc[collection.provider][serviceKey]) {
|
||||
acc[collection.provider][serviceKey] = {}
|
||||
}
|
||||
|
||||
acc[collection.provider][serviceKey][collection.identifier] = true
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
function setViewMode(mode: 'calendar' | 'tasks') {
|
||||
viewMode.value = mode
|
||||
}
|
||||
|
||||
function setCalendarView(view: CalendarViewType) {
|
||||
calendarView.value = view
|
||||
userStore.setSetting('chrono.calendarView', view)
|
||||
}
|
||||
|
||||
function setDaysViewSpan(span: DaysViewSpan) {
|
||||
if (!DAYS_VIEW_SPANS.includes(span)) {
|
||||
return
|
||||
}
|
||||
|
||||
daysViewSpan.value = span
|
||||
userStore.setSetting('chrono.daysViewSpan', span)
|
||||
}
|
||||
|
||||
function setAgendaViewSpan(span: AgendaViewSpan) {
|
||||
agendaViewSpan.value = span
|
||||
userStore.setSetting('chrono.agendaViewSpan', span)
|
||||
}
|
||||
|
||||
function selectCalendar(calendar: CollectionObject) {
|
||||
selectedCollection.value = calendar
|
||||
}
|
||||
|
||||
function selectTaskList(list: CollectionObject) {
|
||||
selectedCollection.value = list
|
||||
}
|
||||
|
||||
function openCreateCalendar() {
|
||||
editingCollection.value = new CollectionObject()
|
||||
editingCollection.value.properties.fromJson({
|
||||
...editingCollection.value.properties.toJson(),
|
||||
contents: { event: true },
|
||||
})
|
||||
collectionEditorMode.value = 'create'
|
||||
collectionEditorType.value = 'calendar'
|
||||
showCollectionEditor.value = true
|
||||
}
|
||||
|
||||
function openCreateTaskList() {
|
||||
editingCollection.value = new CollectionObject()
|
||||
editingCollection.value.properties.fromJson({
|
||||
...editingCollection.value.properties.toJson(),
|
||||
contents: { task: true },
|
||||
})
|
||||
collectionEditorMode.value = 'create'
|
||||
collectionEditorType.value = 'tasklist'
|
||||
showCollectionEditor.value = true
|
||||
}
|
||||
|
||||
function openEditCalendar(collection: CollectionObject) {
|
||||
editingCollection.value = collection
|
||||
collectionEditorMode.value = 'edit'
|
||||
collectionEditorType.value = 'calendar'
|
||||
showCollectionEditor.value = true
|
||||
}
|
||||
|
||||
function openEditTaskList(collection: CollectionObject) {
|
||||
editingCollection.value = collection
|
||||
collectionEditorMode.value = 'edit'
|
||||
collectionEditorType.value = 'tasklist'
|
||||
showCollectionEditor.value = true
|
||||
}
|
||||
|
||||
function ensureSelectedCollection(targetType: 'calendar' | 'tasklist'): CollectionObject | null {
|
||||
const list = targetType === 'calendar' ? calendars.value : taskLists.value
|
||||
|
||||
if (!selectedCollection.value && list.length > 0) {
|
||||
selectedCollection.value = list[0]
|
||||
}
|
||||
|
||||
return selectedCollection.value
|
||||
}
|
||||
|
||||
function createEvent() {
|
||||
const collection = ensureSelectedCollection('calendar')
|
||||
if (!collection) {
|
||||
return
|
||||
}
|
||||
|
||||
const entity = new EntityObject()
|
||||
entity.properties = new EventObject()
|
||||
selectedEntity.value = entity
|
||||
entityEditorMode.value = 'edit'
|
||||
showEventEditor.value = true
|
||||
}
|
||||
|
||||
function editEvent(entity: EntityObject) {
|
||||
selectedEntity.value = entity
|
||||
entityEditorMode.value = 'view'
|
||||
showEventEditor.value = true
|
||||
}
|
||||
|
||||
function createTask() {
|
||||
const collection = ensureSelectedCollection('tasklist')
|
||||
if (!collection) {
|
||||
return
|
||||
}
|
||||
|
||||
const entity = new EntityObject()
|
||||
entity.properties = new TaskObject()
|
||||
selectedEntity.value = entity
|
||||
entityEditorMode.value = 'edit'
|
||||
showTaskEditor.value = true
|
||||
}
|
||||
|
||||
function editTask(entity: EntityObject) {
|
||||
selectedEntity.value = entity
|
||||
entityEditorMode.value = 'view'
|
||||
showTaskEditor.value = true
|
||||
}
|
||||
|
||||
function createEventFromDate(date: Date) {
|
||||
const collection = ensureSelectedCollection('calendar')
|
||||
if (!collection) {
|
||||
return
|
||||
}
|
||||
|
||||
const entity = new EntityObject()
|
||||
entity.properties = new EventObject()
|
||||
const eventData = entity.properties as EventObject
|
||||
eventData.startsOn = date.toISOString()
|
||||
eventData.endsOn = new Date(date.getTime() + 60 * 60 * 1000).toISOString()
|
||||
|
||||
selectedEntity.value = entity
|
||||
entityEditorMode.value = 'edit'
|
||||
showEventEditor.value = true
|
||||
}
|
||||
|
||||
function startEditingSelectedEntity() {
|
||||
entityEditorMode.value = 'edit'
|
||||
}
|
||||
|
||||
function cancelEditingSelectedEntity() {
|
||||
entityEditorMode.value = 'view'
|
||||
}
|
||||
|
||||
function closeEntityEditor() {
|
||||
selectedEntity.value = null
|
||||
entityEditorMode.value = 'view'
|
||||
showEventEditor.value = false
|
||||
showTaskEditor.value = false
|
||||
}
|
||||
|
||||
async function toggleCalendarVisibility(collection: CollectionObject) {
|
||||
const nextVisibility = collection.properties.visibility === false
|
||||
const updated = collection.clone()
|
||||
updated.properties.visibility = nextVisibility
|
||||
|
||||
await collectionsStore.update(
|
||||
updated.provider,
|
||||
updated.service,
|
||||
updated.identifier,
|
||||
updated.properties,
|
||||
)
|
||||
}
|
||||
|
||||
async function saveEvent(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
const targetCollection = collection instanceof CollectionObject
|
||||
? collection
|
||||
: selectedCollection.value
|
||||
|
||||
if (!targetCollection) {
|
||||
throw new Error('No calendar collection selected')
|
||||
}
|
||||
|
||||
selectedEntity.value = await entityActions.saveEntity(entity, targetCollection)
|
||||
selectedCollection.value = targetCollection
|
||||
entityEditorMode.value = 'view'
|
||||
}
|
||||
|
||||
async function saveTask(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
const targetCollection = collection instanceof CollectionObject
|
||||
? collection
|
||||
: selectedCollection.value
|
||||
|
||||
if (!targetCollection) {
|
||||
throw new Error('No task list selected')
|
||||
}
|
||||
|
||||
selectedEntity.value = await entityActions.saveEntity(entity, targetCollection)
|
||||
selectedCollection.value = targetCollection
|
||||
entityEditorMode.value = 'view'
|
||||
}
|
||||
|
||||
async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
await entityActions.deleteEntity(entity, collection)
|
||||
closeEntityEditor()
|
||||
}
|
||||
|
||||
async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
await entityActions.deleteEntity(entity, collection)
|
||||
closeEntityEditor()
|
||||
}
|
||||
|
||||
async function toggleTaskComplete(taskId: string | number) {
|
||||
const taskEntity = tasks.value.find(task => task.identifier === taskId)
|
||||
if (!taskEntity) {
|
||||
return
|
||||
}
|
||||
|
||||
await entityActions.toggleTaskCompletion(taskEntity)
|
||||
}
|
||||
|
||||
async function saveCollection(collection: CollectionObject, service: ServiceObject) {
|
||||
if (collectionEditorMode.value === 'create') {
|
||||
await collectionsStore.create(
|
||||
service.provider,
|
||||
service.identifier || '',
|
||||
null,
|
||||
collection.properties,
|
||||
)
|
||||
} else {
|
||||
await collectionsStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
collection.properties,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCollection(collection: CollectionObject) {
|
||||
await collectionsStore.delete(collection.provider, collection.service, collection.identifier)
|
||||
|
||||
if (selectedCollection.value?.identifier === collection.identifier) {
|
||||
selectedCollection.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
loading.value = true
|
||||
try {
|
||||
await collectionsStore.list()
|
||||
|
||||
const sources = buildSources(collections.value)
|
||||
if (Object.keys(sources).length > 0) {
|
||||
await entitiesStore.list(sources as SourceSelector)
|
||||
}
|
||||
|
||||
if (
|
||||
selectedCollection.value
|
||||
&& !collections.value.find(collection => collection.identifier === selectedCollection.value?.identifier)
|
||||
) {
|
||||
selectedCollection.value = null
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
viewMode,
|
||||
calendarView,
|
||||
daysViewSpan,
|
||||
agendaViewSpan,
|
||||
currentDate,
|
||||
sidebarVisible,
|
||||
selectedCollection,
|
||||
selectedEntity,
|
||||
showEventEditor,
|
||||
showTaskEditor,
|
||||
showCollectionEditor,
|
||||
entityEditorMode,
|
||||
editingCollection,
|
||||
collectionEditorMode,
|
||||
collectionEditorType,
|
||||
loading,
|
||||
|
||||
collections,
|
||||
entities,
|
||||
calendars,
|
||||
taskLists,
|
||||
events,
|
||||
tasks,
|
||||
isTaskView,
|
||||
filteredEvents,
|
||||
filteredTasks,
|
||||
|
||||
setViewMode,
|
||||
setCalendarView,
|
||||
setDaysViewSpan,
|
||||
setAgendaViewSpan,
|
||||
selectCalendar,
|
||||
selectTaskList,
|
||||
openCreateCalendar,
|
||||
openCreateTaskList,
|
||||
openEditCalendar,
|
||||
openEditTaskList,
|
||||
createEvent,
|
||||
editEvent,
|
||||
createTask,
|
||||
editTask,
|
||||
createEventFromDate,
|
||||
startEditingSelectedEntity,
|
||||
cancelEditingSelectedEntity,
|
||||
closeEntityEditor,
|
||||
toggleCalendarVisibility,
|
||||
toggleTaskComplete,
|
||||
saveEvent,
|
||||
deleteEvent,
|
||||
saveTask,
|
||||
deleteTask,
|
||||
saveCollection,
|
||||
deleteCollection,
|
||||
initialize,
|
||||
}
|
||||
})
|
||||
30
src/types/spans.ts
Normal file
30
src/types/spans.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export const DAYS_VIEW_SPANS = ['1d', '3d', '5d', '7d', '9d', '11d', '14d'] as const
|
||||
export type DaysViewSpan = typeof DAYS_VIEW_SPANS[number]
|
||||
|
||||
export const AGENDA_VIEW_SPANS = ['1d', '3d', '1w', '2w', '3w', '1m'] as const
|
||||
export type AgendaViewSpan = typeof AGENDA_VIEW_SPANS[number]
|
||||
|
||||
export const SPAN_TO_DAYS = {
|
||||
'1d': 1,
|
||||
'3d': 3,
|
||||
'5d': 5,
|
||||
'7d': 7,
|
||||
'9d': 9,
|
||||
'11d': 11,
|
||||
'14d': 14,
|
||||
'1w': 7,
|
||||
'2w': 14,
|
||||
'3w': 21,
|
||||
'1m': 30,
|
||||
} as const
|
||||
|
||||
export type TimeSpanLabel = keyof typeof SPAN_TO_DAYS
|
||||
|
||||
export function spanToDays(span: TimeSpanLabel): number {
|
||||
return SPAN_TO_DAYS[span]
|
||||
}
|
||||
|
||||
export function dayCountToDaysViewSpan(days: number): DaysViewSpan | null {
|
||||
const label = `${days}d`
|
||||
return DAYS_VIEW_SPANS.includes(label as DaysViewSpan) ? (label as DaysViewSpan) : null
|
||||
}
|
||||
Reference in New Issue
Block a user