Files
chrono/src/components/EventEditor.vue
T
Sebastian 41a2de2b3d refactor: code clean up
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-07 20:48:20 -04:00

313 lines
9.2 KiB
Vue

<script setup lang="ts">
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'
import EventEditorLabel from './editors/EventEditorLabel.vue'
import EventEditorDescription from './editors/EventEditorDescription.vue'
import EventEditorDates from './editors/EventEditorDates.vue'
import EventEditorLocations from './editors/EventEditorLocations.vue'
import EventEditorParticipants from './editors/EventEditorParticipants.vue'
import EventEditorNotifications from './editors/EventEditorNotifications.vue'
import EventEditorOccurrence from './editors/EventEditorOccurrence.vue'
import EventEditorTags from './editors/EventEditorTags.vue'
// Props - wrapped entity with id, data properties
const props = defineProps<{
mode: 'edit' | 'view'
entity: EntityObject | null
collection?: CollectionObject | null
calendars?: CollectionObject[]
}>()
// Emits
const emit = defineEmits<{
'edit': []
'cancel': []
'close': []
'save': [entity: EntityObject, collection?: CollectionObject | null]
'delete': [entity: EntityObject, collection?: CollectionObject | null]
}>()
// State
const loading = ref(false)
const saving = ref(false)
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 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 => ({
title: cal.properties.label || 'Unnamed Calendar',
value: cal.identifier,
}))
)
const selectedCalendar = computed(() =>
(props.calendars || []).find(calendar => calendar.identifier === selectedCollectionId.value) ?? null
)
// Permissions
const isExternalEvent = computed(() => {
return false
})
const canEdit = computed(() => {
return true
})
const canDelete = computed(() => {
return !isExternalEvent.value
})
// UI helpers
const entityIcon = computed(() => 'mdi-calendar')
// Functions
const startEdit = () => {
emit('edit')
}
const cancelEdit = () => {
const entity = props.entity as EntityObject | null
draftEntity.value = entity?.clone ? entity.clone() : null
emit('cancel')
}
const saveEntity = async () => {
if (!editableEntity.value || !selectedCalendar.value) {
return
}
saving.value = true
emit('save', editableEntity.value as EntityObject, selectedCalendar.value)
saving.value = false
}
// Location management
const addLocationPhysical = () => {
entityObject.value?.addLocationPhysical()
}
const removeLocationPhysical = (key: string) => {
entityObject.value?.removeLocationPhysical(key)
}
const addLocationVirtual = () => {
entityObject.value?.addLocationVirtual()
}
const removeLocationVirtual = (key: string) => {
entityObject.value?.removeLocationVirtual(key)
}
// Participant management
const addParticipant = () => {
entityObject.value?.addParticipant()
}
const removeParticipant = (key: string) => {
entityObject.value?.removeParticipant(key)
}
// Notification management
const addNotification = () => {
entityObject.value?.addNotification()
}
const removeNotification = (key: string) => {
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 || ''
},
{ 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 || ''
}
},
)
</script>
<template>
<v-sheet class="pa-2 text-start overflow-auto" min-height="84vh" max-height="84vh">
<div class="event-editor-header d-flex justify-space-between pa-4">
<div>
<v-icon :icon="entityIcon" class="mr-2" />
{{ entityObject?.label || 'Nothing Selected' }}
</div>
<div v-if="!loading && editableEntity">
<v-btn
v-if="mode === 'view' && canEdit"
icon="mdi-pencil"
size="small"
variant="text"
@click="startEdit"
/>
<v-btn
v-if="!entityFresh && canDelete"
icon="mdi-delete"
size="small"
variant="text"
color="error"
@click="emit('delete', editableEntity as EntityObject, collection)"
/>
<v-btn
icon="mdi-close"
size="small"
variant="text"
@click="$emit('close')"
/>
</div>
</div>
<div class="event-editor-fields">
<v-divider />
<v-progress-linear v-if="loading" indeterminate color="primary" />
<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' && !editableEntity?.identifier"
v-model="selectedCollectionId"
:items="calendarOptions"
label="Calendar"
:rules="[value => (value !== '' && value != null) || 'Calendar is required']"
variant="outlined"
density="compact"
class="mb-4"
/>
<EventEditorLabel
:mode="mode"
:label="entityObject!.label"
@update:label="entityObject!.label = $event"
/>
<EventEditorDescription
:mode="mode"
:description="entityObject!.description"
@update:description="entityObject!.description = $event"
/>
<EventEditorDates
:mode="mode"
:starts-on="entityObject!.startsOn"
:ends-on="entityObject!.endsOn"
:timeless="entityObject!.timeless"
:time-zone="entityObject!.timeZone"
@update:starts-on="entityObject!.startsOn = $event"
@update:ends-on="entityObject!.endsOn = $event"
@update:timeless="entityObject!.timeless = $event"
@update:time-zone="entityObject!.timeZone = $event"
/>
<EventEditorOccurrence
:mode="mode"
:pattern="entityObject!.pattern"
:starts-on="entityObject!.startsOn"
@update:pattern="entityObject!.pattern = $event"
/>
<EventEditorLocations
:mode="mode"
:locations-physical="entityObject!.locationsPhysical || {}"
:locations-virtual="entityObject!.locationsVirtual || {}"
@add-location-physical="addLocationPhysical"
@remove-location-physical="removeLocationPhysical"
@add-location-virtual="addLocationVirtual"
@remove-location-virtual="removeLocationVirtual"
/>
<EventEditorParticipants
:mode="mode"
:organizer="entityObject!.organizer"
:participants="entityObject!.participants || {}"
@update:organizer="entityObject!.organizer = $event"
@add-participant="addParticipant"
@remove-participant="removeParticipant"
/>
<EventEditorNotifications
:mode="mode"
:notifications="entityObject!.notifications || {}"
@add-notification="addNotification"
@remove-notification="removeNotification"
/>
<EventEditorTags
:mode="mode"
:tags="entityObject!.tags"
@update:tags="entityObject!.tags = $event"
/>
<!-- Metadata for view mode -->
<div v-if="mode === 'view' && entityObject!.created" class="event-editor-section">
<v-divider class="mb-4"></v-divider>
<div class="text-caption text-medium-emphasis">
<div>Created: {{ new Date(entityObject!.created).toLocaleString() }}</div>
<div v-if="entityObject!.modified">Modified: {{ new Date(entityObject!.modified).toLocaleString() }}</div>
<div v-if="isExternalEvent" class="mt-2">
<v-chip size="small" color="info" variant="tonal">
<v-icon start size="small">mdi-account-arrow-right</v-icon>
External Event
</v-chip>
</div>
</div>
</div>
</div>
<v-divider/>
</div>
<div class="event-editor-footer d-flex text-end justify-space-between pa-4">
<v-btn v-if="mode === 'edit'"
variant="text"
@click="cancelEdit"
:disabled="saving">
Cancel
</v-btn>
<v-btn v-if="mode === 'edit'"
color="primary"
variant="elevated"
@click="saveEntity"
:disabled="!selectedCalendar"
:loading="saving">
Save
</v-btn>
</div>
</v-sheet>
</template>
<style scoped>
.event-editor-section {
margin-bottom: 1.5rem;
}
</style>