2026-08-11 23:17:04 +07:00
|
|
|
<template>
|
2026-08-24 08:07:23 +07:00
|
|
|
<div
|
|
|
|
|
class="flex h-dvh flex-col lg:grid"
|
|
|
|
|
:class="sidebarOpen ? 'lg:grid-cols-[236px_1fr]' : 'lg:grid-cols-1'"
|
|
|
|
|
>
|
2026-08-11 23:17:04 +07:00
|
|
|
<div
|
|
|
|
|
v-if="sidebarOpen"
|
2026-08-24 08:07:23 +07:00
|
|
|
class="fixed inset-0 z-30 bg-slate-950/60 backdrop-blur-sm lg:hidden"
|
2026-08-11 23:17:04 +07:00
|
|
|
@click="sidebarOpen = false"
|
|
|
|
|
></div>
|
2026-08-24 08:07:23 +07:00
|
|
|
|
2026-08-11 23:17:04 +07:00
|
|
|
<aside
|
2026-08-24 08:07:23 +07:00
|
|
|
class="fixed inset-y-0 left-0 z-40 flex w-[236px] transform flex-col border-r px-3 py-4 transition-transform duration-200 lg:static"
|
2026-08-11 23:17:04 +07:00
|
|
|
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:hidden'"
|
2026-09-14 21:24:59 +07:00
|
|
|
style="background: var(--primary); border-color: var(--primary)"
|
2026-08-11 23:17:04 +07:00
|
|
|
>
|
2026-08-24 08:07:23 +07:00
|
|
|
<div class="mb-5 flex items-center justify-between px-2">
|
2026-09-12 19:38:11 +07:00
|
|
|
<router-link :to="auth.isAdmin ? '/projects' : '/select-project'" class="flex items-center gap-2.5">
|
2026-08-24 08:07:23 +07:00
|
|
|
<span
|
2026-09-14 21:24:59 +07:00
|
|
|
class="grid h-8 w-8 place-items-center rounded-[10px] font-display text-[15px] font-extrabold"
|
|
|
|
|
style="background: #fff; color: var(--primary)"
|
2026-08-24 08:07:23 +07:00
|
|
|
>M</span
|
|
|
|
|
>
|
2026-09-14 21:24:59 +07:00
|
|
|
<span class="font-display text-[15px] font-extrabold tracking-[-0.01em]" style="color: #fff">
|
2026-08-24 08:07:23 +07:00
|
|
|
Workspace
|
|
|
|
|
</span>
|
|
|
|
|
</router-link>
|
2026-09-12 19:38:11 +07:00
|
|
|
<Button
|
|
|
|
|
icon="pi pi-times"
|
|
|
|
|
text
|
|
|
|
|
rounded
|
|
|
|
|
severity="secondary"
|
|
|
|
|
class="lg:hidden"
|
2026-09-14 21:24:59 +07:00
|
|
|
style="color: #fff"
|
2026-08-24 08:07:23 +07:00
|
|
|
aria-label="Close menu"
|
|
|
|
|
@click="sidebarOpen = false"
|
2026-09-12 19:38:11 +07:00
|
|
|
/>
|
2026-08-11 23:17:04 +07:00
|
|
|
</div>
|
2026-08-24 08:07:23 +07:00
|
|
|
|
2026-09-12 19:38:11 +07:00
|
|
|
<nav v-if="currentProject" class="flex flex-col gap-0.5">
|
2026-09-14 21:24:59 +07:00
|
|
|
<div class="truncate px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide" style="color: rgba(255, 255, 255, 0.75)">
|
2026-09-12 19:38:11 +07:00
|
|
|
{{ currentProject.name }}
|
2026-08-26 00:11:45 +07:00
|
|
|
</div>
|
2026-08-11 23:17:04 +07:00
|
|
|
<router-link
|
2026-09-12 19:38:11 +07:00
|
|
|
v-if="auth.canView('tasks')"
|
2026-08-11 23:17:04 +07:00
|
|
|
class="nav-link"
|
2026-09-12 19:38:11 +07:00
|
|
|
:to="{ name: 'tasks-board', params: { id: currentProject.id } }"
|
2026-08-11 23:17:04 +07:00
|
|
|
@click="handleNavClick"
|
|
|
|
|
>
|
2026-09-12 19:38:11 +07:00
|
|
|
<i class="pi pi-check-square"></i> Tasks
|
|
|
|
|
</router-link>
|
|
|
|
|
<router-link
|
|
|
|
|
v-if="auth.canView('documents')"
|
|
|
|
|
class="nav-link"
|
|
|
|
|
:to="{ name: 'documents', params: { id: currentProject.id } }"
|
|
|
|
|
@click="handleNavClick"
|
|
|
|
|
>
|
|
|
|
|
<i class="pi pi-file"></i> Documents
|
|
|
|
|
</router-link>
|
|
|
|
|
<router-link
|
|
|
|
|
class="nav-link"
|
|
|
|
|
:to="{ name: 'members', params: { id: currentProject.id } }"
|
|
|
|
|
@click="handleNavClick"
|
|
|
|
|
>
|
|
|
|
|
<i class="pi pi-users"></i> Members
|
2026-08-11 23:17:04 +07:00
|
|
|
</router-link>
|
|
|
|
|
</nav>
|
2026-08-24 08:07:23 +07:00
|
|
|
|
2026-08-11 23:17:04 +07:00
|
|
|
<div class="flex-1"></div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
2026-08-24 08:07:23 +07:00
|
|
|
<header
|
|
|
|
|
class="flex items-center justify-between gap-3 border-b px-4 py-2.5 lg:px-6"
|
|
|
|
|
style="background: var(--panel); border-color: var(--hairline)"
|
|
|
|
|
>
|
2026-08-26 00:11:45 +07:00
|
|
|
<div class="flex min-w-0 items-center gap-3">
|
2026-08-11 23:17:04 +07:00
|
|
|
<Button
|
|
|
|
|
icon="pi pi-bars"
|
|
|
|
|
rounded
|
|
|
|
|
text
|
2026-08-24 08:07:23 +07:00
|
|
|
severity="secondary"
|
2026-08-11 23:17:04 +07:00
|
|
|
:aria-label="sidebarOpen ? 'Close menu' : 'Open menu'"
|
|
|
|
|
@click="sidebarOpen = !sidebarOpen"
|
|
|
|
|
/>
|
2026-08-26 00:11:45 +07:00
|
|
|
<Select
|
|
|
|
|
v-if="currentProject"
|
|
|
|
|
:model-value="currentProject.id"
|
|
|
|
|
:options="projects"
|
|
|
|
|
option-label="name"
|
|
|
|
|
option-value="id"
|
|
|
|
|
placeholder="Select project"
|
|
|
|
|
class="w-[200px]"
|
|
|
|
|
@change="onProjectChange"
|
|
|
|
|
/>
|
2026-08-11 23:17:04 +07:00
|
|
|
</div>
|
2026-08-24 08:07:23 +07:00
|
|
|
<div class="flex min-w-0 items-center gap-2">
|
2026-09-12 19:38:11 +07:00
|
|
|
<UserMenu />
|
2026-08-24 08:07:23 +07:00
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main class="flex-1 overflow-auto">
|
2026-09-20 16:17:33 +07:00
|
|
|
<div class="mx-auto h-full w-full p-5 sm:p-7 lg:px-9 lg:py-8">
|
2026-08-24 08:07:23 +07:00
|
|
|
<router-view />
|
|
|
|
|
</div>
|
2026-08-11 23:17:04 +07:00
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { useAuthStore } from '../stores/auth'
|
2026-09-12 19:38:11 +07:00
|
|
|
import { getProject, getProjects } from '../services/backend'
|
|
|
|
|
import UserMenu from '../components/UserMenu.vue'
|
2026-08-11 23:17:04 +07:00
|
|
|
import type { Project } from '../types'
|
|
|
|
|
|
|
|
|
|
const auth = useAuthStore()
|
|
|
|
|
const route = useRoute()
|
2026-08-26 00:11:45 +07:00
|
|
|
const router = useRouter()
|
2026-08-11 23:17:04 +07:00
|
|
|
|
|
|
|
|
const desktopMq = window.matchMedia('(min-width: 1024px)')
|
|
|
|
|
const isDesktop = ref(desktopMq.matches)
|
2026-08-24 08:07:23 +07:00
|
|
|
const sidebarOpen = ref(isDesktop.value)
|
2026-09-12 19:38:11 +07:00
|
|
|
const onDesktopChange = (e: MediaQueryListEvent) => {
|
|
|
|
|
isDesktop.value = e.matches
|
|
|
|
|
}
|
|
|
|
|
desktopMq.addEventListener('change', onDesktopChange)
|
|
|
|
|
onBeforeUnmount(() => desktopMq.removeEventListener('change', onDesktopChange))
|
2026-08-11 23:17:04 +07:00
|
|
|
|
|
|
|
|
function handleNavClick() {
|
|
|
|
|
if (!isDesktop.value) sidebarOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentProject = ref<Project | null>(null)
|
2026-08-26 00:11:45 +07:00
|
|
|
const projects = ref<Project[]>([])
|
|
|
|
|
|
|
|
|
|
async function loadProjects() {
|
|
|
|
|
try {
|
2026-09-13 18:59:48 +07:00
|
|
|
const res = await getProjects({ page: 1, pageSize: 100 })
|
2026-08-26 00:11:45 +07:00
|
|
|
projects.value = res.items
|
|
|
|
|
} catch {
|
|
|
|
|
projects.value = []
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-11 23:17:04 +07:00
|
|
|
|
|
|
|
|
async function loadCurrentProject(id: string | string[]) {
|
|
|
|
|
try {
|
|
|
|
|
currentProject.value = await getProject(String(id))
|
2026-09-12 19:38:11 +07:00
|
|
|
localStorage.setItem('mws_last_project', String(id))
|
2026-08-11 23:17:04 +07:00
|
|
|
} catch {
|
2026-08-26 00:11:45 +07:00
|
|
|
// keep existing project, don't clear on error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadLastProject() {
|
|
|
|
|
if (currentProject.value) return
|
|
|
|
|
const last = localStorage.getItem('mws_last_project')
|
|
|
|
|
if (last) {
|
|
|
|
|
try {
|
|
|
|
|
currentProject.value = await getProject(last)
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onProjectChange(e: { value: string }) {
|
|
|
|
|
const projectId = e.value
|
|
|
|
|
if (projectId) {
|
|
|
|
|
localStorage.setItem('mws_last_project', projectId)
|
|
|
|
|
router.push({ name: 'tasks-board', params: { id: projectId } })
|
2026-08-11 23:17:04 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => route.params.id,
|
|
|
|
|
(id) => {
|
2026-08-26 00:11:45 +07:00
|
|
|
if (id) {
|
|
|
|
|
void loadCurrentProject(id)
|
|
|
|
|
}
|
2026-08-11 23:17:04 +07:00
|
|
|
},
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => route.fullPath,
|
2026-09-12 19:38:11 +07:00
|
|
|
() => {
|
2026-08-11 23:17:04 +07:00
|
|
|
if (!isDesktop.value) sidebarOpen.value = false
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-26 00:11:45 +07:00
|
|
|
onMounted(() => {
|
|
|
|
|
void loadProjects()
|
|
|
|
|
void loadLastProject()
|
|
|
|
|
})
|
2026-08-11 23:17:04 +07:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.nav-link {
|
2026-08-24 08:07:23 +07:00
|
|
|
position: relative;
|
2026-08-11 23:17:04 +07:00
|
|
|
display: flex;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
align-items: center;
|
2026-08-24 08:07:23 +07:00
|
|
|
gap: 0.65rem;
|
2026-09-14 21:24:59 +07:00
|
|
|
border-radius: 10px;
|
2026-08-24 08:07:23 +07:00
|
|
|
padding: 0.5rem 0.875rem;
|
|
|
|
|
font-size: 0.875rem;
|
2026-08-11 23:17:04 +07:00
|
|
|
font-weight: 500;
|
2026-09-14 21:24:59 +07:00
|
|
|
color: rgba(255, 255, 255, 0.85);
|
2026-08-24 08:07:23 +07:00
|
|
|
transition: background-color 0.15s ease, color 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
.nav-link i {
|
|
|
|
|
font-size: 0.9rem;
|
2026-08-11 23:17:04 +07:00
|
|
|
}
|
|
|
|
|
.nav-link:hover {
|
2026-09-14 21:24:59 +07:00
|
|
|
background-color: rgba(255, 255, 255, 0.14);
|
|
|
|
|
color: #fff;
|
2026-08-11 23:17:04 +07:00
|
|
|
}
|
|
|
|
|
.nav-link.router-link-active {
|
2026-09-14 21:24:59 +07:00
|
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
|
|
|
color: #fff;
|
2026-08-24 08:07:23 +07:00
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
.app-dark .nav-link.router-link-active {
|
2026-09-14 21:24:59 +07:00
|
|
|
color: #fff;
|
2026-08-24 08:07:23 +07:00
|
|
|
}
|
|
|
|
|
.nav-link.router-link-active::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 0;
|
|
|
|
|
top: 20%;
|
|
|
|
|
height: 60%;
|
|
|
|
|
width: 3px;
|
|
|
|
|
border-radius: 999px;
|
2026-09-14 21:24:59 +07:00
|
|
|
background: #fff;
|
2026-08-11 23:17:04 +07:00
|
|
|
}
|
|
|
|
|
</style>
|