-
Created: {{ new Date(entity.properties.created).toLocaleString() }}
-
Modified: {{ new Date(entity.properties.modified).toLocaleString() }}
-
- Completed: {{ new Date(entity.properties.completedOn).toLocaleString() }}
+
Created: {{ new Date(taskProperties.created).toLocaleString() }}
+
Modified: {{ new Date(taskProperties.modified).toLocaleString() }}
+
+ Completed: {{ new Date(taskProperties.completedOn).toLocaleString() }}
@@ -129,7 +129,7 @@
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(cloneEntity(props.entity));
+const selectedCollectionId = ref('');
+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);
}
}
diff --git a/src/composables/useChronoEntityActions.ts b/src/composables/useChronoEntityActions.ts
new file mode 100644
index 0000000..c634a6b
--- /dev/null
+++ b/src/composables/useChronoEntityActions.ts
@@ -0,0 +1,96 @@
+import { CollectionObject } from '@ChronoManager/models/collection'
+import { EntityObject } from '@ChronoManager/models/entity'
+import { EventObject } from '@ChronoManager/models/event'
+import { TaskObject } from '@ChronoManager/models/task'
+import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore'
+import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore'
+
+type ChronoEntityProperties = EventObject | TaskObject
+
+export function useChronoEntityActions(
+ entitiesStore: ReturnType,
+ collectionsStore: ReturnType,
+) {
+ function getEntityData(entity: EntityObject): Record {
+ const properties = entity.properties as ChronoEntityProperties
+ const now = new Date().toISOString()
+
+ properties.modified = now as never
+ if (!entity.identifier) {
+ properties.created = now as never
+ }
+
+ return properties.toJson() as unknown as Record
+ }
+
+ function resolveCollection(entity: EntityObject, collection?: CollectionObject | null): CollectionObject {
+ if (collection instanceof CollectionObject) {
+ return collection
+ }
+
+ const foundCollection = collectionsStore.collections.find((item) => item.identifier === entity.collection)
+ if (!foundCollection) {
+ throw new Error('Collection not found for entity operation')
+ }
+
+ return foundCollection
+ }
+
+ async function saveEntity(entity: EntityObject, collection: CollectionObject): Promise {
+ const data = getEntityData(entity)
+
+ if (!entity.identifier) {
+ return entitiesStore.create(
+ collection.provider,
+ collection.service,
+ collection.identifier,
+ data,
+ )
+ }
+
+ return entitiesStore.update(
+ collection.provider,
+ collection.service,
+ collection.identifier,
+ entity.identifier,
+ data,
+ )
+ }
+
+ async function deleteEntity(entity: EntityObject, collection?: CollectionObject | null): Promise {
+ const resolvedCollection = resolveCollection(entity, collection)
+
+ await entitiesStore.delete(
+ resolvedCollection.provider,
+ resolvedCollection.service,
+ resolvedCollection.identifier,
+ entity.identifier,
+ )
+ }
+
+ async function toggleTaskCompletion(entity: EntityObject, collection?: CollectionObject | null): Promise {
+ const resolvedCollection = resolveCollection(entity, collection)
+ const taskData = entity.properties as TaskObject
+ const isCompleted = taskData.status === 'completed'
+
+ taskData.status = isCompleted ? 'needs-action' : 'completed'
+ taskData.completedOn = isCompleted ? null : new Date().toISOString() as never
+ taskData.progress = isCompleted ? null : 100
+ taskData.modified = new Date().toISOString() as never
+
+ return entitiesStore.update(
+ resolvedCollection.provider,
+ resolvedCollection.service,
+ resolvedCollection.identifier,
+ entity.identifier,
+ taskData.toJson(),
+ )
+ }
+
+ return {
+ saveEntity,
+ deleteEntity,
+ toggleTaskCompletion,
+ resolveCollection,
+ }
+}
diff --git a/src/pages/ChronoPage.vue b/src/pages/ChronoPage.vue
index 15848f2..57213b7 100644
--- a/src/pages/ChronoPage.vue
+++ b/src/pages/ChronoPage.vue
@@ -1,17 +1,11 @@
@@ -467,33 +120,16 @@ onMounted(async () => {
-
-
-
- mdi-calendar
- Calendar
-
-
- mdi-checkbox-marked-outline
- Tasks
-
-
-
Days
Month
@@ -514,18 +150,20 @@ onMounted(async () => {
{
color="primary"
block
class="mt-3"
- @click="createCalendar"
+ @click="openCreateCalendar"
>
mdi-plus
New Calendar
@@ -545,7 +183,7 @@ onMounted(async () => {
color="primary"
block
class="mt-3"
- @click="createTaskList"
+ @click="openCreateTaskList"
>
mdi-plus
New Task List
@@ -591,8 +229,12 @@ onMounted(async () => {
:current-date="currentDate"
:events="filteredEvents"
:calendars="calendars"
+ :initial-days-span="daysViewSpan"
+ :initial-agenda-view-span="agendaViewSpan"
@event-click="editEvent"
- @date-click="handleDateClick"
+ @date-click="createEventFromDate"
+ @update:days-span="setDaysViewSpan"
+ @update:agenda-span="setAgendaViewSpan"
/>
{
:calendars="calendars"
@save="saveEvent"
@delete="deleteEvent"
- @edit="handleEditorEdit"
- @cancel="handleEditorCancel"
- @close="handleEditorClose"
+ @edit="startEditingSelectedEntity"
+ @cancel="cancelEditingSelectedEntity"
+ @close="closeEntityEditor"
/>
@@ -631,9 +273,9 @@ onMounted(async () => {
:lists="taskLists"
@save="saveTask"
@delete="deleteTask"
- @edit="handleEditorEdit"
- @cancel="handleEditorCancel"
- @close="handleEditorClose"
+ @edit="startEditingSelectedEntity"
+ @cancel="cancelEditingSelectedEntity"
+ @close="closeEntityEditor"
/>
diff --git a/src/stores/chronoStore.ts b/src/stores/chronoStore.ts
new file mode 100644
index 0000000..7802c7c
--- /dev/null
+++ b/src/stores/chronoStore.ts
@@ -0,0 +1,424 @@
+import { computed, ref, shallowRef } from 'vue'
+import { defineStore } from 'pinia'
+import { useUserStore } from '@KTXC/stores/userStore'
+import { CollectionObject } from '@ChronoManager/models/collection'
+import { EntityObject } from '@ChronoManager/models/entity'
+import { EventObject } from '@ChronoManager/models/event'
+import { TaskObject } from '@ChronoManager/models/task'
+import { ServiceObject } from '@ChronoManager/models/service'
+import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore'
+import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore'
+import type { SourceSelector } from '@ChronoManager/types'
+import type { CalendarView as CalendarViewType } from '@/types'
+import {
+ AGENDA_VIEW_SPANS,
+ DAYS_VIEW_SPANS,
+ type AgendaViewSpan,
+ type DaysViewSpan,
+} from '@/types/spans'
+import { useChronoEntityActions } from '@/composables/useChronoEntityActions'
+
+type EntitySources = Record>>
+
+export const useChronoStore = defineStore('chronoStore', () => {
+ const userStore = useUserStore()
+ const collectionsStore = useCollectionsStore()
+ const entitiesStore = useEntitiesStore()
+ const entityActions = useChronoEntityActions(entitiesStore, collectionsStore)
+
+ const savedCalendarView = userStore.getSetting('chrono.calendarView')
+ const savedDaysViewSpan = userStore.getSetting('chrono.daysViewSpan')
+ const savedAgendaViewSpan = userStore.getSetting('chrono.agendaViewSpan')
+
+ const viewMode = ref<'calendar' | 'tasks'>('calendar')
+ const calendarView = ref(
+ savedCalendarView === 'month' || savedCalendarView === 'agenda' || savedCalendarView === 'days'
+ ? savedCalendarView
+ : 'days',
+ )
+ const daysViewSpan = ref(
+ typeof savedDaysViewSpan === 'string' && DAYS_VIEW_SPANS.includes(savedDaysViewSpan as DaysViewSpan)
+ ? (savedDaysViewSpan as DaysViewSpan)
+ : '7d',
+ )
+
+ const agendaViewSpan = ref(
+ typeof savedAgendaViewSpan === 'string' && AGENDA_VIEW_SPANS.includes(savedAgendaViewSpan as AgendaViewSpan)
+ ? (savedAgendaViewSpan as AgendaViewSpan)
+ : '1w',
+ )
+ const currentDate = ref(new Date())
+ const sidebarVisible = ref(true)
+
+ const selectedCollection = shallowRef(null)
+ const selectedEntity = shallowRef(null)
+
+ const showEventEditor = ref(false)
+ const showTaskEditor = ref(false)
+ const showCollectionEditor = ref(false)
+
+ const entityEditorMode = ref<'edit' | 'view'>('view')
+ const editingCollection = shallowRef(null)
+ const collectionEditorMode = ref<'create' | 'edit'>('create')
+ const collectionEditorType = ref<'calendar' | 'tasklist'>('calendar')
+
+ const loading = ref(false)
+
+ const collections = computed(() => collectionsStore.collections)
+ const entities = computed(() => entitiesStore.entities)
+
+ const calendars = computed(() => {
+ return collections.value.filter(col => col.properties.contents?.event)
+ })
+
+ const taskLists = computed(() => {
+ return collections.value.filter(col => col.properties.contents?.task)
+ })
+
+ const events = computed(() => {
+ return entities.value.filter(entity => entity.properties instanceof EventObject)
+ })
+
+ const tasks = computed(() => {
+ return entities.value.filter(entity => entity.properties instanceof TaskObject)
+ })
+
+ const isTaskView = computed(() => viewMode.value === 'tasks')
+
+ const filteredEvents = computed(() => {
+ const visibleCalendarIds = calendars.value
+ .filter(cal => cal.properties.visibility !== false)
+ .map(cal => cal.identifier)
+
+ return events.value.filter(event => visibleCalendarIds.includes(event.collection))
+ })
+
+ const filteredTasks = computed(() => tasks.value)
+
+ function buildSources(collectionItems: CollectionObject[]): EntitySources {
+ return collectionItems.reduce((acc, collection) => {
+ if (!acc[collection.provider]) {
+ acc[collection.provider] = {}
+ }
+
+ const serviceKey = String(collection.service)
+ if (!acc[collection.provider][serviceKey]) {
+ acc[collection.provider][serviceKey] = {}
+ }
+
+ acc[collection.provider][serviceKey][collection.identifier] = true
+ return acc
+ }, {})
+ }
+
+ function setViewMode(mode: 'calendar' | 'tasks') {
+ viewMode.value = mode
+ }
+
+ function setCalendarView(view: CalendarViewType) {
+ calendarView.value = view
+ userStore.setSetting('chrono.calendarView', view)
+ }
+
+ function setDaysViewSpan(span: DaysViewSpan) {
+ if (!DAYS_VIEW_SPANS.includes(span)) {
+ return
+ }
+
+ daysViewSpan.value = span
+ userStore.setSetting('chrono.daysViewSpan', span)
+ }
+
+ function setAgendaViewSpan(span: AgendaViewSpan) {
+ agendaViewSpan.value = span
+ userStore.setSetting('chrono.agendaViewSpan', span)
+ }
+
+ function selectCalendar(calendar: CollectionObject) {
+ selectedCollection.value = calendar
+ }
+
+ function selectTaskList(list: CollectionObject) {
+ selectedCollection.value = list
+ }
+
+ function openCreateCalendar() {
+ editingCollection.value = new CollectionObject()
+ editingCollection.value.properties.fromJson({
+ ...editingCollection.value.properties.toJson(),
+ contents: { event: true },
+ })
+ collectionEditorMode.value = 'create'
+ collectionEditorType.value = 'calendar'
+ showCollectionEditor.value = true
+ }
+
+ function openCreateTaskList() {
+ editingCollection.value = new CollectionObject()
+ editingCollection.value.properties.fromJson({
+ ...editingCollection.value.properties.toJson(),
+ contents: { task: true },
+ })
+ collectionEditorMode.value = 'create'
+ collectionEditorType.value = 'tasklist'
+ showCollectionEditor.value = true
+ }
+
+ function openEditCalendar(collection: CollectionObject) {
+ editingCollection.value = collection
+ collectionEditorMode.value = 'edit'
+ collectionEditorType.value = 'calendar'
+ showCollectionEditor.value = true
+ }
+
+ function openEditTaskList(collection: CollectionObject) {
+ editingCollection.value = collection
+ collectionEditorMode.value = 'edit'
+ collectionEditorType.value = 'tasklist'
+ showCollectionEditor.value = true
+ }
+
+ function ensureSelectedCollection(targetType: 'calendar' | 'tasklist'): CollectionObject | null {
+ const list = targetType === 'calendar' ? calendars.value : taskLists.value
+
+ if (!selectedCollection.value && list.length > 0) {
+ selectedCollection.value = list[0]
+ }
+
+ return selectedCollection.value
+ }
+
+ function createEvent() {
+ const collection = ensureSelectedCollection('calendar')
+ if (!collection) {
+ return
+ }
+
+ const entity = new EntityObject()
+ entity.properties = new EventObject()
+ selectedEntity.value = entity
+ entityEditorMode.value = 'edit'
+ showEventEditor.value = true
+ }
+
+ function editEvent(entity: EntityObject) {
+ selectedEntity.value = entity
+ entityEditorMode.value = 'view'
+ showEventEditor.value = true
+ }
+
+ function createTask() {
+ const collection = ensureSelectedCollection('tasklist')
+ if (!collection) {
+ return
+ }
+
+ const entity = new EntityObject()
+ entity.properties = new TaskObject()
+ selectedEntity.value = entity
+ entityEditorMode.value = 'edit'
+ showTaskEditor.value = true
+ }
+
+ function editTask(entity: EntityObject) {
+ selectedEntity.value = entity
+ entityEditorMode.value = 'view'
+ showTaskEditor.value = true
+ }
+
+ function createEventFromDate(date: Date) {
+ const collection = ensureSelectedCollection('calendar')
+ if (!collection) {
+ return
+ }
+
+ const entity = new EntityObject()
+ entity.properties = new EventObject()
+ const eventData = entity.properties as EventObject
+ eventData.startsOn = date.toISOString()
+ eventData.endsOn = new Date(date.getTime() + 60 * 60 * 1000).toISOString()
+
+ selectedEntity.value = entity
+ entityEditorMode.value = 'edit'
+ showEventEditor.value = true
+ }
+
+ function startEditingSelectedEntity() {
+ entityEditorMode.value = 'edit'
+ }
+
+ function cancelEditingSelectedEntity() {
+ entityEditorMode.value = 'view'
+ }
+
+ function closeEntityEditor() {
+ selectedEntity.value = null
+ entityEditorMode.value = 'view'
+ showEventEditor.value = false
+ showTaskEditor.value = false
+ }
+
+ async function toggleCalendarVisibility(collection: CollectionObject) {
+ const nextVisibility = collection.properties.visibility === false
+ const updated = collection.clone()
+ updated.properties.visibility = nextVisibility
+
+ await collectionsStore.update(
+ updated.provider,
+ updated.service,
+ updated.identifier,
+ updated.properties,
+ )
+ }
+
+ async function saveEvent(entity: EntityObject, collection?: CollectionObject | null) {
+ const targetCollection = collection instanceof CollectionObject
+ ? collection
+ : selectedCollection.value
+
+ if (!targetCollection) {
+ throw new Error('No calendar collection selected')
+ }
+
+ selectedEntity.value = await entityActions.saveEntity(entity, targetCollection)
+ selectedCollection.value = targetCollection
+ entityEditorMode.value = 'view'
+ }
+
+ async function saveTask(entity: EntityObject, collection?: CollectionObject | null) {
+ const targetCollection = collection instanceof CollectionObject
+ ? collection
+ : selectedCollection.value
+
+ if (!targetCollection) {
+ throw new Error('No task list selected')
+ }
+
+ selectedEntity.value = await entityActions.saveEntity(entity, targetCollection)
+ selectedCollection.value = targetCollection
+ entityEditorMode.value = 'view'
+ }
+
+ async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) {
+ await entityActions.deleteEntity(entity, collection)
+ closeEntityEditor()
+ }
+
+ async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) {
+ await entityActions.deleteEntity(entity, collection)
+ closeEntityEditor()
+ }
+
+ async function toggleTaskComplete(taskId: string | number) {
+ const taskEntity = tasks.value.find(task => task.identifier === taskId)
+ if (!taskEntity) {
+ return
+ }
+
+ await entityActions.toggleTaskCompletion(taskEntity)
+ }
+
+ async function saveCollection(collection: CollectionObject, service: ServiceObject) {
+ if (collectionEditorMode.value === 'create') {
+ await collectionsStore.create(
+ service.provider,
+ service.identifier || '',
+ null,
+ collection.properties,
+ )
+ } else {
+ await collectionsStore.update(
+ collection.provider,
+ collection.service,
+ collection.identifier,
+ collection.properties,
+ )
+ }
+ }
+
+ async function deleteCollection(collection: CollectionObject) {
+ await collectionsStore.delete(collection.provider, collection.service, collection.identifier)
+
+ if (selectedCollection.value?.identifier === collection.identifier) {
+ selectedCollection.value = null
+ }
+ }
+
+ async function initialize() {
+ loading.value = true
+ try {
+ await collectionsStore.list()
+
+ const sources = buildSources(collections.value)
+ if (Object.keys(sources).length > 0) {
+ await entitiesStore.list(sources as SourceSelector)
+ }
+
+ if (
+ selectedCollection.value
+ && !collections.value.find(collection => collection.identifier === selectedCollection.value?.identifier)
+ ) {
+ selectedCollection.value = null
+ }
+ } finally {
+ loading.value = false
+ }
+ }
+
+ return {
+ viewMode,
+ calendarView,
+ daysViewSpan,
+ agendaViewSpan,
+ currentDate,
+ sidebarVisible,
+ selectedCollection,
+ selectedEntity,
+ showEventEditor,
+ showTaskEditor,
+ showCollectionEditor,
+ entityEditorMode,
+ editingCollection,
+ collectionEditorMode,
+ collectionEditorType,
+ loading,
+
+ collections,
+ entities,
+ calendars,
+ taskLists,
+ events,
+ tasks,
+ isTaskView,
+ filteredEvents,
+ filteredTasks,
+
+ setViewMode,
+ setCalendarView,
+ setDaysViewSpan,
+ setAgendaViewSpan,
+ selectCalendar,
+ selectTaskList,
+ openCreateCalendar,
+ openCreateTaskList,
+ openEditCalendar,
+ openEditTaskList,
+ createEvent,
+ editEvent,
+ createTask,
+ editTask,
+ createEventFromDate,
+ startEditingSelectedEntity,
+ cancelEditingSelectedEntity,
+ closeEntityEditor,
+ toggleCalendarVisibility,
+ toggleTaskComplete,
+ saveEvent,
+ deleteEvent,
+ saveTask,
+ deleteTask,
+ saveCollection,
+ deleteCollection,
+ initialize,
+ }
+})
diff --git a/src/types/spans.ts b/src/types/spans.ts
new file mode 100644
index 0000000..a5c5a9f
--- /dev/null
+++ b/src/types/spans.ts
@@ -0,0 +1,30 @@
+export const DAYS_VIEW_SPANS = ['1d', '3d', '5d', '7d', '9d', '11d', '14d'] as const
+export type DaysViewSpan = typeof DAYS_VIEW_SPANS[number]
+
+export const AGENDA_VIEW_SPANS = ['1d', '3d', '1w', '2w', '3w', '1m'] as const
+export type AgendaViewSpan = typeof AGENDA_VIEW_SPANS[number]
+
+export const SPAN_TO_DAYS = {
+ '1d': 1,
+ '3d': 3,
+ '5d': 5,
+ '7d': 7,
+ '9d': 9,
+ '11d': 11,
+ '14d': 14,
+ '1w': 7,
+ '2w': 14,
+ '3w': 21,
+ '1m': 30,
+} as const
+
+export type TimeSpanLabel = keyof typeof SPAN_TO_DAYS
+
+export function spanToDays(span: TimeSpanLabel): number {
+ return SPAN_TO_DAYS[span]
+}
+
+export function dayCountToDaysViewSpan(days: number): DaysViewSpan | null {
+ const label = `${days}d`
+ return DAYS_VIEW_SPANS.includes(label as DaysViewSpan) ? (label as DaysViewSpan) : null
+}