feat: use module store

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-02-17 19:11:12 -05:00
parent 6a7b28b86d
commit b663efbb3e
10 changed files with 883 additions and 612 deletions
+53 -34
View File
@@ -9,12 +9,16 @@
</div>
<v-spacer />
<div class="view-selector">
<v-btn size="x-small" variant="text" @click="setSpan('1d')" :color="selectedSpan === '1d' ? 'primary' : undefined">1D</v-btn>
<v-btn size="x-small" variant="text" @click="setSpan('3d')" :color="selectedSpan === '3d' ? 'primary' : undefined">3D</v-btn>
<v-btn size="x-small" variant="text" @click="setSpan('1w')" :color="selectedSpan === '1w' ? 'primary' : undefined">1W</v-btn>
<v-btn size="x-small" variant="text" @click="setSpan('2w')" :color="selectedSpan === '2w' ? 'primary' : undefined">2W</v-btn>
<v-btn size="x-small" variant="text" @click="setSpan('3w')" :color="selectedSpan === '3w' ? 'primary' : undefined">3W</v-btn>
<v-btn size="x-small" variant="text" @click="setSpan('1m')" :color="selectedSpan === '1m' ? 'primary' : undefined">1M</v-btn>
<v-btn
v-for="span in AGENDA_VIEW_SPANS"
:key="span"
size="x-small"
variant="text"
@click="setSpan(span)"
:color="selectedSpan === span ? 'primary' : undefined"
>
{{ span.toUpperCase() }}
</v-btn>
</div>
</div>
<v-list class="agenda-list">
@@ -35,7 +39,7 @@
</template>
<v-list-item-title>{{ entity.properties?.label || 'Untitled' }}</v-list-item-title>
<v-list-item-subtitle>
{{ entity.properties?.timeless ? 'All day' : `${entity.properties?.startsOn ? formatTime(new Date(entity.properties.startsOn)) : ''} - ${entity.properties?.endsOn ? formatTime(new Date(entity.properties.endsOn)) : ''}` }}
{{ getEventProperties(entity).timeless ? 'All day' : `${formatEventDateTime(getEventProperties(entity).startsOn)} - ${formatEventDateTime(getEventProperties(entity).endsOn)}` }}
</v-list-item-subtitle>
</v-list-item>
</template>
@@ -45,23 +49,29 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { AGENDA_VIEW_SPANS, spanToDays, type AgendaViewSpan } from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
import { EventObject } from '@ChronoManager/models/event';
type SpanType = '1d' | '3d' | '1w' | '2w' | '3w' | '1m';
type CalendarEntity = EntityObject;
type CalendarCollection = CollectionObject;
const props = defineProps<{
events: any[];
calendars: any[];
events: CalendarEntity[];
calendars: CalendarCollection[];
currentDate?: Date;
initialSpan?: SpanType;
initialSpan?: AgendaViewSpan;
}>();
defineEmits<{
'event-click': [event: any];
}>();
const selectedSpan = ref<SpanType>(props.initialSpan ?? '1w');
const selectedSpan = ref<AgendaViewSpan>(props.initialSpan ?? '1w');
const localDate = ref(new Date(props.currentDate ?? new Date()));
const emit = defineEmits<{
'event-click': [event: CalendarEntity];
'update:span': [span: AgendaViewSpan];
}>();
// Watch for external date changes (e.g., from mini calendar)
watch(() => props.currentDate, (newDate) => {
if (newDate) {
@@ -69,20 +79,19 @@ watch(() => props.currentDate, (newDate) => {
}
});
function setSpan(span: SpanType) {
watch(() => props.initialSpan, (newSpan) => {
if (newSpan && newSpan !== selectedSpan.value) {
selectedSpan.value = newSpan;
}
});
function setSpan(span: AgendaViewSpan) {
selectedSpan.value = span;
emit('update:span', span);
}
function getSpanDays(): number {
switch (selectedSpan.value) {
case '1d': return 1;
case '3d': return 3;
case '1w': return 7;
case '2w': return 14;
case '3w': return 21;
case '1m': return 30;
default: return 7;
}
return spanToDays(selectedSpan.value);
}
function previousPeriod() {
@@ -128,21 +137,22 @@ const dateRangeLabel = computed(() => {
});
const groupedEvents = computed(() => {
const grouped: Record<string, any[]> = {};
const grouped: Record<string, CalendarEntity[]> = {};
const { start, end } = dateRange.value;
const filtered = props.events.filter(e => {
if (!e.properties?.startsOn) return false;
const eventStart = new Date(e.properties.startsOn);
const filtered = props.events.filter((e): e is CalendarEntity => {
const startsOn = getEventProperties(e).startsOn;
if (typeof startsOn !== 'string') return false;
const eventStart = new Date(startsOn);
return eventStart >= start && eventStart <= end;
});
const sorted = filtered.sort((a, b) =>
new Date(a.properties.startsOn).getTime() - new Date(b.properties.startsOn).getTime()
new Date(getEventProperties(a).startsOn || 0).getTime() - new Date(getEventProperties(b).startsOn || 0).getTime()
);
sorted.forEach(entity => {
const dateKey = new Date(entity.properties.startsOn).toDateString();
const dateKey = new Date(getEventProperties(entity).startsOn || 0).toDateString();
if (!grouped[dateKey]) {
grouped[dateKey] = [];
}
@@ -152,16 +162,25 @@ const groupedEvents = computed(() => {
return grouped;
});
function getEventColor(entity: any): string {
if (entity.properties?.color) return entity.properties.color;
function getEventColor(entity: CalendarEntity): string {
const event = getEventProperties(entity);
if (event.color) return event.color;
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.properties?.color || '#1976D2';
}
function getEventProperties(entity: CalendarEntity): EventObject {
return entity.properties as EventObject;
}
function formatTime(date: Date): string {
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
}
function formatEventDateTime(value: string | null): string {
return value ? formatTime(new Date(value)) : '';
}
function formatAgendaDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
+18 -6
View File
@@ -21,16 +21,21 @@
:current-date="currentDate"
:events="events"
:calendars="calendars"
:initial-span="initialDaysSpan"
@event-click="$emit('event-click', $event)"
@date-click="$emit('date-click', $event)"
@event-hover="handleEventHover"
@event-hover-end="hidePopup"
@update:span="$emit('update:days-span', $event)"
/>
<AgendaView
v-else-if="view === 'agenda'"
:events="events"
:calendars="calendars"
:current-date="currentDate"
:initial-span="initialAgendaViewSpan"
@event-click="$emit('event-click', $event)"
@update:span="$emit('update:agenda-span', $event)"
/>
</div>
</template>
@@ -41,26 +46,33 @@ import MonthView from './MonthView.vue';
import DaysView from './DaysView.vue';
import AgendaView from './AgendaView.vue';
import EventViewerPopup from './EventViewerPopup.vue';
import type { AgendaViewSpan, DaysViewSpan } from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
defineProps<{
view: 'days' | 'month' | 'agenda';
currentDate: Date;
events: any[];
calendars: any[];
events: EntityObject[];
calendars: CollectionObject[];
initialDaysSpan?: DaysViewSpan;
initialAgendaViewSpan?: AgendaViewSpan;
}>();
defineEmits<{
'event-click': [event: any];
'event-click': [event: EntityObject];
'date-click': [date: Date];
'update:days-span': [span: DaysViewSpan];
'update:agenda-span': [span: AgendaViewSpan];
}>();
// Popup state
const hoveredEvent = ref<any>(null);
const hoveredEvent = ref<EntityObject | null>(null);
const popupPosition = ref({ x: 0, y: 0 });
const showPopup = ref(false);
let hideTimeout: NodeJS.Timeout | null = null;
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
function handleEventHover(data: { event: MouseEvent; entity: any }) {
function handleEventHover(data: { event: MouseEvent; entity: EntityObject }) {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
+6 -40
View File
@@ -1,13 +1,10 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore'
import { computed } from 'vue'
import { CollectionObject } from '@ChronoManager/models/collection';
// Store
const collectionsStore = useCollectionsStore()
// Props
const props = defineProps<{
collections: CollectionObject[]
selectedCollection?: CollectionObject | null
type?: 'calendar' | 'tasklist'
}>()
@@ -19,15 +16,11 @@ const emit = defineEmits<{
'toggle-visibility': [collection: CollectionObject]
}>()
// State
const loading = ref(false)
const collections = ref<CollectionObject[]>([])
// Computed
const filteredCollections = computed(() => {
if (!props.type) return collections.value
if (!props.type) return props.collections
return collections.value.filter(collection => {
return props.collections.filter(collection => {
if (props.type === 'calendar') {
return collection.properties.contents?.event
} else if (props.type === 'tasklist') {
@@ -49,17 +42,6 @@ const displayIcon = computed(() => {
return 'mdi-folder'
})
// Lifecycle
onMounted(async () => {
loading.value = true
try {
collections.value = Object.values(await collectionsStore.list())
} catch (error) {
console.error('[Chrono] - Failed to load collections:', error)
}
loading.value = false
})
// Functions
const onCollectionSelect = (collection: CollectionObject) => {
console.log('[Chrono] - Collection selected', collection)
@@ -71,22 +53,8 @@ const onCollectionEdit = (collection: CollectionObject) => {
}
const onToggleVisibility = (collection: CollectionObject) => {
collection.properties.visibility = collection.properties.visibility === false
emit('toggle-visibility', collection)
}
// Expose refresh method
defineExpose({
async refresh() {
loading.value = true
try {
collections.value = Object.values(await collectionsStore.list())
} catch (error) {
console.error('[Chrono] - Failed to load collections:', error)
}
loading.value = false
}
})
</script>
<template>
@@ -101,9 +69,7 @@ defineExpose({
<v-divider class="my-2" />
<div class="collection-selector-content">
<v-progress-linear v-if="loading" indeterminate color="primary" />
<v-list v-else density="compact" nav class="pa-0">
<v-list density="compact" nav class="pa-0">
<v-list-item
v-for="collection in filteredCollections"
:key="collection.identifier"
@@ -142,7 +108,7 @@ defineExpose({
</v-list-item>
</v-list>
<v-alert v-if="!loading && filteredCollections.length === 0" type="info" variant="tonal" density="compact" class="mt-2">
<v-alert v-if="filteredCollections.length === 0" type="info" variant="tonal" density="compact" class="mt-2">
No {{ displayTitle.toLowerCase() }} found
</v-alert>
</div>
+61 -31
View File
@@ -9,13 +9,16 @@
</div>
<v-spacer />
<div class="view-selector">
<v-btn size="x-small" variant="text" @click="setDays(1)" :color="daysCount === 1 ? 'primary' : undefined">1D</v-btn>
<v-btn size="x-small" variant="text" @click="setDays(3)" :color="daysCount === 3 ? 'primary' : undefined">3D</v-btn>
<v-btn size="x-small" variant="text" @click="setDays(5)" :color="daysCount === 5 ? 'primary' : undefined">5D</v-btn>
<v-btn size="x-small" variant="text" @click="setDays(7)" :color="daysCount === 7 ? 'primary' : undefined">7D</v-btn>
<v-btn size="x-small" variant="text" @click="setDays(9)" :color="daysCount === 9 ? 'primary' : undefined">9D</v-btn>
<v-btn size="x-small" variant="text" @click="setDays(11)" :color="daysCount === 11 ? 'primary' : undefined">11D</v-btn>
<v-btn size="x-small" variant="text" @click="setDays(14)" :color="daysCount === 14 ? 'primary' : undefined">14D</v-btn>
<v-btn
v-for="span in DAYS_VIEW_SPANS"
:key="span"
size="x-small"
variant="text"
@click="setSpan(span)"
:color="selectedSpan === span ? 'primary' : undefined"
>
{{ span.toUpperCase() }}
</v-btn>
</div>
</div>
<div class="days-grid">
@@ -80,12 +83,12 @@
>
<template v-if="daysCount <= 3">
<div class="event-time">
{{ entity.properties?.startsOn ? formatTime(new Date(entity.properties.startsOn)) : '' }} - {{ entity.properties?.endsOn ? formatTime(new Date(entity.properties.endsOn)) : '' }}
{{ formatEventDateTime(getEventProperties(entity).startsOn) }} - {{ formatEventDateTime(getEventProperties(entity).endsOn) }}
</div>
<div class="event-title">{{ entity.properties?.label || 'Untitled' }}</div>
<div class="event-title">{{ getEventProperties(entity).label || 'Untitled' }}</div>
</template>
<template v-else>
<div class="event-title-compact">{{ entity.properties?.label || 'Untitled' }}</div>
<div class="event-title-compact">{{ getEventProperties(entity).label || 'Untitled' }}</div>
</template>
</div>
</div>
@@ -104,26 +107,49 @@ import {
getTimedEventsForDate,
type MultiDaySegment,
} from '@/utils/calendarHelpers';
import { DAYS_VIEW_SPANS, spanToDays, type DaysViewSpan } from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
import { EventObject } from '@ChronoManager/models/event';
const ALL_DAY_EVENT_HEIGHT = 24;
type CalendarEntity = EntityObject;
type CalendarCollection = CollectionObject;
const props = defineProps<{
currentDate: Date;
events: any[];
calendars: any[];
initialDays?: number;
events: CalendarEntity[];
calendars: CalendarCollection[];
initialSpan?: DaysViewSpan;
}>();
const daysCount = ref(props.initialDays ?? 7);
const selectedSpan = ref<DaysViewSpan>(props.initialSpan ?? '7d');
const daysCount = computed(() => spanToDays(selectedSpan.value));
const localDate = ref(new Date(props.currentDate));
const emit = defineEmits<{
'event-click': [event: CalendarEntity];
'event-hover': [data: { event: MouseEvent; entity: CalendarEntity }];
'event-hover-end': [];
'date-click': [date: Date];
'update:span': [span: DaysViewSpan];
}>();
// Watch for external date changes (e.g., from mini calendar)
watch(() => props.currentDate, (newDate) => {
localDate.value = new Date(newDate);
});
function setDays(count: number) {
daysCount.value = count;
watch(() => props.initialSpan, (newSpan) => {
if (newSpan && newSpan !== selectedSpan.value) {
selectedSpan.value = newSpan;
}
});
function setSpan(span: DaysViewSpan) {
selectedSpan.value = span;
emit('update:span', span);
}
function previousPeriod() {
@@ -195,12 +221,17 @@ function isToday(date: Date): boolean {
return date.toDateString() === today.toDateString();
}
function getTimedEvents(date: Date): any[] {
return getTimedEventsForDate(props.events, date);
function getTimedEvents(date: Date): CalendarEntity[] {
return getTimedEventsForDate(props.events, date) as CalendarEntity[];
}
function getEventColor(entity: any): string {
if (entity.properties?.color) return entity.properties.color;
function getEventProperties(entity: CalendarEntity): EventObject {
return entity.properties as EventObject;
}
function getEventColor(entity: CalendarEntity): string {
const event = getEventProperties(entity);
if (event.color) return event.color;
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.properties?.color || '#1976D2';
}
@@ -214,12 +245,14 @@ function getAllDayEventStyle(segment: MultiDaySegment) {
};
}
function getEventStyle(entity: any) {
if (!entity.properties?.startsOn || !entity.properties?.endsOn) {
function getEventStyle(entity: CalendarEntity) {
const event = getEventProperties(entity);
if (!event.startsOn || !event.endsOn) {
return { display: 'none' };
}
const startTime = new Date(entity.properties.startsOn);
const endTime = new Date(entity.properties.endsOn);
const startTime = new Date(event.startsOn);
const endTime = new Date(event.endsOn);
const start = startTime.getHours() * 60 + startTime.getMinutes();
const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60);
@@ -240,17 +273,14 @@ function formatTime(date: Date): string {
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
}
function formatEventDateTime(value: string | null): string {
return value ? formatTime(new Date(value)) : '';
}
function formatWeekDay(date: Date): string {
return date.toLocaleDateString('en-US', { weekday: 'short' });
}
const emit = defineEmits<{
'event-click': [event: any];
'event-hover': [data: { event: MouseEvent; entity: any }];
'event-hover-end': [];
'date-click': [date: Date];
}>();
function handleDayClick(event: MouseEvent, day: Date) {
const target = event.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
+49 -20
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import type { CollectionObject } from '@ChronoManager/models/collection'
import type { EntityObject } from '@ChronoManager/models/entity'
import type { EventObject } from '@ChronoManager/models/event'
@@ -25,20 +25,22 @@ const emit = defineEmits<{
'edit': []
'cancel': []
'close': []
'save': [entity: any, collection?: any]
'delete': [entity: any, collection?: any]
'save': [entity: EntityObject, collection?: CollectionObject | null]
'delete': [entity: EntityObject, collection?: CollectionObject | null]
}>()
// State
const loading = ref(false)
const saving = ref(false)
const selectedCollectionId = ref(props.entity?.collection || props.collection?.identifier || props.calendars?.[0]?.identifier || '')
const initialEntity = props.entity as EntityObject | null
const draftEntity = ref<EntityObject | null>(initialEntity?.clone ? initialEntity.clone() : null)
const selectedCollectionId = ref<string | number>('')
// Computed
const mode = computed(() => props.mode)
const entity = computed(() => props.entity || null)
const entityObject = computed(() => entity.value?.properties as EventObject ?? null)
const entityFresh = computed(() => !entity.value?.identifier)
const editableEntity = computed(() => draftEntity.value)
const entityObject = computed(() => editableEntity.value?.properties as EventObject ?? null)
const entityFresh = computed(() => !editableEntity.value?.identifier)
const calendarOptions = computed(() =>
(props.calendars || []).map(cal => ({
@@ -69,50 +71,77 @@ const startEdit = () => {
}
const cancelEdit = () => {
const entity = props.entity as EntityObject | null
draftEntity.value = entity?.clone ? entity.clone() : null
emit('cancel')
}
const saveEntity = async () => {
if (!editableEntity.value) {
return
}
saving.value = true
const targetCollection = (props.calendars || []).find(cal => cal.identifier === selectedCollectionId.value)
emit('save', entity.value!, targetCollection || props.collection)
emit('save', editableEntity.value as EntityObject, targetCollection || props.collection)
saving.value = false
}
// Location management
const addLocationPhysical = () => {
(entityObject.value as any)?.addLocationPhysical()
entityObject.value?.addLocationPhysical()
}
const removeLocationPhysical = (key: string) => {
(entityObject.value as any)?.removeLocationPhysical(key)
entityObject.value?.removeLocationPhysical(key)
}
const addLocationVirtual = () => {
(entityObject.value as any)?.addLocationVirtual()
entityObject.value?.addLocationVirtual()
}
const removeLocationVirtual = (key: string) => {
(entityObject.value as any)?.removeLocationVirtual(key)
entityObject.value?.removeLocationVirtual(key)
}
// Participant management
const addParticipant = () => {
(entityObject.value as any)?.addParticipant()
entityObject.value?.addParticipant()
}
const removeParticipant = (key: string) => {
(entityObject.value as any)?.removeParticipant(key)
entityObject.value?.removeParticipant(key)
}
// Notification management
const addNotification = () => {
(entityObject.value as any)?.addNotification()
entityObject.value?.addNotification()
}
const removeNotification = (key: string) => {
(entityObject.value as any)?.removeNotification(key)
entityObject.value?.removeNotification(key)
}
watch(
() => props.entity,
(newEntity) => {
const entity = newEntity as EntityObject | null
draftEntity.value = entity?.clone ? entity.clone() : null
selectedCollectionId.value = entity?.collection || props.collection?.identifier || props.calendars?.[0]?.identifier || ''
},
{ immediate: true },
)
watch(
() => props.mode,
(newMode) => {
const entity = props.entity as EntityObject | null
if (newMode === 'edit' && entity?.clone) {
draftEntity.value = entity.clone()
selectedCollectionId.value = entity.collection || props.collection?.identifier || props.calendars?.[0]?.identifier || ''
}
},
)
</script>
<template>
@@ -123,7 +152,7 @@ const removeNotification = (key: string) => {
{{ entityObject?.label || 'Nothing Selected' }}
</div>
<div v-if="!loading && entity">
<div v-if="!loading && editableEntity">
<v-btn
v-if="mode === 'view' && canEdit"
icon="mdi-pencil"
@@ -138,7 +167,7 @@ const removeNotification = (key: string) => {
size="small"
variant="text"
color="error"
@click="emit('delete', entity!, collection)"
@click="emit('delete', editableEntity as EntityObject, collection)"
/>
<v-btn
@@ -154,14 +183,14 @@ const removeNotification = (key: string) => {
<v-divider />
<v-progress-linear v-if="loading" indeterminate color="primary" />
<div v-if="!entity" class="text-center pa-8">
<div v-if="!editableEntity" class="text-center pa-8">
<v-icon icon="mdi-calendar-blank" size="64" color="grey" class="mb-4" />
<p class="text-h6 text-grey">Select an event to view details</p>
</div>
<div v-else class="pa-4">
<v-select
v-if="mode === 'edit' && !entity.identifier"
v-if="mode === 'edit' && !editableEntity?.identifier"
v-model="selectedCollectionId"
:items="calendarOptions"
label="Calendar"
+60 -37
View File
@@ -1,7 +1,7 @@
<template>
<v-card v-if="entity" class="task-editor">
<v-card v-if="editableEntity && taskProperties" class="task-editor">
<v-card-title class="d-flex justify-space-between align-center">
<span>{{ entity.identifier ? entity.properties?.label || 'Untitled Task' : 'New Task' }}</span>
<span>{{ editableEntity.identifier ? taskProperties.label || 'Untitled Task' : 'New Task' }}</span>
<div class="d-flex gap-2">
<v-btn
v-if="mode === 'view' && canEdit"
@@ -22,7 +22,7 @@
<v-card-text class="task-editor-content">
<v-form ref="formRef">
<v-text-field
v-model="entity.properties.label"
v-model="taskProperties.label"
label="Title"
:rules="[v => !!v || 'Title is required']"
:readonly="!isEditing"
@@ -40,7 +40,7 @@
></v-select>
<v-textarea
v-model="entity.properties.description"
v-model="taskProperties.description"
label="Description"
:readonly="!isEditing"
:variant="isEditing ? 'outlined' : 'plain'"
@@ -49,7 +49,7 @@
></v-textarea>
<v-select
v-model="entity.properties.priority"
v-model="taskProperties.priority"
:items="priorityOptions"
label="Priority"
:readonly="!isEditing"
@@ -78,7 +78,7 @@
></v-text-field>
<v-combobox
v-model="entity.properties.categories"
v-model="taskProperties.tags"
label="Tags"
multiple
chips
@@ -90,7 +90,7 @@
<div class="d-flex gap-4">
<v-text-field
v-model.number="entity.properties.estimatedTime"
v-model.number="taskProperties.progress"
label="Estimated Time (minutes)"
type="number"
:readonly="!isEditing"
@@ -98,7 +98,7 @@
class="flex-1-1"
></v-text-field>
<v-text-field
v-model.number="entity.properties.actualTime"
v-model.number="taskProperties.priority"
label="Actual Time (minutes)"
type="number"
:readonly="!isEditing"
@@ -108,13 +108,13 @@
</div>
<!-- Metadata for view mode -->
<div v-if="mode === 'view' && entity.properties.created" class="mt-4">
<div v-if="mode === 'view' && taskProperties.created" class="mt-4">
<v-divider class="mb-4"></v-divider>
<div class="text-caption text-medium-emphasis">
<div>Created: {{ new Date(entity.properties.created).toLocaleString() }}</div>
<div v-if="entity.properties.modified">Modified: {{ new Date(entity.properties.modified).toLocaleString() }}</div>
<div v-if="entity.properties.status === 'completed' && entity.properties.completedOn">
Completed: {{ new Date(entity.properties.completedOn).toLocaleString() }}
<div>Created: {{ new Date(taskProperties.created).toLocaleString() }}</div>
<div v-if="taskProperties.modified">Modified: {{ new Date(taskProperties.modified).toLocaleString() }}</div>
<div v-if="taskProperties.status === 'completed' && taskProperties.completedOn">
Completed: {{ new Date(taskProperties.completedOn).toLocaleString() }}
</div>
<div v-if="isExternalTask" class="mt-2">
<v-chip size="small" color="info" variant="tonal">
@@ -129,7 +129,7 @@
<v-divider></v-divider>
<v-card-actions>
<v-btn
v-if="mode === 'edit' && entity.identifier && canDelete"
v-if="mode === 'edit' && editableEntity.identifier && canDelete"
color="error"
variant="text"
@click="handleDelete"
@@ -165,34 +165,43 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import type { CollectionObject } from '@ChronoManager/models/collection';
import { EntityObject } from '@ChronoManager/models/entity';
import { TaskObject } from '@ChronoManager/models/task';
const props = defineProps<{
mode: 'view' | 'edit';
entity: any | null;
collection: any | null;
lists: any[];
entity: EntityObject | null;
collection: CollectionObject | null;
lists: CollectionObject[];
}>();
const emit = defineEmits<{
save: [entity: any, collection?: any];
delete: [entity: any, collection?: any];
save: [entity: EntityObject, collection?: CollectionObject | null];
delete: [entity: EntityObject, collection?: CollectionObject | null];
edit: [];
cancel: [];
close: [];
}>();
function cloneEntity(entity: EntityObject | null): EntityObject | null {
return entity?.clone ? entity.clone() : null;
}
const formRef = ref();
const selectedCollectionId = ref(props.entity?.collection || props.collection?.identifier || props.lists[0]?.identifier || '');
const draftEntity = ref<EntityObject | null>(cloneEntity(props.entity));
const selectedCollectionId = ref<string | number>('');
const editableEntity = computed(() => draftEntity.value);
const taskProperties = computed(() => editableEntity.value?.properties as TaskObject | null);
// Determine permissions based on entity metadata
const isExternalTask = computed(() => {
// Check if task was assigned by another user
return props.entity?.properties?.external === true || props.entity?.properties?.assignedBy;
return false;
});
const canEdit = computed(() => {
if (!props.entity) return false;
if (props.entity.properties?.readonly === true) return false;
if (!editableEntity.value) return false;
return true;
});
@@ -203,12 +212,12 @@ const canEditDates = computed(() => {
const canChangeCollection = computed(() => {
// Can't move external tasks to different lists
return !isExternalTask.value && !props.entity?.identifier;
return !isExternalTask.value && !editableEntity.value?.identifier;
});
const canDelete = computed(() => {
// Can't delete external/readonly tasks
return !isExternalTask.value && props.entity?.properties?.readonly !== true;
return !isExternalTask.value;
});
const isEditing = computed(() => props.mode === 'edit');
@@ -218,21 +227,32 @@ const startDate = ref('');
// Watch for entity changes to update date fields
watch(() => props.entity, (newEntity) => {
if (newEntity?.properties?.dueOn) {
dueDate.value = formatDate(new Date(newEntity.properties.dueOn));
draftEntity.value = cloneEntity(newEntity);
const task = draftEntity.value?.properties as TaskObject | undefined;
if (task?.dueOn) {
dueDate.value = formatDate(new Date(task.dueOn));
} else {
dueDate.value = '';
}
if (newEntity?.properties?.startsOn) {
startDate.value = formatDate(new Date(newEntity.properties.startsOn));
if (task?.startsOn) {
startDate.value = formatDate(new Date(task.startsOn));
} else {
startDate.value = '';
}
selectedCollectionId.value = newEntity?.collection || props.collection?.identifier || props.lists[0]?.identifier || '';
selectedCollectionId.value = draftEntity.value?.collection || props.collection?.identifier || props.lists[0]?.identifier || '';
}, { immediate: true });
watch(() => props.mode, (newMode) => {
if (newMode === 'edit' && props.entity) {
draftEntity.value = cloneEntity(props.entity);
selectedCollectionId.value = draftEntity.value?.collection || props.collection?.identifier || props.lists[0]?.identifier || '';
}
});
const listOptions = computed(() =>
props.lists.map(list => ({
title: list.properties.label || 'Unnamed List',
@@ -247,14 +267,14 @@ const priorityOptions = [
];
watch(dueDate, (newVal) => {
if (props.entity && isEditing.value) {
props.entity.properties.dueOn = newVal ? new Date(newVal).toISOString() : null;
if (taskProperties.value && isEditing.value) {
taskProperties.value.dueOn = newVal ? new Date(newVal).toISOString() as never : null;
}
});
watch(startDate, (newVal) => {
if (props.entity && isEditing.value) {
props.entity.properties.startsOn = newVal ? new Date(newVal).toISOString() : null;
if (taskProperties.value && isEditing.value) {
taskProperties.value.startsOn = newVal ? new Date(newVal).toISOString() as never : null;
}
});
@@ -263,21 +283,24 @@ function formatDate(date: Date): string {
}
async function handleSave() {
if (!editableEntity.value) return;
const { valid } = await formRef.value.validate();
if (!valid) return;
const targetCollection = props.lists.find(list => list.identifier === selectedCollectionId.value);
emit('save', props.entity, targetCollection || props.collection);
emit('save', editableEntity.value as EntityObject, targetCollection || props.collection);
}
function handleCancel() {
draftEntity.value = cloneEntity(props.entity);
emit('cancel');
}
function handleDelete() {
if (props.entity) {
if (editableEntity.value) {
const targetCollection = props.lists.find(list => list.identifier === selectedCollectionId.value);
emit('delete', props.entity, targetCollection || props.collection);
emit('delete', editableEntity.value as EntityObject, targetCollection || props.collection);
}
}
</script>