Initial commit
This commit is contained in:
@@ -0,0 +1,641 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useDisplay } from 'vuetify';
|
||||
import { useModuleStore } from '@KTXC/stores/moduleStore';
|
||||
import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore';
|
||||
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 { ServiceObject } from '@ChronoManager/models/service';
|
||||
import type { CalendarView as CalendarViewType } from '@/types';
|
||||
import CollectionList from '@/components/CollectionList.vue';
|
||||
import CollectionEditor from '@/components/CollectionEditor.vue';
|
||||
import CalendarView from '@/components/CalendarView.vue';
|
||||
import TaskView from '@/components/TaskView.vue';
|
||||
import EventEditor from '@/components/EventEditor.vue';
|
||||
import TaskEditor from '@/components/TaskEditor.vue';
|
||||
import MiniCalendar from '@/components/MiniCalendar.vue';
|
||||
|
||||
// Vuetify display
|
||||
const display = useDisplay();
|
||||
|
||||
// Check if chrono manager is available
|
||||
const moduleStore = useModuleStore();
|
||||
const isChronoManagerAvailable = computed(() => {
|
||||
return moduleStore.has('chrono_manager') || moduleStore.has('ChronoManager')
|
||||
});
|
||||
|
||||
// Stores
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const entitiesStore = useEntitiesStore();
|
||||
const servicesStore = useServicesStore();
|
||||
|
||||
// View state
|
||||
const viewMode = ref<'calendar' | 'tasks'>('calendar');
|
||||
const calendarView = ref<CalendarViewType>('days');
|
||||
const currentDate = ref(new Date());
|
||||
const sidebarVisible = ref(true);
|
||||
|
||||
// Data - using manager objects directly
|
||||
const collections = ref<CollectionObject[]>([]);
|
||||
const entities = ref<EntityObject[]>([]);
|
||||
const selectedCollection = ref<CollectionObject | null>(null);
|
||||
|
||||
// Computed - filter collections and entities
|
||||
const calendars = computed(() => {
|
||||
return collections.value.filter(col => col.contents?.event);
|
||||
});
|
||||
|
||||
const taskLists = computed(() => {
|
||||
return collections.value.filter(col => col.contents?.task);
|
||||
});
|
||||
|
||||
const events = computed(() => {
|
||||
return entities.value.filter(entity => entity.data && (entity.data as any).type === 'event');
|
||||
});
|
||||
|
||||
const tasks = computed(() => {
|
||||
return entities.value.filter(entity => entity.data && (entity.data as any).type === 'task');
|
||||
});
|
||||
|
||||
// Dialog state
|
||||
const showEventEditor = ref(false);
|
||||
const showTaskEditor = ref(false);
|
||||
const showCollectionEditor = ref(false);
|
||||
const selectedEntity = ref<EntityObject | null>(null);
|
||||
const entityEditorMode = ref<'edit' | 'view'>('view');
|
||||
const editingCollection = ref<CollectionObject | null>(null);
|
||||
const collectionEditorMode = ref<'create' | 'edit'>('create');
|
||||
const collectionEditorType = ref<'calendar' | 'tasklist'>('calendar');
|
||||
|
||||
// Computed
|
||||
const isTaskView = computed(() => viewMode.value === 'tasks');
|
||||
|
||||
const filteredEvents = computed(() => {
|
||||
const visibleCalendarIds = calendars.value
|
||||
.filter(cal => cal.enabled !== false)
|
||||
.map(cal => cal.id);
|
||||
|
||||
return events.value.filter(event =>
|
||||
visibleCalendarIds.includes(event.in)
|
||||
);
|
||||
});
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
return tasks.value;
|
||||
});
|
||||
|
||||
// Methods
|
||||
function selectCalendar(calendar: CollectionObject) {
|
||||
selectedCollection.value = calendar;
|
||||
console.log('[Chrono] - Selected calendar:', calendar);
|
||||
}
|
||||
|
||||
function createCalendar() {
|
||||
editingCollection.value = collectionsStore.fresh();
|
||||
editingCollection.value.contents = { event: true };
|
||||
collectionEditorMode.value = 'create';
|
||||
collectionEditorType.value = 'calendar';
|
||||
showCollectionEditor.value = true;
|
||||
}
|
||||
|
||||
function editCalendar(collection: CollectionObject) {
|
||||
editingCollection.value = collection;
|
||||
collectionEditorMode.value = 'edit';
|
||||
collectionEditorType.value = 'calendar';
|
||||
showCollectionEditor.value = true;
|
||||
}
|
||||
|
||||
async function toggleCalendarVisibility(collection: CollectionObject) {
|
||||
try {
|
||||
await collectionsStore.modify(collection);
|
||||
console.log('[Chrono] - Toggled calendar visibility:', collection);
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to toggle calendar visibility:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function selectTaskList(list: CollectionObject) {
|
||||
selectedCollection.value = list;
|
||||
console.log('[Chrono] - Selected task list:', list);
|
||||
}
|
||||
|
||||
function createTaskList() {
|
||||
editingCollection.value = collectionsStore.fresh();
|
||||
editingCollection.value.contents = { task: true };
|
||||
collectionEditorMode.value = 'create';
|
||||
collectionEditorType.value = 'tasklist';
|
||||
showCollectionEditor.value = true;
|
||||
}
|
||||
|
||||
function editTaskList(collection: CollectionObject) {
|
||||
editingCollection.value = collection;
|
||||
collectionEditorMode.value = 'edit';
|
||||
collectionEditorType.value = 'tasklist';
|
||||
showCollectionEditor.value = true;
|
||||
}
|
||||
|
||||
function createEvent() {
|
||||
// Select first calendar collection or use selected
|
||||
if (!selectedCollection.value && calendars.value.length > 0) {
|
||||
selectedCollection.value = calendars.value[0];
|
||||
}
|
||||
|
||||
if (!selectedCollection.value) {
|
||||
console.warn('[Chrono] - No calendar collection available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create fresh event entity
|
||||
selectedEntity.value = entitiesStore.fresh('event');
|
||||
selectedEntity.value.in = selectedCollection.value.id;
|
||||
entityEditorMode.value = 'edit';
|
||||
showEventEditor.value = true;
|
||||
}
|
||||
|
||||
function editEvent(entity: EntityObject) {
|
||||
selectedEntity.value = entity;
|
||||
entityEditorMode.value = 'view';
|
||||
showEventEditor.value = true;
|
||||
}
|
||||
|
||||
async function saveEvent(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = selectedCollection.value;
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection selected');
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity.data) {
|
||||
entity.data.modified = new Date();
|
||||
}
|
||||
|
||||
if (entity.id === null) {
|
||||
entity.data.created = new Date();
|
||||
selectedEntity.value = await entitiesStore.create(collection, entity);
|
||||
} else {
|
||||
selectedEntity.value = await entitiesStore.modify(collection, entity);
|
||||
}
|
||||
|
||||
entityEditorMode.value = 'view';
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to save event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = collections.value.find(c => c.id === entity.in);
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection found');
|
||||
return;
|
||||
}
|
||||
|
||||
await entitiesStore.destroy(collection, entity);
|
||||
selectedEntity.value = null;
|
||||
entityEditorMode.value = 'view';
|
||||
showEventEditor.value = false;
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to delete event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDateClick(date: Date) {
|
||||
// Select first calendar collection or use selected
|
||||
if (!selectedCollection.value && calendars.value.length > 0) {
|
||||
selectedCollection.value = calendars.value[0];
|
||||
}
|
||||
|
||||
if (!selectedCollection.value) {
|
||||
console.warn('[Chrono] - No calendar collection available');
|
||||
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);
|
||||
}
|
||||
entityEditorMode.value = 'edit';
|
||||
showEventEditor.value = true;
|
||||
}
|
||||
|
||||
function handleEditorEdit() {
|
||||
console.log('[Chrono] - Editor editing started');
|
||||
entityEditorMode.value = 'edit';
|
||||
}
|
||||
|
||||
function handleEditorCancel() {
|
||||
console.log('[Chrono] - Editor editing cancelled');
|
||||
entityEditorMode.value = 'view';
|
||||
}
|
||||
|
||||
function handleEditorClose() {
|
||||
console.log('[Chrono] - Editor closed');
|
||||
selectedEntity.value = null;
|
||||
entityEditorMode.value = 'view';
|
||||
showEventEditor.value = false;
|
||||
showTaskEditor.value = false;
|
||||
}
|
||||
|
||||
function createTask() {
|
||||
// Select first task list collection or use selected
|
||||
if (!selectedCollection.value && taskLists.value.length > 0) {
|
||||
selectedCollection.value = taskLists.value[0];
|
||||
}
|
||||
|
||||
if (!selectedCollection.value) {
|
||||
console.warn('[Chrono] - No task list available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create fresh task entity
|
||||
selectedEntity.value = entitiesStore.fresh('task');
|
||||
selectedEntity.value.in = selectedCollection.value.id;
|
||||
entityEditorMode.value = 'edit';
|
||||
showTaskEditor.value = true;
|
||||
}
|
||||
|
||||
function editTask(entity: EntityObject) {
|
||||
selectedEntity.value = entity;
|
||||
entityEditorMode.value = 'view';
|
||||
showTaskEditor.value = true;
|
||||
}
|
||||
|
||||
async function saveTask(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = selectedCollection.value;
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection selected');
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity.data) {
|
||||
entity.data.modified = new Date();
|
||||
}
|
||||
|
||||
if (entity.id === null) {
|
||||
entity.data.created = new Date();
|
||||
selectedEntity.value = await entitiesStore.create(collection, entity);
|
||||
} else {
|
||||
selectedEntity.value = await entitiesStore.modify(collection, entity);
|
||||
}
|
||||
|
||||
entityEditorMode.value = 'view';
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to save task:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) {
|
||||
try {
|
||||
if (!(collection instanceof CollectionObject)) {
|
||||
collection = collections.value.find(c => c.id === entity.in);
|
||||
}
|
||||
if (!collection) {
|
||||
console.error('[Chrono] - No collection found');
|
||||
return;
|
||||
}
|
||||
|
||||
await entitiesStore.destroy(collection, entity);
|
||||
selectedEntity.value = null;
|
||||
entityEditorMode.value = 'view';
|
||||
showTaskEditor.value = false;
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to delete task:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTaskComplete(taskId: string | number) {
|
||||
try {
|
||||
const entity = entities.value.find(e => e.id === taskId);
|
||||
if (!entity || !entity.data) return;
|
||||
|
||||
const collection = collections.value.find(c => c.id === entity.in);
|
||||
if (!collection) return;
|
||||
|
||||
const taskData = entity.data as any;
|
||||
const isCompleted = taskData.status === 'completed';
|
||||
|
||||
taskData.status = isCompleted ? 'needs-action' : 'completed';
|
||||
taskData.completedOn = isCompleted ? null : new Date();
|
||||
taskData.progress = isCompleted ? null : 100;
|
||||
taskData.modified = new Date();
|
||||
|
||||
await entitiesStore.modify(collection, entity);
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to toggle task completion:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCollection(collection: CollectionObject, service: ServiceObject) {
|
||||
try {
|
||||
if (collectionEditorMode.value === 'create') {
|
||||
await collectionsStore.create(service, collection);
|
||||
console.log('[Chrono] - Created collection:', collection);
|
||||
} else {
|
||||
await collectionsStore.modify(collection);
|
||||
console.log('[Chrono] - Modified collection:', collection);
|
||||
}
|
||||
// Reload collections
|
||||
collections.value = await collectionsStore.list();
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to save collection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCollection(collection: CollectionObject) {
|
||||
try {
|
||||
await collectionsStore.destroy(collection);
|
||||
console.log('[Chrono] - Deleted collection:', collection);
|
||||
// Reload collections
|
||||
collections.value = await collectionsStore.list();
|
||||
if (selectedCollection.value?.id === collection.id) {
|
||||
selectedCollection.value = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to delete collection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize data from stores
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load collections (calendars and task lists)
|
||||
collections.value = await collectionsStore.list();
|
||||
|
||||
// Load entities (events and tasks)
|
||||
entities.value = await entitiesStore.list(null, null, null);
|
||||
|
||||
console.log('[Chrono] - Loaded data from ChronoManager:', {
|
||||
collections: collections.value.length,
|
||||
calendars: calendars.value.length,
|
||||
events: events.value.length,
|
||||
tasks: tasks.value.length,
|
||||
taskLists: taskLists.value.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Chrono] - Failed to load data from ChronoManager:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chrono-container">
|
||||
<!-- Top Navigation Bar -->
|
||||
<v-app-bar elevation="0" class="chrono-toolbar border-b">
|
||||
<template #prepend>
|
||||
<v-btn
|
||||
icon="mdi-menu"
|
||||
variant="text"
|
||||
@click="sidebarVisible = !sidebarVisible"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<v-app-bar-title class="d-flex align-center">
|
||||
<v-icon size="28" color="primary" class="mr-2">mdi-calendar-month</v-icon>
|
||||
<span class="text-h6 font-weight-bold">Chrono</span>
|
||||
</v-app-bar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!-- View Toggle -->
|
||||
<v-btn-toggle
|
||||
v-model="viewMode"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="mr-2"
|
||||
>
|
||||
<v-btn value="calendar" size="small">
|
||||
<v-icon>mdi-calendar</v-icon>
|
||||
<span class="ml-1 d-none d-sm-inline">Calendar</span>
|
||||
</v-btn>
|
||||
<v-btn value="tasks" size="small">
|
||||
<v-icon>mdi-checkbox-marked-outline</v-icon>
|
||||
<span class="ml-1 d-none d-sm-inline">Tasks</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<!-- View Switcher for Calendar -->
|
||||
<v-btn-toggle
|
||||
v-if="!isTaskView"
|
||||
v-model="calendarView"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
mandatory
|
||||
class="mr-2"
|
||||
>
|
||||
<v-btn value="days" size="small">Days</v-btn>
|
||||
<v-btn value="month" size="small">Month</v-btn>
|
||||
<v-btn value="agenda" size="small">Agenda</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="chrono-content">
|
||||
<!-- Sidebar Navigation Drawer -->
|
||||
<v-navigation-drawer
|
||||
v-model="sidebarVisible"
|
||||
:permanent="display.mdAndUp.value"
|
||||
:temporary="display.smAndDown.value"
|
||||
width="280"
|
||||
class="chrono-sidebar"
|
||||
>
|
||||
<div class="pa-4">
|
||||
<CollectionList
|
||||
v-if="!isTaskView"
|
||||
:selected-collection="selectedCollection"
|
||||
type="calendar"
|
||||
@select="selectCalendar"
|
||||
@edit="editCalendar"
|
||||
@toggle-visibility="toggleCalendarVisibility"
|
||||
/>
|
||||
<CollectionList
|
||||
v-else
|
||||
:selected-collection="selectedCollection"
|
||||
type="tasklist"
|
||||
@select="selectTaskList"
|
||||
@edit="editTaskList"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
v-if="!isTaskView"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
block
|
||||
class="mt-3"
|
||||
@click="createCalendar"
|
||||
>
|
||||
<v-icon start>mdi-plus</v-icon>
|
||||
New Calendar
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
block
|
||||
class="mt-3"
|
||||
@click="createTaskList"
|
||||
>
|
||||
<v-icon start>mdi-plus</v-icon>
|
||||
New Task List
|
||||
</v-btn>
|
||||
|
||||
<!-- Mini Calendar Widget -->
|
||||
<v-card v-if="!isTaskView" class="mt-4" variant="outlined">
|
||||
<v-card-text class="pa-2">
|
||||
<MiniCalendar v-model="currentDate" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- Main Calendar/Task View -->
|
||||
<div class="chrono-main">
|
||||
<v-alert
|
||||
v-if="!isChronoManagerAvailable"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
closable
|
||||
class="mb-4"
|
||||
>
|
||||
<v-alert-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-alert-circle" class="mr-2" />
|
||||
Chrono Manager Not Available
|
||||
</v-alert-title>
|
||||
<div class="mt-2">
|
||||
<p>
|
||||
The Chrono Manager module is not installed or enabled.
|
||||
This module requires the <strong>chrono_manager</strong> module to function properly.
|
||||
</p>
|
||||
<p class="mt-2 mb-0">
|
||||
Please contact your system administrator to install and enable the
|
||||
<code>chrono_manager</code> module.
|
||||
</p>
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<CalendarView
|
||||
v-if="!isTaskView"
|
||||
:view="calendarView"
|
||||
:current-date="currentDate"
|
||||
:events="filteredEvents"
|
||||
:calendars="calendars"
|
||||
@event-click="editEvent"
|
||||
@date-click="handleDateClick"
|
||||
/>
|
||||
|
||||
<TaskView
|
||||
v-else
|
||||
:tasks="filteredTasks"
|
||||
:lists="taskLists"
|
||||
@task-click="editTask"
|
||||
@toggle-complete="toggleTaskComplete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Editor Dialog -->
|
||||
<v-dialog v-model="showEventEditor" max-width="800px" scrollable>
|
||||
<EventEditor
|
||||
v-if="showEventEditor"
|
||||
:mode="entityEditorMode"
|
||||
:entity="selectedEntity"
|
||||
:collection="selectedCollection"
|
||||
:calendars="calendars"
|
||||
@save="saveEvent"
|
||||
@delete="deleteEvent"
|
||||
@edit="handleEditorEdit"
|
||||
@cancel="handleEditorCancel"
|
||||
@close="handleEditorClose"
|
||||
/>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Task Editor Dialog -->
|
||||
<v-dialog v-model="showTaskEditor" max-width="700px" scrollable>
|
||||
<TaskEditor
|
||||
v-if="showTaskEditor"
|
||||
:mode="entityEditorMode"
|
||||
:entity="selectedEntity"
|
||||
:collection="selectedCollection"
|
||||
:lists="taskLists"
|
||||
@save="saveTask"
|
||||
@delete="deleteTask"
|
||||
@edit="handleEditorEdit"
|
||||
@cancel="handleEditorCancel"
|
||||
@close="handleEditorClose"
|
||||
/>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Collection Editor Dialog -->
|
||||
<CollectionEditor
|
||||
v-model="showCollectionEditor"
|
||||
:collection="editingCollection"
|
||||
:mode="collectionEditorMode"
|
||||
:type="collectionEditorType"
|
||||
@save="saveCollection"
|
||||
@delete="deleteCollection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chrono-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
isolation: isolate; /* Create stacking context to prevent style leakage */
|
||||
}
|
||||
|
||||
.chrono-toolbar {
|
||||
border-bottom: 1px solid rgb(var(--v-border-color)) !important;
|
||||
}
|
||||
|
||||
.chrono-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chrono-sidebar {
|
||||
border-right: 1px solid rgb(var(--v-border-color)) !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chrono-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.border-b {
|
||||
border-bottom: 1px solid rgb(var(--v-border-color));
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 960px) {
|
||||
.chrono-main {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.chrono-main {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user