feat: use module store #3

Merged
Sebastian merged 1 commits from feat/use-module-store into main 2026-02-18 00:13:35 +00:00
10 changed files with 883 additions and 612 deletions

View File

@@ -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', {

View File

@@ -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;

View File

@@ -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>

View File

@@ -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();

View File

@@ -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"

View File

@@ -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>

View 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,
}
}

View File

@@ -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
View 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
View 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
}