232 lines
7.7 KiB
Vue
232 lines
7.7 KiB
Vue
<template>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger as-child>
|
|
<SidebarMenuButton
|
|
size="lg"
|
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
:disabled="isLoading"
|
|
>
|
|
<!-- Loading State -->
|
|
<div
|
|
v-if="isLoading"
|
|
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
|
>
|
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
|
|
</div>
|
|
<!-- Error State -->
|
|
<div
|
|
v-else-if="error"
|
|
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-destructive text-destructive-foreground"
|
|
>
|
|
<AlertCircle class="size-4" />
|
|
</div>
|
|
<!-- Normal State -->
|
|
<div
|
|
v-else
|
|
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
|
>
|
|
<component :is="activeProject.icon" class="size-4" />
|
|
</div>
|
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
<span class="truncate font-semibold">
|
|
{{ isLoading ? 'Loading...' : error ? 'Error' : activeProject.name }}
|
|
</span>
|
|
<span class="truncate text-xs">
|
|
{{ isLoading ? 'Fetching projects' : error ? 'Failed to load' : activeProject.status }}
|
|
</span>
|
|
</div>
|
|
<ChevronsUpDown class="ml-auto" />
|
|
</SidebarMenuButton>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
class="w-[--reka-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
|
align="start"
|
|
:side="isMobile ? 'bottom' : 'right'"
|
|
:side-offset="4"
|
|
>
|
|
<DropdownMenuLabel class="text-xs text-muted-foreground">
|
|
Projects
|
|
</DropdownMenuLabel>
|
|
|
|
<!-- Loading State in Dropdown -->
|
|
<DropdownMenuItem v-if="isLoading" class="gap-2 p-2" disabled>
|
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
|
|
<span class="text-muted-foreground">Loading projects...</span>
|
|
</DropdownMenuItem>
|
|
|
|
<!-- Error State in Dropdown -->
|
|
<template v-else-if="error">
|
|
<DropdownMenuItem class="gap-2 p-2" disabled>
|
|
<AlertCircle class="size-4 text-destructive" />
|
|
<span class="text-destructive text-sm">{{ error }}</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem class="gap-2 p-2" @click="refreshProjects">
|
|
<RefreshCw class="size-4" />
|
|
<span>Retry</span>
|
|
</DropdownMenuItem>
|
|
</template>
|
|
|
|
<!-- Projects List -->
|
|
<template v-else>
|
|
<DropdownMenuItem
|
|
v-for="(project, index) in projects"
|
|
:key="project.id"
|
|
class="gap-2 p-2"
|
|
@click="setActiveProject(project)"
|
|
>
|
|
<div
|
|
class="flex size-6 items-center justify-center rounded-sm border"
|
|
>
|
|
<component :is="project.icon" class="size-4 shrink-0" />
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<span class="font-medium">{{ project.name }}</span>
|
|
<span class="text-xs text-muted-foreground">{{
|
|
project.status
|
|
}}</span>
|
|
</div>
|
|
<DropdownMenuShortcut>⌘{{ index + 1 }}</DropdownMenuShortcut>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem class="gap-2 p-2" @click="refreshProjects">
|
|
<RefreshCw class="size-4" />
|
|
<div class="font-medium text-muted-foreground">Refresh projects</div>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
v-if="canCreateProjects"
|
|
class="gap-2 p-2"
|
|
@click="handleCreateProject"
|
|
>
|
|
<div
|
|
class="flex size-6 items-center justify-center rounded-md border bg-background"
|
|
>
|
|
<Plus class="size-4" />
|
|
</div>
|
|
<div class="font-medium text-muted-foreground">Create project</div>
|
|
</DropdownMenuItem>
|
|
</template>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, watch, onMounted } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import { ChevronsUpDown, Plus, AlertCircle, RefreshCw } from "lucide-vue-next";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuShortcut,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
useSidebar,
|
|
} from "@/components/ui/sidebar";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { useProjectsStore } from "@/stores/projects";
|
|
|
|
const router = useRouter();
|
|
const { isMobile } = useSidebar();
|
|
const authStore = useAuthStore();
|
|
const projectsStore = useProjectsStore();
|
|
|
|
// Get projects and active project from store
|
|
const projects = computed(() => projectsStore.availableProjects);
|
|
const activeProject = computed(() => projectsStore.currentProject);
|
|
const isLoading = computed(() => projectsStore.isLoading);
|
|
const error = computed(() => projectsStore.error);
|
|
|
|
// Check if user can create projects
|
|
const canCreateProjects = computed(() => {
|
|
const user = authStore.user;
|
|
return user?.is_admin || user?.role === "coordinator";
|
|
});
|
|
|
|
const setActiveProject = (project: any) => {
|
|
projectsStore.setActiveProject(project);
|
|
|
|
// Navigate to project-specific view
|
|
if (project.id === 0) {
|
|
// "All Projects" view
|
|
router.push("/projects");
|
|
} else {
|
|
// Specific project view - navigate to overview tab
|
|
router.push(`/projects/${project.id}`);
|
|
}
|
|
};
|
|
|
|
const handleCreateProject = () => {
|
|
if (canCreateProjects.value) {
|
|
router.push("/projects/new");
|
|
}
|
|
};
|
|
|
|
const refreshProjects = async () => {
|
|
try {
|
|
await projectsStore.fetchProjects();
|
|
} catch (error) {
|
|
console.error('Failed to refresh projects:', error);
|
|
}
|
|
};
|
|
|
|
// Set active project based on current route
|
|
const updateActiveProjectFromRoute = () => {
|
|
const currentPath = router.currentRoute.value.path;
|
|
|
|
if (currentPath.startsWith("/projects/")) {
|
|
const pathSegments = currentPath.split("/");
|
|
if (pathSegments[2] === "new") {
|
|
// Creating new project, keep current active project
|
|
return;
|
|
}
|
|
|
|
const projectId = parseInt(pathSegments[2]);
|
|
if (!isNaN(projectId)) {
|
|
const project = projectsStore.getProjectById(projectId);
|
|
if (project) {
|
|
projectsStore.setActiveProject(project);
|
|
}
|
|
}
|
|
} else if (currentPath === "/projects") {
|
|
projectsStore.setActiveProject(projectsStore.allProjectsView);
|
|
}
|
|
};
|
|
|
|
// Watch for route changes
|
|
watch(
|
|
() => router.currentRoute.value.path,
|
|
() => {
|
|
updateActiveProjectFromRoute();
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
// Watch for authentication changes to fetch projects
|
|
watch(
|
|
() => authStore.isAuthenticated,
|
|
(isAuthenticated) => {
|
|
if (isAuthenticated && projectsStore.projects.length === 0 && !projectsStore.isLoading) {
|
|
refreshProjects();
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
// Ensure projects are loaded on component mount
|
|
onMounted(() => {
|
|
if (authStore.isAuthenticated && projectsStore.projects.length === 0 && !projectsStore.isLoading) {
|
|
refreshProjects();
|
|
}
|
|
});
|
|
</script> |