Files
openStack/frontend/src/views/Calendar.vue
T
x3 4fe56265c9
ci / backend (push) Canceled after 0s
ci / frontend (push) Canceled after 0s
feat(frontend): chat tabs, flat SVG icons, upload autosave, per-user rights
- Chat rework: Chats (private DMs with user search, online dots, unread
  badges) and Team chats tabs; shared message component with reactions,
  edit, image attachments, message search and typing indicators
- Flat modern inline SVG icon set (no external dependency) replaces
  emoji chrome across nav, headers, buttons, tabs and settings
- Uploaded avatar/background images apply instantly (auto-save) with
  confirmation note; per-user permission matrix in dashboard members
- Login localized, invite header uses icon, comment header cleaned
2026-09-22 14:41:42 +02:00

108 lines
3.5 KiB
Vue

<script setup>
import { computed, onMounted, ref, watch } 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';
import Icon from '../components/Icon.vue';
const ws = useWorkspaceStore();
const L = useLocaleStore();
const router = useRouter();
const cursor = ref(new Date());
cursor.value.setDate(1);
const tasks = ref([]);
const year = computed(() => cursor.value.getFullYear());
const month = computed(() => cursor.value.getMonth());
const monthName = computed(() =>
cursor.value.toLocaleDateString(L.locale === 'zh' ? 'zh-CN' : L.locale === 'de' ? 'de-DE' : 'en-US', {
month: 'long', year: 'numeric',
}),
);
const cells = computed(() => {
const first = new Date(year.value, month.value, 1);
const startDay = (first.getDay() + 6) % 7; // Monday first
const days = new Date(year.value, month.value + 1, 0).getDate();
const arr = [];
for (let i = 0; i < startDay; i++) arr.push(null);
for (let d = 1; d <= days; d++) arr.push(new Date(year.value, month.value, d));
return arr;
});
function dayKey(d) {
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
}
const byDay = computed(() => {
const map = {};
for (const t of tasks.value) {
if (!t.due_at) continue;
const d = new Date(t.due_at);
const k = dayKey(d);
if (!map[k]) map[k] = [];
map[k].push(t);
}
return map;
});
function isToday(d) {
const n = new Date();
return d && d.toDateString() === n.toDateString();
}
async function load() {
if (!ws.currentId) return;
const from = new Date(year.value, month.value, 1).toISOString();
const to = new Date(year.value, month.value + 1, 0, 23, 59).toISOString();
const { data } = await api.get(`/tasks/by-workspace/${ws.currentId}`, { params: { from, to } });
tasks.value = data.tasks;
}
function prev() { cursor.value = new Date(year.value, month.value - 1, 1); }
function next() { cursor.value = new Date(year.value, month.value + 1, 1); }
function goToday() { const n = new Date(); cursor.value = new Date(n.getFullYear(), n.getMonth(), 1); }
watch(cursor, load);
onMounted(async () => {
if (!ws.workspaces.length) await ws.fetchWorkspaces();
await load();
});
</script>
<template>
<div class="layout">
<Sidebar />
<main class="main">
<div class="topbar">
<h2 style="margin: 0"><Icon name="calendar" :size="22" /> {{ L.t('cal.title') }} — {{ monthName }}</h2>
<div class="row">
<button class="ghost" @click="prev">◀</button>
<button class="ghost" @click="goToday">{{ L.t('cal.today') }}</button>
<button class="ghost" @click="next">▶</button>
</div>
</div>
<div class="calendar">
<div class="cal-head" v-for="d in ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']" :key="d">{{ d }}</div>
<div v-for="(d, i) in cells" :key="i" class="cal-cell" :class="{ today: isToday(d) }">
<template v-if="d">
<div class="cal-day">{{ d.getDate() }}</div>
<div
v-for="t in (byDay[dayKey(d)] || [])" :key="t.id"
class="cal-task" :class="{ done: t.status === 'done' }"
:title="t.title"
@click="router.push('/tasks')"
>
{{ t.status === 'done' ? '✅' : '▸' }} {{ t.title }}
</div>
</template>
</div>
</div>
<p class="muted">▸ {{ L.t('task.due') }} → {{ L.t('cal.title') }} ✨</p>
</main>
</div>
</template>