Files
openStack/frontend/src/views/Tasks.vue
T
x3 fb396d313a
ci / backend (push) Canceled after 0s
ci / frontend (push) Canceled after 0s
feat(frontend): settings window, i18n, admin UI, books, tasks, calendar, timer
- Settings window with tabs: profile (name, email, avatar, password),
  appearance (light/dark/system), language (de/en/zh), background
  (gradient presets, custom URL, blur toggle); persisted server-side
- i18n dictionaries (de/en/zh) applied across nav, sidebar, tasks,
  calendar, time tracking, admin, invites and comments
- Greeting header with time-of-day message, live clock and date; cooler
  theme, cards, kanban, calendar grid and data-table styling
- Shared sidebar: nav, collapsible books with per-book pages, top-level
  pages, search; user chip opens settings
- Tasks kanban with priorities, overdue flags and book/page links;
  calendar month view fed by task deadlines; time view with entries,
  durations and totals; top-right live timer with optional book link
- User discovery autocomplete in the invite card; editor gains tables
  and checklists; standalone theme buttons removed
2026-09-22 13:56:26 +02:00

129 lines
4.7 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import api from '../api';
import { useWorkspaceStore } from '../stores/workspace';
import { useLocaleStore } from '../stores/locale';
import Sidebar from '../components/Sidebar.vue';
const ws = useWorkspaceStore();
const L = useLocaleStore();
const router = useRouter();
const tasks = ref([]);
const title = ref('');
const due = ref('');
const priority = ref('normal');
const bookId = ref('');
const pageId = ref('');
const cols = computed(() => ({
todo: tasks.value.filter((t) => t.status === 'todo'),
doing: tasks.value.filter((t) => t.status === 'doing'),
done: tasks.value.filter((t) => t.status === 'done'),
}));
function isOverdue(t) {
return t.status !== 'done' && t.due_at && new Date(t.due_at) < new Date();
}
function fmtDate(d) {
return d ? new Date(d).toLocaleString() : L.t('task.noDue');
}
async function load() {
if (!ws.currentId) return;
const { data } = await api.get(`/tasks/by-workspace/${ws.currentId}`);
tasks.value = data.tasks;
}
async function create() {
if (!title.value.trim()) return;
const { data } = await api.post('/tasks', {
workspace_id: ws.currentId,
title: title.value.trim(),
priority: priority.value,
due_at: due.value || null,
book_id: bookId.value || null,
page_id: pageId.value || null,
});
tasks.value.unshift(data.task);
title.value = '';
due.value = '';
bookId.value = '';
pageId.value = '';
}
async function setStatus(t, status) {
const { data } = await api.patch(`/tasks/${t.id}`, { status });
Object.assign(t, data.task);
}
async function remove(t) {
if (!confirm('🗑️?')) return;
await api.delete(`/tasks/${t.id}`);
tasks.value = tasks.value.filter((x) => x.id !== t.id);
}
onMounted(async () => {
if (!ws.workspaces.length) await ws.fetchWorkspaces();
await ws.fetchBooks();
await ws.fetchPages();
await load();
});
</script>
<template>
<div class="layout">
<Sidebar />
<main class="main">
<h2>✅ {{ L.t('task.title') }}</h2>
<div class="card new-task">
<div class="row wrap">
<input v-model="title" :placeholder="L.t('task.ph')" @keyup.enter="create" style="flex: 2" />
<input v-model="due" type="datetime-local" :title="L.t('task.due')" />
<select v-model="priority" style="max-width: 130px">
<option value="low">🟢 {{ L.t('task.low') }}</option>
<option value="normal">🟡 {{ L.t('task.normal') }}</option>
<option value="high">🔴 {{ L.t('task.high') }}</option>
</select>
</div>
<div class="row wrap">
<select v-model="bookId">
<option value="">{{ L.t('task.book') }}: {{ L.t('task.none') }}</option>
<option v-for="b in ws.books" :key="b.id" :value="b.id">{{ b.icon }} {{ b.title }}</option>
</select>
<select v-model="pageId">
<option value="">{{ L.t('task.page') }}: {{ L.t('task.none') }}</option>
<option v-for="p in ws.pages" :key="p.id" :value="p.id">{{ p.title }}</option>
</select>
<button @click="create">+ {{ L.t('task.new') }}</button>
</div>
</div>
<div class="kanban">
<div v-for="col in ['todo', 'doing', 'done']" :key="col" class="kanban-col">
<h3>{{ L.t(`task.${col}`) }} ({{ cols[col].length }})</h3>
<div v-for="t in cols[col]" :key="t.id" class="card task-card" :class="{ overdue: isOverdue(t) }">
<strong>{{ t.title }}</strong>
<div class="muted">
{{ t.priority === 'high' ? '🔴' : t.priority === 'low' ? '🟢' : '🟡' }}
🕒 {{ fmtDate(t.due_at) }}
<span v-if="isOverdue(t)" class="overdue-tag">{{ L.t('task.overdue') }}</span>
</div>
<div class="muted">
<span v-if="t.book_title">📚 {{ t.book_title }}</span>
<span v-if="t.page_title" class="link" @click="router.push(`/page/${t.page_id}`)">📄 {{ t.page_title }}</span>
</div>
<div class="row wrap">
<button v-if="t.status !== 'todo'" class="ghost mini" @click="setStatus(t, 'todo')">{{ L.t('task.toTodo') }}</button>
<button v-if="t.status === 'todo'" class="ghost mini" @click="setStatus(t, 'doing')">{{ L.t('task.toDoing') }}</button>
<button v-if="t.status === 'doing'" class="ghost mini" @click="setStatus(t, 'done')">{{ L.t('task.toDone') }}</button>
<button v-if="t.status === 'done'" class="ghost mini" @click="setStatus(t, 'todo')">{{ L.t('task.reopen') }}</button>
<button class="ghost mini" @click="remove(t)">🗑️</button>
</div>
</div>
</div>
</div>
</main>
</div>
</template>