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