Merge pull request 'chore: standardize chrono provider' (#2) from chore/standardize-chrono-provider into main
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -27,15 +27,15 @@
|
||||
<v-list-subheader>{{ formatAgendaDate(date) }}</v-list-subheader>
|
||||
<v-list-item
|
||||
v-for="entity in dayEvents"
|
||||
:key="entity.id"
|
||||
:key="entity.identifier"
|
||||
@click="$emit('event-click', entity)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :color="getEventColor(entity)">mdi-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ entity.data?.label || 'Untitled' }}</v-list-item-title>
|
||||
<v-list-item-title>{{ entity.properties?.label || 'Untitled' }}</v-list-item-title>
|
||||
<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>
|
||||
</template>
|
||||
@@ -132,17 +132,17 @@ const groupedEvents = computed(() => {
|
||||
const { start, end } = dateRange.value;
|
||||
|
||||
const filtered = props.events.filter(e => {
|
||||
if (!e.data?.startsOn) return false;
|
||||
const eventStart = new Date(e.data.startsOn);
|
||||
if (!e.properties?.startsOn) return false;
|
||||
const eventStart = new Date(e.properties.startsOn);
|
||||
return eventStart >= start && eventStart <= end;
|
||||
});
|
||||
|
||||
const sorted = filtered.sort((a, b) =>
|
||||
new Date(a.data.startsOn).getTime() - new Date(b.data.startsOn).getTime()
|
||||
new Date(a.properties.startsOn).getTime() - new Date(b.properties.startsOn).getTime()
|
||||
);
|
||||
|
||||
sorted.forEach(entity => {
|
||||
const dateKey = new Date(entity.data.startsOn).toDateString();
|
||||
const dateKey = new Date(entity.properties.startsOn).toDateString();
|
||||
if (!grouped[dateKey]) {
|
||||
grouped[dateKey] = [];
|
||||
}
|
||||
@@ -153,9 +153,9 @@ const groupedEvents = computed(() => {
|
||||
});
|
||||
|
||||
function getEventColor(entity: any): string {
|
||||
if (entity.data?.color) return entity.data.color;
|
||||
const calendar = props.calendars.find(cal => cal.id === entity.in);
|
||||
return calendar?.color || '#1976D2';
|
||||
if (entity.properties?.color) return entity.properties.color;
|
||||
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
|
||||
return calendar?.properties?.color || '#1976D2';
|
||||
}
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
|
||||
@@ -61,7 +61,7 @@ const dialogIcon = computed(() => {
|
||||
// Functions
|
||||
const onOpen = async () => {
|
||||
if (services.value.length === 0) {
|
||||
services.value = await servicesStore.list()
|
||||
services.value = Object.values(await servicesStore.list())
|
||||
}
|
||||
|
||||
if (!props.collection) {
|
||||
@@ -71,10 +71,10 @@ const onOpen = async () => {
|
||||
// Clone the collection to avoid mutating the original
|
||||
editingCollection.value = props.collection.clone()
|
||||
|
||||
if (props.collection.id !== null) {
|
||||
if (props.mode === 'edit') {
|
||||
// Edit mode - find the service
|
||||
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
|
||||
} else {
|
||||
// Create mode - use first service that can create
|
||||
@@ -102,7 +102,7 @@ const onColorSelect = (color: string | null, closeMenu = true) => {
|
||||
if (!editingCollection.value) {
|
||||
return
|
||||
}
|
||||
editingCollection.value.color = color
|
||||
editingCollection.value.properties.color = color
|
||||
if (closeMenu) {
|
||||
colorMenuOpen.value = false
|
||||
}
|
||||
@@ -144,7 +144,7 @@ watch(() => props.modelValue, async (newValue) => {
|
||||
label="Service"
|
||||
:items="services.filter(s => s.capabilities?.CollectionCreate)"
|
||||
item-title="label"
|
||||
item-value="id"
|
||||
item-value="identifier"
|
||||
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>
|
||||
|
||||
<v-text-field
|
||||
v-model="editingCollection.label"
|
||||
v-model="editingCollection.properties.label"
|
||||
label="Label"
|
||||
required
|
||||
:rules="[(v: string) => !!v || 'Label is required']"
|
||||
@@ -170,7 +170,7 @@ watch(() => props.modelValue, async (newValue) => {
|
||||
icon
|
||||
variant="text"
|
||||
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"
|
||||
title="Select color"
|
||||
>
|
||||
@@ -203,11 +203,11 @@ watch(() => props.modelValue, async (newValue) => {
|
||||
variant="flat"
|
||||
size="small"
|
||||
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 }"
|
||||
@click="onColorSelect(color)">
|
||||
<v-icon
|
||||
v-if="editingCollection.color === color"
|
||||
v-if="editingCollection.properties.color === color"
|
||||
icon="mdi-check"
|
||||
size="x-small"
|
||||
color="white"
|
||||
@@ -218,7 +218,7 @@ watch(() => props.modelValue, async (newValue) => {
|
||||
|
||||
<div class="color-menu-body__picker">
|
||||
<v-color-picker
|
||||
v-model="editingCollection.color"
|
||||
v-model="editingCollection.properties.color"
|
||||
mode="hex"
|
||||
hide-canvas
|
||||
width="100%"
|
||||
@@ -232,7 +232,7 @@ watch(() => props.modelValue, async (newValue) => {
|
||||
</v-text-field>
|
||||
|
||||
<v-textarea
|
||||
v-model="editingCollection.description"
|
||||
v-model="editingCollection.properties.description"
|
||||
label="Description"
|
||||
rows="2"
|
||||
/>
|
||||
@@ -240,7 +240,7 @@ watch(() => props.modelValue, async (newValue) => {
|
||||
<v-row>
|
||||
<v-col v-if="mode === 'edit'" cols="6">
|
||||
<v-switch
|
||||
v-model="editingCollection.enabled"
|
||||
v-model="editingCollection.properties.visibility"
|
||||
label="Enabled"
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
@@ -29,9 +29,9 @@ const filteredCollections = computed(() => {
|
||||
|
||||
return collections.value.filter(collection => {
|
||||
if (props.type === 'calendar') {
|
||||
return collection.contents?.event
|
||||
return collection.properties.contents?.event
|
||||
} else if (props.type === 'tasklist') {
|
||||
return collection.contents?.task
|
||||
return collection.properties.contents?.task
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -53,7 +53,7 @@ const displayIcon = computed(() => {
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
collections.value = await collectionsStore.list()
|
||||
collections.value = Object.values(await collectionsStore.list())
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to load collections:', error)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ const onCollectionEdit = (collection: CollectionObject) => {
|
||||
}
|
||||
|
||||
const onToggleVisibility = (collection: CollectionObject) => {
|
||||
collection.enabled = !collection.enabled
|
||||
collection.properties.visibility = collection.properties.visibility === false
|
||||
emit('toggle-visibility', collection)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ defineExpose({
|
||||
async refresh() {
|
||||
loading.value = true
|
||||
try {
|
||||
collections.value = await collectionsStore.list()
|
||||
collections.value = Object.values(await collectionsStore.list())
|
||||
} catch (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-item
|
||||
v-for="collection in filteredCollections"
|
||||
:key="collection.id"
|
||||
:value="collection.id"
|
||||
:active="selectedCollection?.id === collection.id"
|
||||
:key="collection.identifier"
|
||||
:value="collection.identifier"
|
||||
:active="selectedCollection?.identifier === collection.identifier"
|
||||
@click="onCollectionSelect(collection)"
|
||||
rounded="lg"
|
||||
class="mb-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn
|
||||
:model-value="collection.enabled !== false"
|
||||
:color="collection.color || 'primary'"
|
||||
:model-value="collection.properties.visibility !== false"
|
||||
:color="collection.properties.color || 'primary'"
|
||||
hide-details
|
||||
@click.stop="onToggleVisibility(collection)"
|
||||
/>
|
||||
</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>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:color="collection.color || 'primary'"
|
||||
:color="collection.properties.color || 'primary'"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>mdi-circle</v-icon>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<div class="all-day-events-overlay">
|
||||
<div
|
||||
v-for="segment in allDaySegments.segments"
|
||||
:key="segment.entity.id"
|
||||
:key="segment.entity.identifier"
|
||||
class="all-day-event"
|
||||
:class="{
|
||||
'is-start': segment.isStart,
|
||||
@@ -53,7 +53,7 @@
|
||||
@mouseenter="emit('event-hover', { event: $event, entity: segment.entity })"
|
||||
@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>
|
||||
@@ -71,7 +71,7 @@
|
||||
<div class="day-events" @click="handleDayClick($event, day)">
|
||||
<div
|
||||
v-for="entity in getTimedEvents(day)"
|
||||
:key="entity.id"
|
||||
:key="entity.identifier"
|
||||
class="day-event"
|
||||
:style="getEventStyle(entity)"
|
||||
@click.stop="emit('event-click', entity)"
|
||||
@@ -80,12 +80,12 @@
|
||||
>
|
||||
<template v-if="daysCount <= 3">
|
||||
<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 class="event-title">{{ entity.data?.label || 'Untitled' }}</div>
|
||||
<div class="event-title">{{ entity.properties?.label || 'Untitled' }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="event-title-compact">{{ entity.data?.label || 'Untitled' }}</div>
|
||||
<div class="event-title-compact">{{ entity.properties?.label || 'Untitled' }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,9 +200,9 @@ function getTimedEvents(date: Date): any[] {
|
||||
}
|
||||
|
||||
function getEventColor(entity: any): string {
|
||||
if (entity.data?.color) return entity.data.color;
|
||||
const calendar = props.calendars.find(cal => cal.id === entity.in);
|
||||
return calendar?.color || '#1976D2';
|
||||
if (entity.properties?.color) return entity.properties.color;
|
||||
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
|
||||
return calendar?.properties?.color || '#1976D2';
|
||||
}
|
||||
|
||||
function getAllDayEventStyle(segment: MultiDaySegment) {
|
||||
@@ -215,11 +215,11 @@ function getAllDayEventStyle(segment: MultiDaySegment) {
|
||||
}
|
||||
|
||||
function getEventStyle(entity: any) {
|
||||
if (!entity.data?.startsOn || !entity.data?.endsOn) {
|
||||
if (!entity.properties?.startsOn || !entity.properties?.endsOn) {
|
||||
return { display: 'none' };
|
||||
}
|
||||
const startTime = new Date(entity.data.startsOn);
|
||||
const endTime = new Date(entity.data.endsOn);
|
||||
const startTime = new Date(entity.properties.startsOn);
|
||||
const endTime = new Date(entity.properties.endsOn);
|
||||
const start = startTime.getHours() * 60 + startTime.getMinutes();
|
||||
const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60);
|
||||
|
||||
|
||||
@@ -32,18 +32,18 @@ const emit = defineEmits<{
|
||||
// State
|
||||
const loading = 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
|
||||
const mode = computed(() => props.mode)
|
||||
const entity = computed(() => props.entity || null)
|
||||
const entityObject = computed(() => entity.value?.data as EventObject ?? null)
|
||||
const entityFresh = computed(() => entity.value?.id === null || entity.value?.id === undefined)
|
||||
const entityObject = computed(() => entity.value?.properties as EventObject ?? null)
|
||||
const entityFresh = computed(() => !entity.value?.identifier)
|
||||
|
||||
const calendarOptions = computed(() =>
|
||||
(props.calendars || []).map(cal => ({
|
||||
title: cal.label || 'Unnamed Calendar',
|
||||
value: cal.id,
|
||||
title: cal.properties.label || 'Unnamed Calendar',
|
||||
value: cal.identifier,
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ const cancelEdit = () => {
|
||||
|
||||
const saveEntity = async () => {
|
||||
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)
|
||||
saving.value = false
|
||||
}
|
||||
@@ -161,7 +161,7 @@ const removeNotification = (key: string) => {
|
||||
|
||||
<div v-else class="pa-4">
|
||||
<v-select
|
||||
v-if="mode === 'edit' && !entity.id"
|
||||
v-if="mode === 'edit' && !entity.identifier"
|
||||
v-model="selectedCollectionId"
|
||||
:items="calendarOptions"
|
||||
label="Calendar"
|
||||
|
||||
@@ -41,25 +41,25 @@ const popupStyle = computed(() => {
|
||||
})
|
||||
|
||||
const hasLocation = computed(() => {
|
||||
return props.event?.data?.locationsPhysical && Object.keys(props.event.data.locationsPhysical).length > 0 ||
|
||||
props.event?.data?.locationsVirtual && Object.keys(props.event.data.locationsVirtual).length > 0
|
||||
return props.event?.properties?.locationsPhysical && Object.keys(props.event.properties.locationsPhysical).length > 0 ||
|
||||
props.event?.properties?.locationsVirtual && Object.keys(props.event.properties.locationsVirtual).length > 0
|
||||
})
|
||||
|
||||
const firstLocation = computed(() => {
|
||||
if (props.event?.data?.locationsPhysical && Object.keys(props.event.data.locationsPhysical).length > 0) {
|
||||
const key = Object.keys(props.event.data.locationsPhysical)[0]
|
||||
return props.event.data.locationsPhysical[key]
|
||||
if (props.event?.properties?.locationsPhysical && Object.keys(props.event.properties.locationsPhysical).length > 0) {
|
||||
const key = Object.keys(props.event.properties.locationsPhysical)[0]
|
||||
return props.event.properties.locationsPhysical[key]
|
||||
}
|
||||
if (props.event?.data?.locationsVirtual && Object.keys(props.event.data.locationsVirtual).length > 0) {
|
||||
const key = Object.keys(props.event.data.locationsVirtual)[0]
|
||||
return props.event.data.locationsVirtual[key]
|
||||
if (props.event?.properties?.locationsVirtual && Object.keys(props.event.properties.locationsVirtual).length > 0) {
|
||||
const key = Object.keys(props.event.properties.locationsVirtual)[0]
|
||||
return props.event.properties.locationsVirtual[key]
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const participantCount = computed(() => {
|
||||
if (!props.event?.data?.participants) return 0
|
||||
return Object.keys(props.event.data.participants).length
|
||||
if (!props.event?.properties?.participants) return 0
|
||||
return Object.keys(props.event.properties.participants).length
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -75,18 +75,18 @@ const participantCount = computed(() => {
|
||||
<v-card-text class="pa-3">
|
||||
<div class="d-flex align-center mb-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 class="event-popup-details">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon icon="mdi-clock-outline" size="small" class="mr-2 text-grey" />
|
||||
<div class="text-caption">
|
||||
<div v-if="event.data?.timeless">
|
||||
All Day - {{ new Date(event.data.startsOn).toLocaleDateString() }}
|
||||
<div v-if="event.properties?.timeless">
|
||||
All Day - {{ new Date(event.properties.startsOn).toLocaleDateString() }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ formatTime(event.data?.startsOn) }} - {{ formatTime(event.data?.endsOn) }}
|
||||
{{ formatTime(event.properties?.startsOn) }} - {{ formatTime(event.properties?.endsOn) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,10 +98,10 @@ const participantCount = computed(() => {
|
||||
</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" />
|
||||
<div class="text-caption">
|
||||
{{ event.data.organizer.name || event.data.organizer.address }}
|
||||
{{ event.properties.organizer.name || event.properties.organizer.address }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,13 +112,13 @@ const participantCount = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="event.data?.description" class="mt-2 pt-2 border-t">
|
||||
<div class="text-caption text-grey">{{ event.data.description }}</div>
|
||||
<div v-if="event.properties?.description" class="mt-2 pt-2 border-t">
|
||||
<div class="text-caption text-grey">{{ event.properties.description }}</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-for="(tag, index) in event.data.tags.slice(0, 3)"
|
||||
v-for="(tag, index) in event.properties.tags.slice(0, 3)"
|
||||
:key="index"
|
||||
size="x-small"
|
||||
class="mr-1"
|
||||
@@ -126,8 +126,8 @@ const participantCount = computed(() => {
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
<span v-if="event.data.tags.length > 3" class="text-caption text-grey">
|
||||
+{{ event.data.tags.length - 3 }} more
|
||||
<span v-if="event.properties.tags.length > 3" class="text-caption text-grey">
|
||||
+{{ event.properties.tags.length - 3 }} more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,18 +141,18 @@ function getMultiDaySegments(weekStart: Date, weekEnd: Date): { segments: MultiD
|
||||
|
||||
// Filter multiday events that overlap this week
|
||||
const multiDayEvents = props.events.filter(entity => {
|
||||
if (!entity.data?.startsOn || !isMultiDay(entity)) return false;
|
||||
const eventStart = startOfDay(new Date(entity.data.startsOn));
|
||||
const eventEnd = startOfDay(new Date(entity.data.endsOn));
|
||||
if (!entity.properties?.startsOn || !isMultiDay(entity)) return false;
|
||||
const eventStart = startOfDay(new Date(entity.properties.startsOn));
|
||||
const eventEnd = startOfDay(new Date(entity.properties.endsOn));
|
||||
return eventStart <= weekEnd && eventEnd >= weekStart;
|
||||
});
|
||||
|
||||
// Sort: longer events first, then by start date
|
||||
multiDayEvents.sort((a, b) => {
|
||||
const aStart = new Date(a.data.startsOn);
|
||||
const aEnd = new Date(a.data.endsOn);
|
||||
const bStart = new Date(b.data.startsOn);
|
||||
const bEnd = new Date(b.data.endsOn);
|
||||
const aStart = new Date(a.properties.startsOn);
|
||||
const aEnd = new Date(a.properties.endsOn);
|
||||
const bStart = new Date(b.properties.startsOn);
|
||||
const bEnd = new Date(b.properties.endsOn);
|
||||
const aDuration = daysDiff(aStart, aEnd);
|
||||
const bDuration = daysDiff(bStart, bEnd);
|
||||
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
|
||||
|
||||
for (const entity of multiDayEvents) {
|
||||
const eventStart = startOfDay(new Date(entity.data.startsOn));
|
||||
const eventEnd = startOfDay(new Date(entity.data.endsOn));
|
||||
const eventStart = startOfDay(new Date(entity.properties.startsOn));
|
||||
const eventEnd = startOfDay(new Date(entity.properties.endsOn));
|
||||
|
||||
// Clamp to this week
|
||||
const segStart = eventStart < weekStart ? weekStart : eventStart;
|
||||
@@ -226,9 +226,9 @@ function isToday(date: Date): boolean {
|
||||
}
|
||||
|
||||
function getEventColor(entity: any): string {
|
||||
if (entity.data?.color) return entity.data.color;
|
||||
const calendar = props.calendars.find(cal => cal.id === entity.in);
|
||||
return calendar?.color || '#1976D2';
|
||||
if (entity.properties?.color) return entity.properties.color;
|
||||
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
|
||||
return calendar?.properties?.color || '#1976D2';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -276,14 +276,14 @@ function getEventColor(entity: any): string {
|
||||
<div class="cell-events">
|
||||
<div
|
||||
v-for="entity in getSingleDayEvents(cell.date).slice(0, MAX_VISIBLE_EVENTS - week.laneCount)"
|
||||
:key="entity.id"
|
||||
:key="entity.identifier"
|
||||
class="single-day-event"
|
||||
:style="{ backgroundColor: getEventColor(entity) }"
|
||||
@click.stop="$emit('event-click', entity)"
|
||||
@mouseenter="$emit('event-hover', { event: $event, entity })"
|
||||
@mouseleave="$emit('event-hover-end')"
|
||||
>
|
||||
{{ entity.data?.label || 'Untitled' }}
|
||||
{{ entity.properties?.label || 'Untitled' }}
|
||||
</div>
|
||||
<div
|
||||
v-if="getHiddenCount(cell, week.laneCount) > 0"
|
||||
@@ -300,7 +300,7 @@ function getEventColor(entity: any): string {
|
||||
<div class="multiday-overlay">
|
||||
<div
|
||||
v-for="segment in week.multiDaySegments"
|
||||
:key="`${segment.entity.id}-${weekIndex}`"
|
||||
:key="`${segment.entity.identifier}-${weekIndex}`"
|
||||
class="multiday-event"
|
||||
:class="{
|
||||
'is-start': segment.isStart,
|
||||
@@ -317,7 +317,7 @@ function getEventColor(entity: any): string {
|
||||
@mouseleave="$emit('event-hover-end')"
|
||||
>
|
||||
<span v-if="segment.isStart" class="event-label">
|
||||
{{ segment.entity.data?.label || 'Untitled' }}
|
||||
{{ segment.entity.properties?.label || 'Untitled' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-card v-if="entity" class="task-editor">
|
||||
<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">
|
||||
<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.data.label"
|
||||
v-model="entity.properties.label"
|
||||
label="Title"
|
||||
:rules="[v => !!v || 'Title is required']"
|
||||
:readonly="!isEditing"
|
||||
@@ -40,7 +40,7 @@
|
||||
></v-select>
|
||||
|
||||
<v-textarea
|
||||
v-model="entity.data.description"
|
||||
v-model="entity.properties.description"
|
||||
label="Description"
|
||||
:readonly="!isEditing"
|
||||
:variant="isEditing ? 'outlined' : 'plain'"
|
||||
@@ -49,7 +49,7 @@
|
||||
></v-textarea>
|
||||
|
||||
<v-select
|
||||
v-model="entity.data.priority"
|
||||
v-model="entity.properties.priority"
|
||||
:items="priorityOptions"
|
||||
label="Priority"
|
||||
:readonly="!isEditing"
|
||||
@@ -78,7 +78,7 @@
|
||||
></v-text-field>
|
||||
|
||||
<v-combobox
|
||||
v-model="entity.data.categories"
|
||||
v-model="entity.properties.categories"
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<div class="d-flex gap-4">
|
||||
<v-text-field
|
||||
v-model.number="entity.data.estimatedTime"
|
||||
v-model.number="entity.properties.estimatedTime"
|
||||
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.data.actualTime"
|
||||
v-model.number="entity.properties.actualTime"
|
||||
label="Actual Time (minutes)"
|
||||
type="number"
|
||||
:readonly="!isEditing"
|
||||
@@ -108,13 +108,13 @@
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
<div>Created: {{ new Date(entity.data.created).toLocaleString() }}</div>
|
||||
<div v-if="entity.data.modified">Modified: {{ new Date(entity.data.modified).toLocaleString() }}</div>
|
||||
<div v-if="entity.data.status === 'completed' && entity.data.completedOn">
|
||||
Completed: {{ new Date(entity.data.completedOn).toLocaleString() }}
|
||||
<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>
|
||||
<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.id && canDelete"
|
||||
v-if="mode === 'edit' && entity.identifier && canDelete"
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="handleDelete"
|
||||
@@ -182,17 +182,17 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
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
|
||||
const isExternalTask = computed(() => {
|
||||
// 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(() => {
|
||||
if (!props.entity) return false;
|
||||
if (props.entity.data?.readonly === true) return false;
|
||||
if (props.entity.properties?.readonly === true) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -203,12 +203,12 @@ const canEditDates = computed(() => {
|
||||
|
||||
const canChangeCollection = computed(() => {
|
||||
// Can't move external tasks to different lists
|
||||
return !isExternalTask.value && !props.entity?.id;
|
||||
return !isExternalTask.value && !props.entity?.identifier;
|
||||
});
|
||||
|
||||
const canDelete = computed(() => {
|
||||
// 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');
|
||||
@@ -218,25 +218,25 @@ const startDate = ref('');
|
||||
|
||||
// Watch for entity changes to update date fields
|
||||
watch(() => props.entity, (newEntity) => {
|
||||
if (newEntity?.data?.dueOn) {
|
||||
dueDate.value = formatDate(new Date(newEntity.data.dueOn));
|
||||
if (newEntity?.properties?.dueOn) {
|
||||
dueDate.value = formatDate(new Date(newEntity.properties.dueOn));
|
||||
} else {
|
||||
dueDate.value = '';
|
||||
}
|
||||
|
||||
if (newEntity?.data?.startsOn) {
|
||||
startDate.value = formatDate(new Date(newEntity.data.startsOn));
|
||||
if (newEntity?.properties?.startsOn) {
|
||||
startDate.value = formatDate(new Date(newEntity.properties.startsOn));
|
||||
} else {
|
||||
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 });
|
||||
|
||||
const listOptions = computed(() =>
|
||||
props.lists.map(list => ({
|
||||
title: list.label || 'Unnamed List',
|
||||
value: list.id,
|
||||
title: list.properties.label || 'Unnamed List',
|
||||
value: list.identifier,
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -248,13 +248,13 @@ const priorityOptions = [
|
||||
|
||||
watch(dueDate, (newVal) => {
|
||||
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) => {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ function handleCancel() {
|
||||
|
||||
function handleDelete() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,41 +21,41 @@
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="entity in filteredTasks"
|
||||
:key="entity.id"
|
||||
:key="entity.identifier"
|
||||
class="task-item"
|
||||
:class="{ 'completed': entity.data?.status === 'completed' }"
|
||||
:class="{ 'completed': entity.properties?.status === 'completed' }"
|
||||
@click="$emit('task-click', entity)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn
|
||||
:model-value="entity.data?.status === 'completed'"
|
||||
:model-value="entity.properties?.status === 'completed'"
|
||||
hide-details
|
||||
@click.stop="$emit('toggle-complete', entity.id)"
|
||||
@click.stop="$emit('toggle-complete', entity.identifier)"
|
||||
></v-checkbox-btn>
|
||||
</template>
|
||||
|
||||
<div class="task-content">
|
||||
<div class="task-title" :class="{ 'completed': entity.data?.status === 'completed' }">
|
||||
{{ entity.data?.label || 'Untitled Task' }}
|
||||
<div class="task-title" :class="{ 'completed': entity.properties?.status === 'completed' }">
|
||||
{{ entity.properties?.label || 'Untitled Task' }}
|
||||
</div>
|
||||
<div v-if="entity.data?.description" class="task-description text-caption">
|
||||
{{ entity.data.description }}
|
||||
<div v-if="entity.properties?.description" class="task-description text-caption">
|
||||
{{ entity.properties.description }}
|
||||
</div>
|
||||
<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>
|
||||
{{ formatDueDate(new Date(entity.data.dueOn)) }}
|
||||
{{ formatDueDate(new Date(entity.properties.dueOn)) }}
|
||||
</span>
|
||||
<v-chip
|
||||
v-if="entity.data?.priority"
|
||||
v-if="entity.properties?.priority"
|
||||
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"
|
||||
>
|
||||
{{ 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-for="tag in (entity.data?.categories || [])"
|
||||
v-for="tag in (entity.properties?.categories || [])"
|
||||
:key="tag"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
@@ -100,34 +100,34 @@ const filteredTasks = computed(() => {
|
||||
|
||||
// Filter by tab
|
||||
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') {
|
||||
filtered = filtered.filter(entity => entity.data?.status === 'completed');
|
||||
filtered = filtered.filter(entity => entity.properties?.status === 'completed');
|
||||
}
|
||||
|
||||
// Filter by priority
|
||||
if (filterPriority.value) {
|
||||
const priorityMap = { high: 1, medium: 2, low: 3 };
|
||||
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
|
||||
return filtered.sort((a, b) => {
|
||||
const aCompleted = a.data?.status === 'completed';
|
||||
const bCompleted = b.data?.status === 'completed';
|
||||
const aCompleted = a.properties?.status === 'completed';
|
||||
const bCompleted = b.properties?.status === 'completed';
|
||||
if (aCompleted !== bCompleted) {
|
||||
return aCompleted ? 1 : -1;
|
||||
}
|
||||
|
||||
if (a.data?.dueOn && b.data?.dueOn) {
|
||||
return new Date(a.data.dueOn).getTime() - new Date(b.data.dueOn).getTime();
|
||||
if (a.properties?.dueOn && b.properties?.dueOn) {
|
||||
return new Date(a.properties.dueOn).getTime() - new Date(b.properties.dueOn).getTime();
|
||||
}
|
||||
|
||||
if (a.data?.dueOn) return -1;
|
||||
if (b.data?.dueOn) return 1;
|
||||
if (a.properties?.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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ 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 CollectionList from '@/components/CollectionList.vue';
|
||||
@@ -45,19 +47,19 @@ const selectedCollection = ref<CollectionObject | null>(null);
|
||||
|
||||
// Computed - filter collections and entities
|
||||
const calendars = computed(() => {
|
||||
return collections.value.filter(col => col.contents?.event);
|
||||
return collections.value.filter(col => col.properties.contents?.event);
|
||||
});
|
||||
|
||||
const taskLists = computed(() => {
|
||||
return collections.value.filter(col => col.contents?.task);
|
||||
return collections.value.filter(col => col.properties.contents?.task);
|
||||
});
|
||||
|
||||
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(() => {
|
||||
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
|
||||
@@ -75,11 +77,11 @@ const isTaskView = computed(() => viewMode.value === 'tasks');
|
||||
|
||||
const filteredEvents = computed(() => {
|
||||
const visibleCalendarIds = calendars.value
|
||||
.filter(cal => cal.enabled !== false)
|
||||
.map(cal => cal.id);
|
||||
.filter(cal => cal.properties.visibility !== false)
|
||||
.map(cal => cal.identifier);
|
||||
|
||||
return events.value.filter(event =>
|
||||
visibleCalendarIds.includes(event.in)
|
||||
visibleCalendarIds.includes(event.collection)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -94,8 +96,8 @@ function selectCalendar(calendar: CollectionObject) {
|
||||
}
|
||||
|
||||
function createCalendar() {
|
||||
editingCollection.value = collectionsStore.fresh();
|
||||
editingCollection.value.contents = { event: true };
|
||||
editingCollection.value = new CollectionObject();
|
||||
editingCollection.value.properties.contents = { event: true };
|
||||
collectionEditorMode.value = 'create';
|
||||
collectionEditorType.value = 'calendar';
|
||||
showCollectionEditor.value = true;
|
||||
@@ -110,7 +112,12 @@ function editCalendar(collection: CollectionObject) {
|
||||
|
||||
async function toggleCalendarVisibility(collection: CollectionObject) {
|
||||
try {
|
||||
await collectionsStore.modify(collection);
|
||||
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);
|
||||
@@ -123,8 +130,8 @@ function selectTaskList(list: CollectionObject) {
|
||||
}
|
||||
|
||||
function createTaskList() {
|
||||
editingCollection.value = collectionsStore.fresh();
|
||||
editingCollection.value.contents = { task: true };
|
||||
editingCollection.value = new CollectionObject();
|
||||
editingCollection.value.properties.contents = { task: true };
|
||||
collectionEditorMode.value = 'create';
|
||||
collectionEditorType.value = 'tasklist';
|
||||
showCollectionEditor.value = true;
|
||||
@@ -148,9 +155,8 @@ function createEvent() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create fresh event entity
|
||||
selectedEntity.value = entitiesStore.fresh('event');
|
||||
selectedEntity.value.in = selectedCollection.value.id;
|
||||
selectedEntity.value = new EntityObject();
|
||||
selectedEntity.value.properties = new EventObject();
|
||||
entityEditorMode.value = 'edit';
|
||||
showEventEditor.value = true;
|
||||
}
|
||||
@@ -171,15 +177,25 @@ async function saveEvent(entity: EntityObject, collection?: CollectionObject | n
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity.data) {
|
||||
entity.data.modified = new Date();
|
||||
}
|
||||
const eventData = entity.properties as EventObject;
|
||||
eventData.modified = new Date().toISOString();
|
||||
|
||||
if (entity.id === null) {
|
||||
entity.data.created = new Date();
|
||||
selectedEntity.value = await entitiesStore.create(collection, entity);
|
||||
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.modify(collection, entity);
|
||||
selectedEntity.value = await entitiesStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
entity.identifier,
|
||||
eventData.toJson()
|
||||
);
|
||||
}
|
||||
|
||||
entityEditorMode.value = 'view';
|
||||
@@ -191,14 +207,14 @@ async function saveEvent(entity: EntityObject, collection?: CollectionObject | n
|
||||
async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = collections.value.find(c => c.id === entity.in);
|
||||
collection = collections.value.find(c => c.identifier === entity.collection);
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection found');
|
||||
return;
|
||||
}
|
||||
|
||||
await entitiesStore.destroy(collection, entity);
|
||||
await entitiesStore.delete(collection.provider, collection.service, collection.identifier, entity.identifier);
|
||||
selectedEntity.value = null;
|
||||
entityEditorMode.value = 'view';
|
||||
showEventEditor.value = false;
|
||||
@@ -218,12 +234,11 @@ function handleDateClick(date: Date) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedEntity.value = entitiesStore.fresh('event');
|
||||
selectedEntity.value.in = selectedCollection.value.id;
|
||||
if (selectedEntity.value.data) {
|
||||
selectedEntity.value.data.startsOn = date;
|
||||
selectedEntity.value.data.endsOn = new Date(date.getTime() + 60 * 60 * 1000);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -257,9 +272,8 @@ function createTask() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create fresh task entity
|
||||
selectedEntity.value = entitiesStore.fresh('task');
|
||||
selectedEntity.value.in = selectedCollection.value.id;
|
||||
selectedEntity.value = new EntityObject();
|
||||
selectedEntity.value.properties = new TaskObject();
|
||||
entityEditorMode.value = 'edit';
|
||||
showTaskEditor.value = true;
|
||||
}
|
||||
@@ -280,15 +294,25 @@ async function saveTask(entity: EntityObject, collection?: CollectionObject | nu
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity.data) {
|
||||
entity.data.modified = new Date();
|
||||
}
|
||||
const taskData = entity.properties as TaskObject;
|
||||
taskData.modified = new Date().toISOString();
|
||||
|
||||
if (entity.id === null) {
|
||||
entity.data.created = new Date();
|
||||
selectedEntity.value = await entitiesStore.create(collection, entity);
|
||||
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.modify(collection, entity);
|
||||
selectedEntity.value = await entitiesStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
entity.identifier,
|
||||
taskData.toJson()
|
||||
);
|
||||
}
|
||||
|
||||
entityEditorMode.value = 'view';
|
||||
@@ -300,14 +324,14 @@ async function saveTask(entity: EntityObject, collection?: CollectionObject | nu
|
||||
async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = collections.value.find(c => c.id === entity.in);
|
||||
collection = collections.value.find(c => c.identifier === entity.collection);
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection found');
|
||||
return;
|
||||
}
|
||||
|
||||
await entitiesStore.destroy(collection, entity);
|
||||
await entitiesStore.delete(collection.provider, collection.service, collection.identifier, entity.identifier);
|
||||
selectedEntity.value = null;
|
||||
entityEditorMode.value = 'view';
|
||||
showTaskEditor.value = false;
|
||||
@@ -318,21 +342,27 @@ async function deleteTask(entity: EntityObject, collection?: CollectionObject |
|
||||
|
||||
async function toggleTaskComplete(taskId: string | number) {
|
||||
try {
|
||||
const entity = entities.value.find(e => e.id === taskId);
|
||||
if (!entity || !entity.data) return;
|
||||
const entity = entities.value.find(e => e.identifier === taskId);
|
||||
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;
|
||||
|
||||
const taskData = entity.data as any;
|
||||
const taskData = entity.properties as TaskObject;
|
||||
const isCompleted = taskData.status === '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.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) {
|
||||
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) {
|
||||
try {
|
||||
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);
|
||||
} else {
|
||||
await collectionsStore.modify(collection);
|
||||
await collectionsStore.update(
|
||||
collection.provider,
|
||||
collection.service,
|
||||
collection.identifier,
|
||||
collection.properties
|
||||
);
|
||||
console.log('[Chrono] - Modified collection:', collection);
|
||||
}
|
||||
// Reload collections
|
||||
collections.value = await collectionsStore.list();
|
||||
collections.value = Object.values(await collectionsStore.list());
|
||||
} catch (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) {
|
||||
try {
|
||||
await collectionsStore.destroy(collection);
|
||||
await collectionsStore.delete(collection.provider, collection.service, collection.identifier);
|
||||
console.log('[Chrono] - Deleted collection:', collection);
|
||||
// Reload collections
|
||||
collections.value = await collectionsStore.list();
|
||||
if (selectedCollection.value?.id === collection.id) {
|
||||
collections.value = Object.values(await collectionsStore.list());
|
||||
if (selectedCollection.value?.identifier === collection.identifier) {
|
||||
selectedCollection.value = null;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -372,10 +412,28 @@ async function deleteCollection(collection: CollectionObject) {
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load collections (calendars and task lists)
|
||||
collections.value = await collectionsStore.list();
|
||||
collections.value = Object.values(await collectionsStore.list());
|
||||
|
||||
// Load entities (events and tasks)
|
||||
entities.value = await entitiesStore.list(null, null, null);
|
||||
// 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,
|
||||
|
||||
@@ -38,9 +38,9 @@ export function daysDiff(a: Date, b: Date): number {
|
||||
* Check if an event spans multiple days
|
||||
*/
|
||||
export function isMultiDay(entity: any): boolean {
|
||||
if (!entity.data?.startsOn) return false;
|
||||
const start = startOfDay(new Date(entity.data.startsOn));
|
||||
const end = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : start;
|
||||
if (!entity.properties?.startsOn) return false;
|
||||
const start = startOfDay(new Date(entity.properties.startsOn));
|
||||
const end = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : start;
|
||||
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)
|
||||
*/
|
||||
export function isAllDay(entity: any): boolean {
|
||||
if (!entity.data?.startsOn) return false;
|
||||
if (!entity.properties?.startsOn) return false;
|
||||
|
||||
// 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
|
||||
if (isMultiDay(entity)) return true;
|
||||
@@ -63,9 +63,9 @@ export function isAllDay(entity: any): boolean {
|
||||
* Check if an event overlaps with a date range
|
||||
*/
|
||||
export function eventOverlapsRange(entity: any, rangeStart: Date, rangeEnd: Date): boolean {
|
||||
if (!entity.data?.startsOn) return false;
|
||||
const eventStart = startOfDay(new Date(entity.data.startsOn));
|
||||
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : eventStart;
|
||||
if (!entity.properties?.startsOn) return false;
|
||||
const eventStart = startOfDay(new Date(entity.properties.startsOn));
|
||||
const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
|
||||
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
|
||||
*/
|
||||
export function eventOnDate(entity: any, date: Date): boolean {
|
||||
if (!entity.data?.startsOn) return false;
|
||||
const eventStart = startOfDay(new Date(entity.data.startsOn));
|
||||
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : eventStart;
|
||||
if (!entity.properties?.startsOn) return false;
|
||||
const eventStart = startOfDay(new Date(entity.properties.startsOn));
|
||||
const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
|
||||
const targetDate = startOfDay(date);
|
||||
return eventStart <= targetDate && eventEnd >= targetDate;
|
||||
}
|
||||
@@ -95,17 +95,17 @@ export function getMultiDaySegments(
|
||||
|
||||
// Filter multi-day/all-day events that overlap this range
|
||||
const multiDayEvents = events.filter(entity => {
|
||||
if (!entity.data?.startsOn) return false;
|
||||
if (!entity.properties?.startsOn) return false;
|
||||
if (!isAllDay(entity)) return false;
|
||||
return eventOverlapsRange(entity, rangeStart, rangeEnd);
|
||||
});
|
||||
|
||||
// Sort: longer events first, then by start date
|
||||
multiDayEvents.sort((a, b) => {
|
||||
const aStart = new Date(a.data.startsOn);
|
||||
const aEnd = new Date(a.data.endsOn || a.data.startsOn);
|
||||
const bStart = new Date(b.data.startsOn);
|
||||
const bEnd = new Date(b.data.endsOn || b.data.startsOn);
|
||||
const aStart = new Date(a.properties.startsOn);
|
||||
const aEnd = new Date(a.properties.endsOn || a.properties.startsOn);
|
||||
const bStart = new Date(b.properties.startsOn);
|
||||
const bEnd = new Date(b.properties.endsOn || b.properties.startsOn);
|
||||
const aDuration = daysDiff(aStart, aEnd);
|
||||
const bDuration = daysDiff(bStart, bEnd);
|
||||
if (bDuration !== aDuration) return bDuration - aDuration;
|
||||
@@ -116,8 +116,8 @@ export function getMultiDaySegments(
|
||||
const lanes: boolean[][] = [];
|
||||
|
||||
for (const entity of multiDayEvents) {
|
||||
const eventStart = startOfDay(new Date(entity.data.startsOn));
|
||||
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : eventStart;
|
||||
const eventStart = startOfDay(new Date(entity.properties.startsOn));
|
||||
const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
|
||||
|
||||
// Clamp to visible range
|
||||
const segStart = eventStart < rangeStart ? rangeStart : eventStart;
|
||||
@@ -173,9 +173,9 @@ export function getMultiDaySegments(
|
||||
*/
|
||||
export function getTimedEventsForDate(events: any[], date: Date): any[] {
|
||||
return events.filter(entity => {
|
||||
if (!entity.data?.startsOn) return false;
|
||||
if (!entity.properties?.startsOn) 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();
|
||||
});
|
||||
}
|
||||
@@ -185,9 +185,9 @@ export function getTimedEventsForDate(events: any[], date: Date): any[] {
|
||||
*/
|
||||
export function getSingleDayEventsForDate(events: any[], date: Date): any[] {
|
||||
return events.filter(entity => {
|
||||
if (!entity.data?.startsOn) return false;
|
||||
if (!entity.properties?.startsOn) 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();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user