chore: standardize chrono provider

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-02-17 03:12:12 -05:00
parent cc4e467cef
commit f2f7bfc391
11 changed files with 282 additions and 224 deletions

View File

@@ -27,15 +27,15 @@
<v-list-subheader>{{ formatAgendaDate(date) }}</v-list-subheader> <v-list-subheader>{{ formatAgendaDate(date) }}</v-list-subheader>
<v-list-item <v-list-item
v-for="entity in dayEvents" v-for="entity in dayEvents"
:key="entity.id" :key="entity.identifier"
@click="$emit('event-click', entity)" @click="$emit('event-click', entity)"
> >
<template #prepend> <template #prepend>
<v-icon :color="getEventColor(entity)">mdi-circle</v-icon> <v-icon :color="getEventColor(entity)">mdi-circle</v-icon>
</template> </template>
<v-list-item-title>{{ entity.data?.label || 'Untitled' }}</v-list-item-title> <v-list-item-title>{{ entity.properties?.label || 'Untitled' }}</v-list-item-title>
<v-list-item-subtitle> <v-list-item-subtitle>
{{ entity.data?.allDay ? 'All day' : `${entity.data?.startsOn ? formatTime(new Date(entity.data.startsOn)) : ''} - ${entity.data?.endsOn ? formatTime(new Date(entity.data.endsOn)) : ''}` }} {{ entity.properties?.timeless ? 'All day' : `${entity.properties?.startsOn ? formatTime(new Date(entity.properties.startsOn)) : ''} - ${entity.properties?.endsOn ? formatTime(new Date(entity.properties.endsOn)) : ''}` }}
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item> </v-list-item>
</template> </template>
@@ -132,17 +132,17 @@ const groupedEvents = computed(() => {
const { start, end } = dateRange.value; const { start, end } = dateRange.value;
const filtered = props.events.filter(e => { const filtered = props.events.filter(e => {
if (!e.data?.startsOn) return false; if (!e.properties?.startsOn) return false;
const eventStart = new Date(e.data.startsOn); const eventStart = new Date(e.properties.startsOn);
return eventStart >= start && eventStart <= end; return eventStart >= start && eventStart <= end;
}); });
const sorted = filtered.sort((a, b) => const sorted = filtered.sort((a, b) =>
new Date(a.data.startsOn).getTime() - new Date(b.data.startsOn).getTime() new Date(a.properties.startsOn).getTime() - new Date(b.properties.startsOn).getTime()
); );
sorted.forEach(entity => { sorted.forEach(entity => {
const dateKey = new Date(entity.data.startsOn).toDateString(); const dateKey = new Date(entity.properties.startsOn).toDateString();
if (!grouped[dateKey]) { if (!grouped[dateKey]) {
grouped[dateKey] = []; grouped[dateKey] = [];
} }
@@ -153,9 +153,9 @@ const groupedEvents = computed(() => {
}); });
function getEventColor(entity: any): string { function getEventColor(entity: any): string {
if (entity.data?.color) return entity.data.color; if (entity.properties?.color) return entity.properties.color;
const calendar = props.calendars.find(cal => cal.id === entity.in); const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.color || '#1976D2'; return calendar?.properties?.color || '#1976D2';
} }
function formatTime(date: Date): string { function formatTime(date: Date): string {

View File

@@ -61,7 +61,7 @@ const dialogIcon = computed(() => {
// Functions // Functions
const onOpen = async () => { const onOpen = async () => {
if (services.value.length === 0) { if (services.value.length === 0) {
services.value = await servicesStore.list() services.value = Object.values(await servicesStore.list())
} }
if (!props.collection) { if (!props.collection) {
@@ -71,10 +71,10 @@ const onOpen = async () => {
// Clone the collection to avoid mutating the original // Clone the collection to avoid mutating the original
editingCollection.value = props.collection.clone() editingCollection.value = props.collection.clone()
if (props.collection.id !== null) { if (props.mode === 'edit') {
// Edit mode - find the service // Edit mode - find the service
editingCollectionService.value = services.value.find(s => editingCollectionService.value = services.value.find(s =>
s.provider === props.collection!.provider && s.id === props.collection!.service s.provider === props.collection!.provider && s.identifier === props.collection!.service
) || null ) || null
} else { } else {
// Create mode - use first service that can create // Create mode - use first service that can create
@@ -102,7 +102,7 @@ const onColorSelect = (color: string | null, closeMenu = true) => {
if (!editingCollection.value) { if (!editingCollection.value) {
return return
} }
editingCollection.value.color = color editingCollection.value.properties.color = color
if (closeMenu) { if (closeMenu) {
colorMenuOpen.value = false colorMenuOpen.value = false
} }
@@ -144,7 +144,7 @@ watch(() => props.modelValue, async (newValue) => {
label="Service" label="Service"
:items="services.filter(s => s.capabilities?.CollectionCreate)" :items="services.filter(s => s.capabilities?.CollectionCreate)"
item-title="label" item-title="label"
item-value="id" item-value="identifier"
required required
:rules="[(v: ServiceObject) => !!v || 'Service is required']" :rules="[(v: ServiceObject) => !!v || 'Service is required']"
/> />
@@ -152,7 +152,7 @@ watch(() => props.modelValue, async (newValue) => {
<div v-if="mode === 'edit'" class="mb-4"><strong>Service:</strong> {{ editingCollection.service }}</div> <div v-if="mode === 'edit'" class="mb-4"><strong>Service:</strong> {{ editingCollection.service }}</div>
<v-text-field <v-text-field
v-model="editingCollection.label" v-model="editingCollection.properties.label"
label="Label" label="Label"
required required
:rules="[(v: string) => !!v || 'Label is required']" :rules="[(v: string) => !!v || 'Label is required']"
@@ -170,7 +170,7 @@ watch(() => props.modelValue, async (newValue) => {
icon icon
variant="text" variant="text"
size="small" size="small"
:style="{ color: editingCollection.color || 'var(--v-theme-on-surface)' }" :style="{ color: editingCollection.properties.color || 'var(--v-theme-on-surface)' }"
aria-label="Select color" aria-label="Select color"
title="Select color" title="Select color"
> >
@@ -203,11 +203,11 @@ watch(() => props.modelValue, async (newValue) => {
variant="flat" variant="flat"
size="small" size="small"
class="color-menu-body__presets--swatch" class="color-menu-body__presets--swatch"
:class="{ 'color-menu-body__presets--swatch--active': editingCollection.color === color }" :class="{ 'color-menu-body__presets--swatch--active': editingCollection.properties.color === color }"
:style="{ backgroundColor: color }" :style="{ backgroundColor: color }"
@click="onColorSelect(color)"> @click="onColorSelect(color)">
<v-icon <v-icon
v-if="editingCollection.color === color" v-if="editingCollection.properties.color === color"
icon="mdi-check" icon="mdi-check"
size="x-small" size="x-small"
color="white" color="white"
@@ -218,7 +218,7 @@ watch(() => props.modelValue, async (newValue) => {
<div class="color-menu-body__picker"> <div class="color-menu-body__picker">
<v-color-picker <v-color-picker
v-model="editingCollection.color" v-model="editingCollection.properties.color"
mode="hex" mode="hex"
hide-canvas hide-canvas
width="100%" width="100%"
@@ -232,7 +232,7 @@ watch(() => props.modelValue, async (newValue) => {
</v-text-field> </v-text-field>
<v-textarea <v-textarea
v-model="editingCollection.description" v-model="editingCollection.properties.description"
label="Description" label="Description"
rows="2" rows="2"
/> />
@@ -240,7 +240,7 @@ watch(() => props.modelValue, async (newValue) => {
<v-row> <v-row>
<v-col v-if="mode === 'edit'" cols="6"> <v-col v-if="mode === 'edit'" cols="6">
<v-switch <v-switch
v-model="editingCollection.enabled" v-model="editingCollection.properties.visibility"
label="Enabled" label="Enabled"
color="primary" color="primary"
/> />

View File

@@ -29,9 +29,9 @@ const filteredCollections = computed(() => {
return collections.value.filter(collection => { return collections.value.filter(collection => {
if (props.type === 'calendar') { if (props.type === 'calendar') {
return collection.contents?.event return collection.properties.contents?.event
} else if (props.type === 'tasklist') { } else if (props.type === 'tasklist') {
return collection.contents?.task return collection.properties.contents?.task
} }
return true return true
}) })
@@ -53,7 +53,7 @@ const displayIcon = computed(() => {
onMounted(async () => { onMounted(async () => {
loading.value = true loading.value = true
try { try {
collections.value = await collectionsStore.list() collections.value = Object.values(await collectionsStore.list())
} catch (error) { } catch (error) {
console.error('[Chrono] - Failed to load collections:', error) console.error('[Chrono] - Failed to load collections:', error)
} }
@@ -71,7 +71,7 @@ const onCollectionEdit = (collection: CollectionObject) => {
} }
const onToggleVisibility = (collection: CollectionObject) => { const onToggleVisibility = (collection: CollectionObject) => {
collection.enabled = !collection.enabled collection.properties.visibility = collection.properties.visibility === false
emit('toggle-visibility', collection) emit('toggle-visibility', collection)
} }
@@ -80,7 +80,7 @@ defineExpose({
async refresh() { async refresh() {
loading.value = true loading.value = true
try { try {
collections.value = await collectionsStore.list() collections.value = Object.values(await collectionsStore.list())
} catch (error) { } catch (error) {
console.error('[Chrono] - Failed to load collections:', error) console.error('[Chrono] - Failed to load collections:', error)
} }
@@ -106,28 +106,28 @@ defineExpose({
<v-list v-else density="compact" nav class="pa-0"> <v-list v-else density="compact" nav class="pa-0">
<v-list-item <v-list-item
v-for="collection in filteredCollections" v-for="collection in filteredCollections"
:key="collection.id" :key="collection.identifier"
:value="collection.id" :value="collection.identifier"
:active="selectedCollection?.id === collection.id" :active="selectedCollection?.identifier === collection.identifier"
@click="onCollectionSelect(collection)" @click="onCollectionSelect(collection)"
rounded="lg" rounded="lg"
class="mb-1" class="mb-1"
> >
<template #prepend> <template #prepend>
<v-checkbox-btn <v-checkbox-btn
:model-value="collection.enabled !== false" :model-value="collection.properties.visibility !== false"
:color="collection.color || 'primary'" :color="collection.properties.color || 'primary'"
hide-details hide-details
@click.stop="onToggleVisibility(collection)" @click.stop="onToggleVisibility(collection)"
/> />
</template> </template>
<v-list-item-title>{{ collection.label || 'Unnamed Collection' }}</v-list-item-title> <v-list-item-title>{{ collection.properties.label || 'Unnamed Collection' }}</v-list-item-title>
<template #append> <template #append>
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon <v-icon
:color="collection.color || 'primary'" :color="collection.properties.color || 'primary'"
size="small" size="small"
class="mr-1" class="mr-1"
>mdi-circle</v-icon> >mdi-circle</v-icon>

View File

@@ -42,7 +42,7 @@
<div class="all-day-events-overlay"> <div class="all-day-events-overlay">
<div <div
v-for="segment in allDaySegments.segments" v-for="segment in allDaySegments.segments"
:key="segment.entity.id" :key="segment.entity.identifier"
class="all-day-event" class="all-day-event"
:class="{ :class="{
'is-start': segment.isStart, 'is-start': segment.isStart,
@@ -53,7 +53,7 @@
@mouseenter="emit('event-hover', { event: $event, entity: segment.entity })" @mouseenter="emit('event-hover', { event: $event, entity: segment.entity })"
@mouseleave="emit('event-hover-end')" @mouseleave="emit('event-hover-end')"
> >
<span v-if="segment.isStart" class="event-label">{{ segment.entity.data?.label || 'Untitled' }}</span> <span v-if="segment.isStart" class="event-label">{{ segment.entity.properties?.label || 'Untitled' }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -71,7 +71,7 @@
<div class="day-events" @click="handleDayClick($event, day)"> <div class="day-events" @click="handleDayClick($event, day)">
<div <div
v-for="entity in getTimedEvents(day)" v-for="entity in getTimedEvents(day)"
:key="entity.id" :key="entity.identifier"
class="day-event" class="day-event"
:style="getEventStyle(entity)" :style="getEventStyle(entity)"
@click.stop="emit('event-click', entity)" @click.stop="emit('event-click', entity)"
@@ -80,12 +80,12 @@
> >
<template v-if="daysCount <= 3"> <template v-if="daysCount <= 3">
<div class="event-time"> <div class="event-time">
{{ entity.data?.startsOn ? formatTime(new Date(entity.data.startsOn)) : '' }} - {{ entity.data?.endsOn ? formatTime(new Date(entity.data.endsOn)) : '' }} {{ entity.properties?.startsOn ? formatTime(new Date(entity.properties.startsOn)) : '' }} - {{ entity.properties?.endsOn ? formatTime(new Date(entity.properties.endsOn)) : '' }}
</div> </div>
<div class="event-title">{{ entity.data?.label || 'Untitled' }}</div> <div class="event-title">{{ entity.properties?.label || 'Untitled' }}</div>
</template> </template>
<template v-else> <template v-else>
<div class="event-title-compact">{{ entity.data?.label || 'Untitled' }}</div> <div class="event-title-compact">{{ entity.properties?.label || 'Untitled' }}</div>
</template> </template>
</div> </div>
</div> </div>
@@ -200,9 +200,9 @@ function getTimedEvents(date: Date): any[] {
} }
function getEventColor(entity: any): string { function getEventColor(entity: any): string {
if (entity.data?.color) return entity.data.color; if (entity.properties?.color) return entity.properties.color;
const calendar = props.calendars.find(cal => cal.id === entity.in); const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.color || '#1976D2'; return calendar?.properties?.color || '#1976D2';
} }
function getAllDayEventStyle(segment: MultiDaySegment) { function getAllDayEventStyle(segment: MultiDaySegment) {
@@ -215,11 +215,11 @@ function getAllDayEventStyle(segment: MultiDaySegment) {
} }
function getEventStyle(entity: any) { function getEventStyle(entity: any) {
if (!entity.data?.startsOn || !entity.data?.endsOn) { if (!entity.properties?.startsOn || !entity.properties?.endsOn) {
return { display: 'none' }; return { display: 'none' };
} }
const startTime = new Date(entity.data.startsOn); const startTime = new Date(entity.properties.startsOn);
const endTime = new Date(entity.data.endsOn); const endTime = new Date(entity.properties.endsOn);
const start = startTime.getHours() * 60 + startTime.getMinutes(); const start = startTime.getHours() * 60 + startTime.getMinutes();
const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60); const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60);

View File

@@ -32,18 +32,18 @@ const emit = defineEmits<{
// State // State
const loading = ref(false) const loading = ref(false)
const saving = ref(false) const saving = ref(false)
const selectedCollectionId = ref(props.entity?.in || props.collection?.id || props.calendars?.[0]?.id || '') const selectedCollectionId = ref(props.entity?.collection || props.collection?.identifier || props.calendars?.[0]?.identifier || '')
// Computed // Computed
const mode = computed(() => props.mode) const mode = computed(() => props.mode)
const entity = computed(() => props.entity || null) const entity = computed(() => props.entity || null)
const entityObject = computed(() => entity.value?.data as EventObject ?? null) const entityObject = computed(() => entity.value?.properties as EventObject ?? null)
const entityFresh = computed(() => entity.value?.id === null || entity.value?.id === undefined) const entityFresh = computed(() => !entity.value?.identifier)
const calendarOptions = computed(() => const calendarOptions = computed(() =>
(props.calendars || []).map(cal => ({ (props.calendars || []).map(cal => ({
title: cal.label || 'Unnamed Calendar', title: cal.properties.label || 'Unnamed Calendar',
value: cal.id, value: cal.identifier,
})) }))
) )
@@ -74,7 +74,7 @@ const cancelEdit = () => {
const saveEntity = async () => { const saveEntity = async () => {
saving.value = true saving.value = true
const targetCollection = (props.calendars || []).find(cal => cal.id === selectedCollectionId.value) const targetCollection = (props.calendars || []).find(cal => cal.identifier === selectedCollectionId.value)
emit('save', entity.value!, targetCollection || props.collection) emit('save', entity.value!, targetCollection || props.collection)
saving.value = false saving.value = false
} }
@@ -161,7 +161,7 @@ const removeNotification = (key: string) => {
<div v-else class="pa-4"> <div v-else class="pa-4">
<v-select <v-select
v-if="mode === 'edit' && !entity.id" v-if="mode === 'edit' && !entity.identifier"
v-model="selectedCollectionId" v-model="selectedCollectionId"
:items="calendarOptions" :items="calendarOptions"
label="Calendar" label="Calendar"

View File

@@ -41,25 +41,25 @@ const popupStyle = computed(() => {
}) })
const hasLocation = computed(() => { const hasLocation = computed(() => {
return props.event?.data?.locationsPhysical && Object.keys(props.event.data.locationsPhysical).length > 0 || return props.event?.properties?.locationsPhysical && Object.keys(props.event.properties.locationsPhysical).length > 0 ||
props.event?.data?.locationsVirtual && Object.keys(props.event.data.locationsVirtual).length > 0 props.event?.properties?.locationsVirtual && Object.keys(props.event.properties.locationsVirtual).length > 0
}) })
const firstLocation = computed(() => { const firstLocation = computed(() => {
if (props.event?.data?.locationsPhysical && Object.keys(props.event.data.locationsPhysical).length > 0) { if (props.event?.properties?.locationsPhysical && Object.keys(props.event.properties.locationsPhysical).length > 0) {
const key = Object.keys(props.event.data.locationsPhysical)[0] const key = Object.keys(props.event.properties.locationsPhysical)[0]
return props.event.data.locationsPhysical[key] return props.event.properties.locationsPhysical[key]
} }
if (props.event?.data?.locationsVirtual && Object.keys(props.event.data.locationsVirtual).length > 0) { if (props.event?.properties?.locationsVirtual && Object.keys(props.event.properties.locationsVirtual).length > 0) {
const key = Object.keys(props.event.data.locationsVirtual)[0] const key = Object.keys(props.event.properties.locationsVirtual)[0]
return props.event.data.locationsVirtual[key] return props.event.properties.locationsVirtual[key]
} }
return null return null
}) })
const participantCount = computed(() => { const participantCount = computed(() => {
if (!props.event?.data?.participants) return 0 if (!props.event?.properties?.participants) return 0
return Object.keys(props.event.data.participants).length return Object.keys(props.event.properties.participants).length
}) })
</script> </script>
@@ -75,18 +75,18 @@ const participantCount = computed(() => {
<v-card-text class="pa-3"> <v-card-text class="pa-3">
<div class="d-flex align-center mb-2"> <div class="d-flex align-center mb-2">
<v-icon icon="mdi-calendar" size="small" class="mr-2" /> <v-icon icon="mdi-calendar" size="small" class="mr-2" />
<div class="text-subtitle-1 font-weight-bold">{{ event.data?.label || 'Untitled Event' }}</div> <div class="text-subtitle-1 font-weight-bold">{{ event.properties?.label || 'Untitled Event' }}</div>
</div> </div>
<div class="event-popup-details"> <div class="event-popup-details">
<div class="d-flex align-center mb-1"> <div class="d-flex align-center mb-1">
<v-icon icon="mdi-clock-outline" size="small" class="mr-2 text-grey" /> <v-icon icon="mdi-clock-outline" size="small" class="mr-2 text-grey" />
<div class="text-caption"> <div class="text-caption">
<div v-if="event.data?.timeless"> <div v-if="event.properties?.timeless">
All Day - {{ new Date(event.data.startsOn).toLocaleDateString() }} All Day - {{ new Date(event.properties.startsOn).toLocaleDateString() }}
</div> </div>
<div v-else> <div v-else>
{{ formatTime(event.data?.startsOn) }} - {{ formatTime(event.data?.endsOn) }} {{ formatTime(event.properties?.startsOn) }} - {{ formatTime(event.properties?.endsOn) }}
</div> </div>
</div> </div>
</div> </div>
@@ -98,10 +98,10 @@ const participantCount = computed(() => {
</div> </div>
</div> </div>
<div v-if="event.data?.organizer" class="d-flex align-center mb-1"> <div v-if="event.properties?.organizer" class="d-flex align-center mb-1">
<v-icon icon="mdi-account" size="small" class="mr-2 text-grey" /> <v-icon icon="mdi-account" size="small" class="mr-2 text-grey" />
<div class="text-caption"> <div class="text-caption">
{{ event.data.organizer.name || event.data.organizer.address }} {{ event.properties.organizer.name || event.properties.organizer.address }}
</div> </div>
</div> </div>
@@ -112,13 +112,13 @@ const participantCount = computed(() => {
</div> </div>
</div> </div>
<div v-if="event.data?.description" class="mt-2 pt-2 border-t"> <div v-if="event.properties?.description" class="mt-2 pt-2 border-t">
<div class="text-caption text-grey">{{ event.data.description }}</div> <div class="text-caption text-grey">{{ event.properties.description }}</div>
</div> </div>
<div v-if="event.data?.tags && event.data.tags.length > 0" class="mt-2"> <div v-if="event.properties?.tags && event.properties.tags.length > 0" class="mt-2">
<v-chip <v-chip
v-for="(tag, index) in event.data.tags.slice(0, 3)" v-for="(tag, index) in event.properties.tags.slice(0, 3)"
:key="index" :key="index"
size="x-small" size="x-small"
class="mr-1" class="mr-1"
@@ -126,8 +126,8 @@ const participantCount = computed(() => {
> >
{{ tag }} {{ tag }}
</v-chip> </v-chip>
<span v-if="event.data.tags.length > 3" class="text-caption text-grey"> <span v-if="event.properties.tags.length > 3" class="text-caption text-grey">
+{{ event.data.tags.length - 3 }} more +{{ event.properties.tags.length - 3 }} more
</span> </span>
</div> </div>
</div> </div>

View File

@@ -141,18 +141,18 @@ function getMultiDaySegments(weekStart: Date, weekEnd: Date): { segments: MultiD
// Filter multiday events that overlap this week // Filter multiday events that overlap this week
const multiDayEvents = props.events.filter(entity => { const multiDayEvents = props.events.filter(entity => {
if (!entity.data?.startsOn || !isMultiDay(entity)) return false; if (!entity.properties?.startsOn || !isMultiDay(entity)) return false;
const eventStart = startOfDay(new Date(entity.data.startsOn)); const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = startOfDay(new Date(entity.data.endsOn)); const eventEnd = startOfDay(new Date(entity.properties.endsOn));
return eventStart <= weekEnd && eventEnd >= weekStart; return eventStart <= weekEnd && eventEnd >= weekStart;
}); });
// Sort: longer events first, then by start date // Sort: longer events first, then by start date
multiDayEvents.sort((a, b) => { multiDayEvents.sort((a, b) => {
const aStart = new Date(a.data.startsOn); const aStart = new Date(a.properties.startsOn);
const aEnd = new Date(a.data.endsOn); const aEnd = new Date(a.properties.endsOn);
const bStart = new Date(b.data.startsOn); const bStart = new Date(b.properties.startsOn);
const bEnd = new Date(b.data.endsOn); const bEnd = new Date(b.properties.endsOn);
const aDuration = daysDiff(aStart, aEnd); const aDuration = daysDiff(aStart, aEnd);
const bDuration = daysDiff(bStart, bEnd); const bDuration = daysDiff(bStart, bEnd);
if (bDuration !== aDuration) return bDuration - aDuration; if (bDuration !== aDuration) return bDuration - aDuration;
@@ -163,8 +163,8 @@ function getMultiDaySegments(weekStart: Date, weekEnd: Date): { segments: MultiD
const lanes: boolean[][] = []; // lanes[lane][dayOfWeek] = occupied const lanes: boolean[][] = []; // lanes[lane][dayOfWeek] = occupied
for (const entity of multiDayEvents) { for (const entity of multiDayEvents) {
const eventStart = startOfDay(new Date(entity.data.startsOn)); const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = startOfDay(new Date(entity.data.endsOn)); const eventEnd = startOfDay(new Date(entity.properties.endsOn));
// Clamp to this week // Clamp to this week
const segStart = eventStart < weekStart ? weekStart : eventStart; const segStart = eventStart < weekStart ? weekStart : eventStart;
@@ -226,9 +226,9 @@ function isToday(date: Date): boolean {
} }
function getEventColor(entity: any): string { function getEventColor(entity: any): string {
if (entity.data?.color) return entity.data.color; if (entity.properties?.color) return entity.properties.color;
const calendar = props.calendars.find(cal => cal.id === entity.in); const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.color || '#1976D2'; return calendar?.properties?.color || '#1976D2';
} }
</script> </script>
@@ -276,14 +276,14 @@ function getEventColor(entity: any): string {
<div class="cell-events"> <div class="cell-events">
<div <div
v-for="entity in getSingleDayEvents(cell.date).slice(0, MAX_VISIBLE_EVENTS - week.laneCount)" v-for="entity in getSingleDayEvents(cell.date).slice(0, MAX_VISIBLE_EVENTS - week.laneCount)"
:key="entity.id" :key="entity.identifier"
class="single-day-event" class="single-day-event"
:style="{ backgroundColor: getEventColor(entity) }" :style="{ backgroundColor: getEventColor(entity) }"
@click.stop="$emit('event-click', entity)" @click.stop="$emit('event-click', entity)"
@mouseenter="$emit('event-hover', { event: $event, entity })" @mouseenter="$emit('event-hover', { event: $event, entity })"
@mouseleave="$emit('event-hover-end')" @mouseleave="$emit('event-hover-end')"
> >
{{ entity.data?.label || 'Untitled' }} {{ entity.properties?.label || 'Untitled' }}
</div> </div>
<div <div
v-if="getHiddenCount(cell, week.laneCount) > 0" v-if="getHiddenCount(cell, week.laneCount) > 0"
@@ -300,7 +300,7 @@ function getEventColor(entity: any): string {
<div class="multiday-overlay"> <div class="multiday-overlay">
<div <div
v-for="segment in week.multiDaySegments" v-for="segment in week.multiDaySegments"
:key="`${segment.entity.id}-${weekIndex}`" :key="`${segment.entity.identifier}-${weekIndex}`"
class="multiday-event" class="multiday-event"
:class="{ :class="{
'is-start': segment.isStart, 'is-start': segment.isStart,
@@ -317,7 +317,7 @@ function getEventColor(entity: any): string {
@mouseleave="$emit('event-hover-end')" @mouseleave="$emit('event-hover-end')"
> >
<span v-if="segment.isStart" class="event-label"> <span v-if="segment.isStart" class="event-label">
{{ segment.entity.data?.label || 'Untitled' }} {{ segment.entity.properties?.label || 'Untitled' }}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<template> <template>
<v-card v-if="entity" class="task-editor"> <v-card v-if="entity" class="task-editor">
<v-card-title class="d-flex justify-space-between align-center"> <v-card-title class="d-flex justify-space-between align-center">
<span>{{ entity.id ? entity.data?.label || 'Untitled Task' : 'New Task' }}</span> <span>{{ entity.identifier ? entity.properties?.label || 'Untitled Task' : 'New Task' }}</span>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<v-btn <v-btn
v-if="mode === 'view' && canEdit" v-if="mode === 'view' && canEdit"
@@ -22,7 +22,7 @@
<v-card-text class="task-editor-content"> <v-card-text class="task-editor-content">
<v-form ref="formRef"> <v-form ref="formRef">
<v-text-field <v-text-field
v-model="entity.data.label" v-model="entity.properties.label"
label="Title" label="Title"
:rules="[v => !!v || 'Title is required']" :rules="[v => !!v || 'Title is required']"
:readonly="!isEditing" :readonly="!isEditing"
@@ -40,7 +40,7 @@
></v-select> ></v-select>
<v-textarea <v-textarea
v-model="entity.data.description" v-model="entity.properties.description"
label="Description" label="Description"
:readonly="!isEditing" :readonly="!isEditing"
:variant="isEditing ? 'outlined' : 'plain'" :variant="isEditing ? 'outlined' : 'plain'"
@@ -49,7 +49,7 @@
></v-textarea> ></v-textarea>
<v-select <v-select
v-model="entity.data.priority" v-model="entity.properties.priority"
:items="priorityOptions" :items="priorityOptions"
label="Priority" label="Priority"
:readonly="!isEditing" :readonly="!isEditing"
@@ -78,7 +78,7 @@
></v-text-field> ></v-text-field>
<v-combobox <v-combobox
v-model="entity.data.categories" v-model="entity.properties.categories"
label="Tags" label="Tags"
multiple multiple
chips chips
@@ -90,7 +90,7 @@
<div class="d-flex gap-4"> <div class="d-flex gap-4">
<v-text-field <v-text-field
v-model.number="entity.data.estimatedTime" v-model.number="entity.properties.estimatedTime"
label="Estimated Time (minutes)" label="Estimated Time (minutes)"
type="number" type="number"
:readonly="!isEditing" :readonly="!isEditing"
@@ -98,7 +98,7 @@
class="flex-1-1" class="flex-1-1"
></v-text-field> ></v-text-field>
<v-text-field <v-text-field
v-model.number="entity.data.actualTime" v-model.number="entity.properties.actualTime"
label="Actual Time (minutes)" label="Actual Time (minutes)"
type="number" type="number"
:readonly="!isEditing" :readonly="!isEditing"
@@ -108,13 +108,13 @@
</div> </div>
<!-- Metadata for view mode --> <!-- Metadata for view mode -->
<div v-if="mode === 'view' && entity.data.created" class="mt-4"> <div v-if="mode === 'view' && entity.properties.created" class="mt-4">
<v-divider class="mb-4"></v-divider> <v-divider class="mb-4"></v-divider>
<div class="text-caption text-medium-emphasis"> <div class="text-caption text-medium-emphasis">
<div>Created: {{ new Date(entity.data.created).toLocaleString() }}</div> <div>Created: {{ new Date(entity.properties.created).toLocaleString() }}</div>
<div v-if="entity.data.modified">Modified: {{ new Date(entity.data.modified).toLocaleString() }}</div> <div v-if="entity.properties.modified">Modified: {{ new Date(entity.properties.modified).toLocaleString() }}</div>
<div v-if="entity.data.status === 'completed' && entity.data.completedOn"> <div v-if="entity.properties.status === 'completed' && entity.properties.completedOn">
Completed: {{ new Date(entity.data.completedOn).toLocaleString() }} Completed: {{ new Date(entity.properties.completedOn).toLocaleString() }}
</div> </div>
<div v-if="isExternalTask" class="mt-2"> <div v-if="isExternalTask" class="mt-2">
<v-chip size="small" color="info" variant="tonal"> <v-chip size="small" color="info" variant="tonal">
@@ -129,7 +129,7 @@
<v-divider></v-divider> <v-divider></v-divider>
<v-card-actions> <v-card-actions>
<v-btn <v-btn
v-if="mode === 'edit' && entity.id && canDelete" v-if="mode === 'edit' && entity.identifier && canDelete"
color="error" color="error"
variant="text" variant="text"
@click="handleDelete" @click="handleDelete"
@@ -182,17 +182,17 @@ const emit = defineEmits<{
}>(); }>();
const formRef = ref(); const formRef = ref();
const selectedCollectionId = ref(props.entity?.in || props.collection?.id || props.lists[0]?.id || ''); const selectedCollectionId = ref(props.entity?.collection || props.collection?.identifier || props.lists[0]?.identifier || '');
// Determine permissions based on entity metadata // Determine permissions based on entity metadata
const isExternalTask = computed(() => { const isExternalTask = computed(() => {
// Check if task was assigned by another user // Check if task was assigned by another user
return props.entity?.data?.external === true || props.entity?.data?.assignedBy; return props.entity?.properties?.external === true || props.entity?.properties?.assignedBy;
}); });
const canEdit = computed(() => { const canEdit = computed(() => {
if (!props.entity) return false; if (!props.entity) return false;
if (props.entity.data?.readonly === true) return false; if (props.entity.properties?.readonly === true) return false;
return true; return true;
}); });
@@ -203,12 +203,12 @@ const canEditDates = computed(() => {
const canChangeCollection = computed(() => { const canChangeCollection = computed(() => {
// Can't move external tasks to different lists // Can't move external tasks to different lists
return !isExternalTask.value && !props.entity?.id; return !isExternalTask.value && !props.entity?.identifier;
}); });
const canDelete = computed(() => { const canDelete = computed(() => {
// Can't delete external/readonly tasks // Can't delete external/readonly tasks
return !isExternalTask.value && props.entity?.data?.readonly !== true; return !isExternalTask.value && props.entity?.properties?.readonly !== true;
}); });
const isEditing = computed(() => props.mode === 'edit'); const isEditing = computed(() => props.mode === 'edit');
@@ -218,25 +218,25 @@ const startDate = ref('');
// Watch for entity changes to update date fields // Watch for entity changes to update date fields
watch(() => props.entity, (newEntity) => { watch(() => props.entity, (newEntity) => {
if (newEntity?.data?.dueOn) { if (newEntity?.properties?.dueOn) {
dueDate.value = formatDate(new Date(newEntity.data.dueOn)); dueDate.value = formatDate(new Date(newEntity.properties.dueOn));
} else { } else {
dueDate.value = ''; dueDate.value = '';
} }
if (newEntity?.data?.startsOn) { if (newEntity?.properties?.startsOn) {
startDate.value = formatDate(new Date(newEntity.data.startsOn)); startDate.value = formatDate(new Date(newEntity.properties.startsOn));
} else { } else {
startDate.value = ''; startDate.value = '';
} }
selectedCollectionId.value = newEntity?.in || props.collection?.id || props.lists[0]?.id || ''; selectedCollectionId.value = newEntity?.collection || props.collection?.identifier || props.lists[0]?.identifier || '';
}, { immediate: true }); }, { immediate: true });
const listOptions = computed(() => const listOptions = computed(() =>
props.lists.map(list => ({ props.lists.map(list => ({
title: list.label || 'Unnamed List', title: list.properties.label || 'Unnamed List',
value: list.id, value: list.identifier,
})) }))
); );
@@ -248,13 +248,13 @@ const priorityOptions = [
watch(dueDate, (newVal) => { watch(dueDate, (newVal) => {
if (props.entity && isEditing.value) { if (props.entity && isEditing.value) {
props.entity.data.dueOn = newVal ? new Date(newVal).toISOString() : null; props.entity.properties.dueOn = newVal ? new Date(newVal).toISOString() : null;
} }
}); });
watch(startDate, (newVal) => { watch(startDate, (newVal) => {
if (props.entity && isEditing.value) { if (props.entity && isEditing.value) {
props.entity.data.startsOn = newVal ? new Date(newVal).toISOString() : null; props.entity.properties.startsOn = newVal ? new Date(newVal).toISOString() : null;
} }
}); });
@@ -266,7 +266,7 @@ async function handleSave() {
const { valid } = await formRef.value.validate(); const { valid } = await formRef.value.validate();
if (!valid) return; if (!valid) return;
const targetCollection = props.lists.find(list => list.id === selectedCollectionId.value); const targetCollection = props.lists.find(list => list.identifier === selectedCollectionId.value);
emit('save', props.entity, targetCollection || props.collection); emit('save', props.entity, targetCollection || props.collection);
} }
@@ -276,7 +276,7 @@ function handleCancel() {
function handleDelete() { function handleDelete() {
if (props.entity) { if (props.entity) {
const targetCollection = props.lists.find(list => list.id === selectedCollectionId.value); const targetCollection = props.lists.find(list => list.identifier === selectedCollectionId.value);
emit('delete', props.entity, targetCollection || props.collection); emit('delete', props.entity, targetCollection || props.collection);
} }
} }

View File

@@ -21,41 +21,41 @@
<v-list> <v-list>
<v-list-item <v-list-item
v-for="entity in filteredTasks" v-for="entity in filteredTasks"
:key="entity.id" :key="entity.identifier"
class="task-item" class="task-item"
:class="{ 'completed': entity.data?.status === 'completed' }" :class="{ 'completed': entity.properties?.status === 'completed' }"
@click="$emit('task-click', entity)" @click="$emit('task-click', entity)"
> >
<template #prepend> <template #prepend>
<v-checkbox-btn <v-checkbox-btn
:model-value="entity.data?.status === 'completed'" :model-value="entity.properties?.status === 'completed'"
hide-details hide-details
@click.stop="$emit('toggle-complete', entity.id)" @click.stop="$emit('toggle-complete', entity.identifier)"
></v-checkbox-btn> ></v-checkbox-btn>
</template> </template>
<div class="task-content"> <div class="task-content">
<div class="task-title" :class="{ 'completed': entity.data?.status === 'completed' }"> <div class="task-title" :class="{ 'completed': entity.properties?.status === 'completed' }">
{{ entity.data?.label || 'Untitled Task' }} {{ entity.properties?.label || 'Untitled Task' }}
</div> </div>
<div v-if="entity.data?.description" class="task-description text-caption"> <div v-if="entity.properties?.description" class="task-description text-caption">
{{ entity.data.description }} {{ entity.properties.description }}
</div> </div>
<div class="task-meta"> <div class="task-meta">
<span v-if="entity.data?.dueOn" class="due-date"> <span v-if="entity.properties?.dueOn" class="due-date">
<v-icon size="small">mdi-calendar</v-icon> <v-icon size="small">mdi-calendar</v-icon>
{{ formatDueDate(new Date(entity.data.dueOn)) }} {{ formatDueDate(new Date(entity.properties.dueOn)) }}
</span> </span>
<v-chip <v-chip
v-if="entity.data?.priority" v-if="entity.properties?.priority"
size="x-small" size="x-small"
:class="`priority-${entity.data.priority === 1 ? 'high' : entity.data.priority === 2 ? 'medium' : 'low'}`" :class="`priority-${entity.properties.priority === 1 ? 'high' : entity.properties.priority === 2 ? 'medium' : 'low'}`"
class="priority-badge" class="priority-badge"
> >
{{ entity.data.priority === 1 ? 'high' : entity.data.priority === 2 ? 'medium' : 'low' }} {{ entity.properties.priority === 1 ? 'high' : entity.properties.priority === 2 ? 'medium' : 'low' }}
</v-chip> </v-chip>
<v-chip <v-chip
v-for="tag in (entity.data?.categories || [])" v-for="tag in (entity.properties?.categories || [])"
:key="tag" :key="tag"
size="x-small" size="x-small"
variant="outlined" variant="outlined"
@@ -100,34 +100,34 @@ const filteredTasks = computed(() => {
// Filter by tab // Filter by tab
if (activeTab.value === 'active') { if (activeTab.value === 'active') {
filtered = filtered.filter(entity => entity.data?.status !== 'completed'); filtered = filtered.filter(entity => entity.properties?.status !== 'completed');
} else if (activeTab.value === 'completed') { } else if (activeTab.value === 'completed') {
filtered = filtered.filter(entity => entity.data?.status === 'completed'); filtered = filtered.filter(entity => entity.properties?.status === 'completed');
} }
// Filter by priority // Filter by priority
if (filterPriority.value) { if (filterPriority.value) {
const priorityMap = { high: 1, medium: 2, low: 3 }; const priorityMap = { high: 1, medium: 2, low: 3 };
const targetPriority = priorityMap[filterPriority.value]; const targetPriority = priorityMap[filterPriority.value];
filtered = filtered.filter(entity => entity.data?.priority === targetPriority); filtered = filtered.filter(entity => entity.properties?.priority === targetPriority);
} }
// Sort: incomplete first, then by due date, then by priority // Sort: incomplete first, then by due date, then by priority
return filtered.sort((a, b) => { return filtered.sort((a, b) => {
const aCompleted = a.data?.status === 'completed'; const aCompleted = a.properties?.status === 'completed';
const bCompleted = b.data?.status === 'completed'; const bCompleted = b.properties?.status === 'completed';
if (aCompleted !== bCompleted) { if (aCompleted !== bCompleted) {
return aCompleted ? 1 : -1; return aCompleted ? 1 : -1;
} }
if (a.data?.dueOn && b.data?.dueOn) { if (a.properties?.dueOn && b.properties?.dueOn) {
return new Date(a.data.dueOn).getTime() - new Date(b.data.dueOn).getTime(); return new Date(a.properties.dueOn).getTime() - new Date(b.properties.dueOn).getTime();
} }
if (a.data?.dueOn) return -1; if (a.properties?.dueOn) return -1;
if (b.data?.dueOn) return 1; if (b.properties?.dueOn) return 1;
return (a.data?.priority || 4) - (b.data?.priority || 4); return (a.properties?.priority || 4) - (b.properties?.priority || 4);
}); });
}); });

View File

@@ -8,6 +8,8 @@ import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore';
import { useServicesStore } from '@ChronoManager/stores/servicesStore'; import { useServicesStore } from '@ChronoManager/stores/servicesStore';
import { CollectionObject } from '@ChronoManager/models/collection'; import { CollectionObject } from '@ChronoManager/models/collection';
import { EntityObject } from '@ChronoManager/models/entity'; 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 { ServiceObject } from '@ChronoManager/models/service';
import type { CalendarView as CalendarViewType } from '@/types'; import type { CalendarView as CalendarViewType } from '@/types';
import CollectionList from '@/components/CollectionList.vue'; import CollectionList from '@/components/CollectionList.vue';
@@ -45,19 +47,19 @@ const selectedCollection = ref<CollectionObject | null>(null);
// Computed - filter collections and entities // Computed - filter collections and entities
const calendars = computed(() => { const calendars = computed(() => {
return collections.value.filter(col => col.contents?.event); return collections.value.filter(col => col.properties.contents?.event);
}); });
const taskLists = computed(() => { const taskLists = computed(() => {
return collections.value.filter(col => col.contents?.task); return collections.value.filter(col => col.properties.contents?.task);
}); });
const events = computed(() => { const events = computed(() => {
return entities.value.filter(entity => entity.data && (entity.data as any).type === 'event'); return entities.value.filter(entity => entity.properties && (entity.properties as any).type === 'event');
}); });
const tasks = computed(() => { const tasks = computed(() => {
return entities.value.filter(entity => entity.data && (entity.data as any).type === 'task'); return entities.value.filter(entity => entity.properties && (entity.properties as any).type === 'task');
}); });
// Dialog state // Dialog state
@@ -75,11 +77,11 @@ const isTaskView = computed(() => viewMode.value === 'tasks');
const filteredEvents = computed(() => { const filteredEvents = computed(() => {
const visibleCalendarIds = calendars.value const visibleCalendarIds = calendars.value
.filter(cal => cal.enabled !== false) .filter(cal => cal.properties.visibility !== false)
.map(cal => cal.id); .map(cal => cal.identifier);
return events.value.filter(event => return events.value.filter(event =>
visibleCalendarIds.includes(event.in) visibleCalendarIds.includes(event.collection)
); );
}); });
@@ -94,8 +96,8 @@ function selectCalendar(calendar: CollectionObject) {
} }
function createCalendar() { function createCalendar() {
editingCollection.value = collectionsStore.fresh(); editingCollection.value = new CollectionObject();
editingCollection.value.contents = { event: true }; editingCollection.value.properties.contents = { event: true };
collectionEditorMode.value = 'create'; collectionEditorMode.value = 'create';
collectionEditorType.value = 'calendar'; collectionEditorType.value = 'calendar';
showCollectionEditor.value = true; showCollectionEditor.value = true;
@@ -110,7 +112,12 @@ function editCalendar(collection: CollectionObject) {
async function toggleCalendarVisibility(collection: CollectionObject) { async function toggleCalendarVisibility(collection: CollectionObject) {
try { try {
await collectionsStore.modify(collection); await collectionsStore.update(
collection.provider,
collection.service,
collection.identifier,
collection.properties
);
console.log('[Chrono] - Toggled calendar visibility:', collection); console.log('[Chrono] - Toggled calendar visibility:', collection);
} catch (error) { } catch (error) {
console.error('[Chrono] - Failed to toggle calendar visibility:', error); console.error('[Chrono] - Failed to toggle calendar visibility:', error);
@@ -123,8 +130,8 @@ function selectTaskList(list: CollectionObject) {
} }
function createTaskList() { function createTaskList() {
editingCollection.value = collectionsStore.fresh(); editingCollection.value = new CollectionObject();
editingCollection.value.contents = { task: true }; editingCollection.value.properties.contents = { task: true };
collectionEditorMode.value = 'create'; collectionEditorMode.value = 'create';
collectionEditorType.value = 'tasklist'; collectionEditorType.value = 'tasklist';
showCollectionEditor.value = true; showCollectionEditor.value = true;
@@ -148,9 +155,8 @@ function createEvent() {
return; return;
} }
// Create fresh event entity selectedEntity.value = new EntityObject();
selectedEntity.value = entitiesStore.fresh('event'); selectedEntity.value.properties = new EventObject();
selectedEntity.value.in = selectedCollection.value.id;
entityEditorMode.value = 'edit'; entityEditorMode.value = 'edit';
showEventEditor.value = true; showEventEditor.value = true;
} }
@@ -171,15 +177,25 @@ async function saveEvent(entity: EntityObject, collection?: CollectionObject | n
return; return;
} }
if (entity.data) { const eventData = entity.properties as EventObject;
entity.data.modified = new Date(); eventData.modified = new Date().toISOString();
}
if (entity.id === null) { if (!entity.identifier) {
entity.data.created = new Date(); eventData.created = new Date().toISOString();
selectedEntity.value = await entitiesStore.create(collection, entity); selectedEntity.value = await entitiesStore.create(
collection.provider,
collection.service,
collection.identifier,
eventData.toJson()
);
} else { } else {
selectedEntity.value = await entitiesStore.modify(collection, entity); selectedEntity.value = await entitiesStore.update(
collection.provider,
collection.service,
collection.identifier,
entity.identifier,
eventData.toJson()
);
} }
entityEditorMode.value = 'view'; entityEditorMode.value = 'view';
@@ -191,14 +207,14 @@ async function saveEvent(entity: EntityObject, collection?: CollectionObject | n
async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) { async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) {
try { try {
if (!(collection instanceof CollectionObject)) { if (!(collection instanceof CollectionObject)) {
collection = collections.value.find(c => c.id === entity.in); collection = collections.value.find(c => c.identifier === entity.collection);
} }
if (!collection) { if (!collection) {
console.error('[Chrono] - No collection found'); console.error('[Chrono] - No collection found');
return; return;
} }
await entitiesStore.destroy(collection, entity); await entitiesStore.delete(collection.provider, collection.service, collection.identifier, entity.identifier);
selectedEntity.value = null; selectedEntity.value = null;
entityEditorMode.value = 'view'; entityEditorMode.value = 'view';
showEventEditor.value = false; showEventEditor.value = false;
@@ -218,12 +234,11 @@ function handleDateClick(date: Date) {
return; return;
} }
selectedEntity.value = entitiesStore.fresh('event'); selectedEntity.value = new EntityObject();
selectedEntity.value.in = selectedCollection.value.id; selectedEntity.value.properties = new EventObject();
if (selectedEntity.value.data) { const eventData = selectedEntity.value.properties as EventObject;
selectedEntity.value.data.startsOn = date; eventData.startsOn = date.toISOString();
selectedEntity.value.data.endsOn = new Date(date.getTime() + 60 * 60 * 1000); eventData.endsOn = new Date(date.getTime() + 60 * 60 * 1000).toISOString();
}
entityEditorMode.value = 'edit'; entityEditorMode.value = 'edit';
showEventEditor.value = true; showEventEditor.value = true;
} }
@@ -257,9 +272,8 @@ function createTask() {
return; return;
} }
// Create fresh task entity selectedEntity.value = new EntityObject();
selectedEntity.value = entitiesStore.fresh('task'); selectedEntity.value.properties = new TaskObject();
selectedEntity.value.in = selectedCollection.value.id;
entityEditorMode.value = 'edit'; entityEditorMode.value = 'edit';
showTaskEditor.value = true; showTaskEditor.value = true;
} }
@@ -280,15 +294,25 @@ async function saveTask(entity: EntityObject, collection?: CollectionObject | nu
return; return;
} }
if (entity.data) { const taskData = entity.properties as TaskObject;
entity.data.modified = new Date(); taskData.modified = new Date().toISOString();
}
if (entity.id === null) { if (!entity.identifier) {
entity.data.created = new Date(); taskData.created = new Date().toISOString();
selectedEntity.value = await entitiesStore.create(collection, entity); selectedEntity.value = await entitiesStore.create(
collection.provider,
collection.service,
collection.identifier,
taskData.toJson()
);
} else { } else {
selectedEntity.value = await entitiesStore.modify(collection, entity); selectedEntity.value = await entitiesStore.update(
collection.provider,
collection.service,
collection.identifier,
entity.identifier,
taskData.toJson()
);
} }
entityEditorMode.value = 'view'; entityEditorMode.value = 'view';
@@ -300,14 +324,14 @@ async function saveTask(entity: EntityObject, collection?: CollectionObject | nu
async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) { async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) {
try { try {
if (!(collection instanceof CollectionObject)) { if (!(collection instanceof CollectionObject)) {
collection = collections.value.find(c => c.id === entity.in); collection = collections.value.find(c => c.identifier === entity.collection);
} }
if (!collection) { if (!collection) {
console.error('[Chrono] - No collection found'); console.error('[Chrono] - No collection found');
return; return;
} }
await entitiesStore.destroy(collection, entity); await entitiesStore.delete(collection.provider, collection.service, collection.identifier, entity.identifier);
selectedEntity.value = null; selectedEntity.value = null;
entityEditorMode.value = 'view'; entityEditorMode.value = 'view';
showTaskEditor.value = false; showTaskEditor.value = false;
@@ -318,21 +342,27 @@ async function deleteTask(entity: EntityObject, collection?: CollectionObject |
async function toggleTaskComplete(taskId: string | number) { async function toggleTaskComplete(taskId: string | number) {
try { try {
const entity = entities.value.find(e => e.id === taskId); const entity = entities.value.find(e => e.identifier === taskId);
if (!entity || !entity.data) return; if (!entity || !entity.properties) return;
const collection = collections.value.find(c => c.id === entity.in); const collection = collections.value.find(c => c.identifier === entity.collection);
if (!collection) return; if (!collection) return;
const taskData = entity.data as any; const taskData = entity.properties as TaskObject;
const isCompleted = taskData.status === 'completed'; const isCompleted = taskData.status === 'completed';
taskData.status = isCompleted ? 'needs-action' : 'completed'; taskData.status = isCompleted ? 'needs-action' : 'completed';
taskData.completedOn = isCompleted ? null : new Date(); taskData.completedOn = isCompleted ? null : new Date().toISOString();
taskData.progress = isCompleted ? null : 100; taskData.progress = isCompleted ? null : 100;
taskData.modified = new Date(); taskData.modified = new Date().toISOString();
await entitiesStore.modify(collection, entity); await entitiesStore.update(
collection.provider,
collection.service,
collection.identifier,
entity.identifier,
taskData.toJson()
);
} catch (error) { } catch (error) {
console.error('[Chrono] - Failed to toggle task completion:', error); console.error('[Chrono] - Failed to toggle task completion:', error);
} }
@@ -341,14 +371,24 @@ async function toggleTaskComplete(taskId: string | number) {
async function saveCollection(collection: CollectionObject, service: ServiceObject) { async function saveCollection(collection: CollectionObject, service: ServiceObject) {
try { try {
if (collectionEditorMode.value === 'create') { if (collectionEditorMode.value === 'create') {
await collectionsStore.create(service, collection); await collectionsStore.create(
service.provider,
service.identifier || '',
null,
collection.properties
);
console.log('[Chrono] - Created collection:', collection); console.log('[Chrono] - Created collection:', collection);
} else { } else {
await collectionsStore.modify(collection); await collectionsStore.update(
collection.provider,
collection.service,
collection.identifier,
collection.properties
);
console.log('[Chrono] - Modified collection:', collection); console.log('[Chrono] - Modified collection:', collection);
} }
// Reload collections // Reload collections
collections.value = await collectionsStore.list(); collections.value = Object.values(await collectionsStore.list());
} catch (error) { } catch (error) {
console.error('[Chrono] - Failed to save collection:', error); console.error('[Chrono] - Failed to save collection:', error);
} }
@@ -356,11 +396,11 @@ async function saveCollection(collection: CollectionObject, service: ServiceObje
async function deleteCollection(collection: CollectionObject) { async function deleteCollection(collection: CollectionObject) {
try { try {
await collectionsStore.destroy(collection); await collectionsStore.delete(collection.provider, collection.service, collection.identifier);
console.log('[Chrono] - Deleted collection:', collection); console.log('[Chrono] - Deleted collection:', collection);
// Reload collections // Reload collections
collections.value = await collectionsStore.list(); collections.value = Object.values(await collectionsStore.list());
if (selectedCollection.value?.id === collection.id) { if (selectedCollection.value?.identifier === collection.identifier) {
selectedCollection.value = null; selectedCollection.value = null;
} }
} catch (error) { } catch (error) {
@@ -372,10 +412,28 @@ async function deleteCollection(collection: CollectionObject) {
onMounted(async () => { onMounted(async () => {
try { try {
// Load collections (calendars and task lists) // Load collections (calendars and task lists)
collections.value = await collectionsStore.list(); collections.value = Object.values(await collectionsStore.list());
// Load entities (events and tasks) // Load entities (events and tasks) for available collection sources only
entities.value = await entitiesStore.list(null, null, null); 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:', { console.log('[Chrono] - Loaded data from ChronoManager:', {
collections: collections.value.length, collections: collections.value.length,

View File

@@ -38,9 +38,9 @@ export function daysDiff(a: Date, b: Date): number {
* Check if an event spans multiple days * Check if an event spans multiple days
*/ */
export function isMultiDay(entity: any): boolean { export function isMultiDay(entity: any): boolean {
if (!entity.data?.startsOn) return false; if (!entity.properties?.startsOn) return false;
const start = startOfDay(new Date(entity.data.startsOn)); const start = startOfDay(new Date(entity.properties.startsOn));
const end = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : start; const end = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : start;
return daysDiff(start, end) > 0; return daysDiff(start, end) > 0;
} }
@@ -48,10 +48,10 @@ export function isMultiDay(entity: any): boolean {
* Check if an event is an all-day event (no specific time, or spans full day) * Check if an event is an all-day event (no specific time, or spans full day)
*/ */
export function isAllDay(entity: any): boolean { export function isAllDay(entity: any): boolean {
if (!entity.data?.startsOn) return false; if (!entity.properties?.startsOn) return false;
// Explicit all-day flag // Explicit all-day flag
if (entity.data?.allDay === true) return true; if (entity.properties?.timeless === true) return true;
// Multi-day events are treated as all-day in the days view // Multi-day events are treated as all-day in the days view
if (isMultiDay(entity)) return true; if (isMultiDay(entity)) return true;
@@ -63,9 +63,9 @@ export function isAllDay(entity: any): boolean {
* Check if an event overlaps with a date range * Check if an event overlaps with a date range
*/ */
export function eventOverlapsRange(entity: any, rangeStart: Date, rangeEnd: Date): boolean { export function eventOverlapsRange(entity: any, rangeStart: Date, rangeEnd: Date): boolean {
if (!entity.data?.startsOn) return false; if (!entity.properties?.startsOn) return false;
const eventStart = startOfDay(new Date(entity.data.startsOn)); const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : eventStart; const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
return eventStart <= rangeEnd && eventEnd >= rangeStart; return eventStart <= rangeEnd && eventEnd >= rangeStart;
} }
@@ -73,9 +73,9 @@ export function eventOverlapsRange(entity: any, rangeStart: Date, rangeEnd: Date
* Check if an event occurs on a specific date * Check if an event occurs on a specific date
*/ */
export function eventOnDate(entity: any, date: Date): boolean { export function eventOnDate(entity: any, date: Date): boolean {
if (!entity.data?.startsOn) return false; if (!entity.properties?.startsOn) return false;
const eventStart = startOfDay(new Date(entity.data.startsOn)); const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : eventStart; const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
const targetDate = startOfDay(date); const targetDate = startOfDay(date);
return eventStart <= targetDate && eventEnd >= targetDate; return eventStart <= targetDate && eventEnd >= targetDate;
} }
@@ -95,17 +95,17 @@ export function getMultiDaySegments(
// Filter multi-day/all-day events that overlap this range // Filter multi-day/all-day events that overlap this range
const multiDayEvents = events.filter(entity => { const multiDayEvents = events.filter(entity => {
if (!entity.data?.startsOn) return false; if (!entity.properties?.startsOn) return false;
if (!isAllDay(entity)) return false; if (!isAllDay(entity)) return false;
return eventOverlapsRange(entity, rangeStart, rangeEnd); return eventOverlapsRange(entity, rangeStart, rangeEnd);
}); });
// Sort: longer events first, then by start date // Sort: longer events first, then by start date
multiDayEvents.sort((a, b) => { multiDayEvents.sort((a, b) => {
const aStart = new Date(a.data.startsOn); const aStart = new Date(a.properties.startsOn);
const aEnd = new Date(a.data.endsOn || a.data.startsOn); const aEnd = new Date(a.properties.endsOn || a.properties.startsOn);
const bStart = new Date(b.data.startsOn); const bStart = new Date(b.properties.startsOn);
const bEnd = new Date(b.data.endsOn || b.data.startsOn); const bEnd = new Date(b.properties.endsOn || b.properties.startsOn);
const aDuration = daysDiff(aStart, aEnd); const aDuration = daysDiff(aStart, aEnd);
const bDuration = daysDiff(bStart, bEnd); const bDuration = daysDiff(bStart, bEnd);
if (bDuration !== aDuration) return bDuration - aDuration; if (bDuration !== aDuration) return bDuration - aDuration;
@@ -116,8 +116,8 @@ export function getMultiDaySegments(
const lanes: boolean[][] = []; const lanes: boolean[][] = [];
for (const entity of multiDayEvents) { for (const entity of multiDayEvents) {
const eventStart = startOfDay(new Date(entity.data.startsOn)); const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : eventStart; const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
// Clamp to visible range // Clamp to visible range
const segStart = eventStart < rangeStart ? rangeStart : eventStart; const segStart = eventStart < rangeStart ? rangeStart : eventStart;
@@ -173,9 +173,9 @@ export function getMultiDaySegments(
*/ */
export function getTimedEventsForDate(events: any[], date: Date): any[] { export function getTimedEventsForDate(events: any[], date: Date): any[] {
return events.filter(entity => { return events.filter(entity => {
if (!entity.data?.startsOn) return false; if (!entity.properties?.startsOn) return false;
if (isAllDay(entity)) return false; if (isAllDay(entity)) return false;
const eventDate = startOfDay(new Date(entity.data.startsOn)); const eventDate = startOfDay(new Date(entity.properties.startsOn));
return eventDate.toDateString() === date.toDateString(); return eventDate.toDateString() === date.toDateString();
}); });
} }
@@ -185,9 +185,9 @@ export function getTimedEventsForDate(events: any[], date: Date): any[] {
*/ */
export function getSingleDayEventsForDate(events: any[], date: Date): any[] { export function getSingleDayEventsForDate(events: any[], date: Date): any[] {
return events.filter(entity => { return events.filter(entity => {
if (!entity.data?.startsOn) return false; if (!entity.properties?.startsOn) return false;
if (isMultiDay(entity)) return false; if (isMultiDay(entity)) return false;
const eventDate = startOfDay(new Date(entity.data.startsOn)); const eventDate = startOfDay(new Date(entity.properties.startsOn));
return eventDate.toDateString() === date.toDateString(); return eventDate.toDateString() === date.toDateString();
}); });
} }