f547d05478
Adds a Schedule tab (Kitsu-style production schedule) under each project: tasks grouped by type with drag-to-reschedule bars, Day/Week/ Month zoom, manual date-range control, weekend shading, a frozen task column, and a two-tier month/date axis header. Requires a new start_date field on Task (start_date was previously missing; only deadline existed) and shadcn DatePicker inputs replace native date inputs on the Schedule toolbar and TaskDetailPanel's Start Date/ Deadline fields. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
295 lines
11 KiB
Vue
295 lines
11 KiB
Vue
<template>
|
|
<Sidebar variant="inset" v-bind="props">
|
|
<SidebarHeader>
|
|
<ProjectSwitcher v-if="userRole !== 'developer'" />
|
|
|
|
<!-- Developer header (no project switching) -->
|
|
<SidebarMenu v-else>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton size="lg" as-child>
|
|
<router-link to="/" class="flex items-center gap-2">
|
|
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
<Clapperboard class="size-4" />
|
|
</div>
|
|
<div class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
|
<span class="truncate font-semibold">VFX Studio</span>
|
|
<span class="truncate text-xs">Developer Tools</span>
|
|
</div>
|
|
</router-link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarHeader>
|
|
|
|
<SidebarContent>
|
|
<!-- Main Navigation -->
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem v-for="item in navigationItems" :key="item.title">
|
|
<!-- Collapsed sidebar + Projects (with an active project): hovering reveals sub-links,
|
|
while the icon itself still navigates to /projects like any other nav item. -->
|
|
<div
|
|
v-if="item.title === 'Projects' && isCollapsed && projectTabs.length > 0"
|
|
@mouseenter="showProjectFlyout = true"
|
|
@mouseleave="showProjectFlyout = false"
|
|
>
|
|
<Popover :open="showProjectFlyout">
|
|
<PopoverAnchor>
|
|
<SidebarMenuButton as-child :title="item.title">
|
|
<router-link :to="item.url" class="flex items-center gap-2">
|
|
<component :is="item.icon" class="size-4" />
|
|
<span class="group-data-[collapsible=icon]:hidden">{{ item.title }}</span>
|
|
</router-link>
|
|
</SidebarMenuButton>
|
|
</PopoverAnchor>
|
|
<PopoverContent
|
|
side="right"
|
|
align="start"
|
|
class="w-48 p-1"
|
|
@open-auto-focus.prevent
|
|
@mouseenter="showProjectFlyout = true"
|
|
@mouseleave="showProjectFlyout = false"
|
|
>
|
|
<router-link
|
|
v-for="tab in projectTabs"
|
|
:key="tab.id"
|
|
:to="tab.route"
|
|
class="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground"
|
|
:class="activeProjectTab === tab.id && 'bg-accent text-accent-foreground'"
|
|
@click="showProjectFlyout = false"
|
|
>
|
|
<component :is="tab.icon" class="size-4" />
|
|
<span>{{ tab.label }}</span>
|
|
</router-link>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<template v-else>
|
|
<SidebarMenuButton as-child :tooltip="isCollapsed ? item.title : undefined">
|
|
<router-link :to="item.url" class="flex items-center gap-2">
|
|
<component :is="item.icon" class="size-4" />
|
|
<span class="group-data-[collapsible=icon]:hidden">{{ item.title }}</span>
|
|
</router-link>
|
|
</SidebarMenuButton>
|
|
|
|
<!-- Project sub-navigation - shown under "Projects" when inside a specific project -->
|
|
<SidebarMenuSub v-if="item.title === 'Projects' && projectTabs.length > 0">
|
|
<SidebarMenuSubItem v-for="tab in projectTabs" :key="tab.id">
|
|
<SidebarMenuSubButton as-child :is-active="activeProjectTab === tab.id">
|
|
<router-link :to="tab.route" class="flex items-center gap-2">
|
|
<component :is="tab.icon" class="size-4" />
|
|
<span class="truncate">{{ tab.label }}</span>
|
|
</router-link>
|
|
</SidebarMenuSubButton>
|
|
</SidebarMenuSubItem>
|
|
</SidebarMenuSub>
|
|
</template>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
|
|
<!-- View Settings Section - Only show on shot/project pages -->
|
|
<SidebarColumnSwitch v-if="userRole !== 'developer' && isOnShotPage" />
|
|
|
|
<!-- Projects Section -->
|
|
<!-- <SidebarGroup v-if="userRole !== 'developer'">
|
|
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem v-for="project in recentProjects" :key="project.id">
|
|
<SidebarMenuButton as-child :tooltip="isCollapsed ? project.name : undefined">
|
|
<router-link :to="`/projects/${project.id}`" class="flex items-center gap-2">
|
|
<Folder class="size-4" />
|
|
<span class="truncate group-data-[collapsible=icon]:hidden">{{ project.name }}</span>
|
|
</router-link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroup> -->
|
|
|
|
<!-- Admin Tools (only for admin users) -->
|
|
<SidebarGroup v-if="authStore.isAdmin">
|
|
<SidebarGroupLabel>Administration</SidebarGroupLabel>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem v-for="item in adminItems" :key="item.title">
|
|
<SidebarMenuButton as-child :tooltip="isCollapsed ? item.title : undefined">
|
|
<router-link :to="item.url" class="flex items-center gap-2">
|
|
<component :is="item.icon" class="size-4" />
|
|
<span class="group-data-[collapsible=icon]:hidden">{{ item.title }}</span>
|
|
</router-link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
|
|
<!-- Developer Tools (only for developer role) -->
|
|
<SidebarGroup v-if="userRole === 'developer'">
|
|
<SidebarGroupLabel>Developer Tools</SidebarGroupLabel>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem v-for="item in developerItems" :key="item.title">
|
|
<SidebarMenuButton as-child :tooltip="isCollapsed ? item.title : undefined">
|
|
<router-link :to="item.url" class="flex items-center gap-2">
|
|
<component :is="item.icon" class="size-4" />
|
|
<span class="group-data-[collapsible=icon]:hidden">{{ item.title }}</span>
|
|
</router-link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
</SidebarContent>
|
|
|
|
<SidebarFooter>
|
|
<UserMenu />
|
|
</SidebarFooter>
|
|
</Sidebar>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarGroupLabel,
|
|
SidebarHeader,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarMenuSub,
|
|
SidebarMenuSubButton,
|
|
SidebarMenuSubItem,
|
|
useSidebar,
|
|
SidebarProps
|
|
} from '@/components/ui/sidebar'
|
|
import {
|
|
Clapperboard,
|
|
Home,
|
|
CheckSquare,
|
|
FolderOpen,
|
|
Users,
|
|
Settings,
|
|
Folder,
|
|
Key,
|
|
Database,
|
|
BarChart3,
|
|
RotateCcw,
|
|
LayoutDashboard,
|
|
Camera,
|
|
Package,
|
|
ListTodo,
|
|
ShieldCheck,
|
|
GanttChartSquare,
|
|
} from 'lucide-vue-next'
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { usePermission } from '@/composables/usePermission'
|
|
import ProjectSwitcher from './ProjectSwitcher.vue'
|
|
import UserMenu from './UserMenu.vue'
|
|
import SidebarColumnSwitch from '@/components/ui/sidebar/SidebarColumnSwitch.vue'
|
|
const authStore = useAuthStore()
|
|
const { isCoordinatorOrAdmin } = usePermission()
|
|
const { state } = useSidebar()
|
|
const route = useRoute()
|
|
const showProjectFlyout = ref(false)
|
|
const user = computed(() => authStore.user)
|
|
const userRole = computed(() => authStore.user?.role || 'artist')
|
|
|
|
// Check if sidebar is collapsed
|
|
const isCollapsed = computed(() => state.value === 'collapsed')
|
|
|
|
// Check if on shot page - show column switch only on shot pages
|
|
const isOnShotPage = computed(() => {
|
|
return route.path.endsWith('/shots') || route.path.includes('/project/')
|
|
})
|
|
|
|
// The project id when on a specific project's page (null on /projects, /projects/new, etc.)
|
|
const currentProjectId = computed(() => {
|
|
const id = route.params.projectId
|
|
const parsed = typeof id === 'string' ? parseInt(id) : NaN
|
|
return isNaN(parsed) ? null : parsed
|
|
})
|
|
|
|
// Sub-nav items shown under the "Projects" nav item when inside a specific project
|
|
const projectTabs = computed(() => {
|
|
const id = currentProjectId.value
|
|
if (id === null) return []
|
|
return [
|
|
{ id: 'overview', label: 'Overview', icon: LayoutDashboard, route: `/projects/${id}` },
|
|
{ id: 'shots', label: 'Shots', icon: Camera, route: `/projects/${id}/shots` },
|
|
{ id: 'assets', label: 'Assets', icon: Package, route: `/projects/${id}/assets` },
|
|
{ id: 'tasks', label: 'Tasks', icon: ListTodo, route: `/projects/${id}/tasks` },
|
|
{ id: 'schedule', label: 'Schedule', icon: GanttChartSquare, route: `/projects/${id}/schedule` },
|
|
{ id: 'settings', label: 'Settings', icon: Settings, route: `/projects/${id}/settings` },
|
|
]
|
|
})
|
|
|
|
const activeProjectTab = computed(() => {
|
|
const id = currentProjectId.value
|
|
if (id === null) return null
|
|
const path = route.path
|
|
if (path === `/projects/${id}`) return 'overview'
|
|
if (path.startsWith(`/projects/${id}/shots`)) return 'shots'
|
|
if (path.startsWith(`/projects/${id}/assets`)) return 'assets'
|
|
if (path.startsWith(`/projects/${id}/tasks`)) return 'tasks'
|
|
if (path.startsWith(`/projects/${id}/schedule`)) return 'schedule'
|
|
if (path.startsWith(`/projects/${id}/settings`)) return 'settings'
|
|
return null
|
|
})
|
|
|
|
const props = withDefaults(defineProps<SidebarProps>(), {
|
|
collapsible: "icon",
|
|
})
|
|
// Navigation items based on user role
|
|
const navigationItems = computed(() => {
|
|
const baseItems = [
|
|
{ title: 'Dashboard', url: '/', icon: Home },
|
|
{ title: 'My Tasks', url: '/tasks', icon: CheckSquare },
|
|
]
|
|
|
|
if (isCoordinatorOrAdmin.value) {
|
|
baseItems.push(
|
|
{ title: 'Projects', url: '/projects', icon: FolderOpen },
|
|
{ title: 'Team', url: '/users', icon: Users }
|
|
)
|
|
}
|
|
|
|
if (userRole.value === 'director' || authStore.isAdmin) {
|
|
baseItems.push(
|
|
{ title: 'Reviews', url: '/reviews', icon: CheckSquare }
|
|
)
|
|
}
|
|
|
|
if (authStore.isAdmin) {
|
|
baseItems.push(
|
|
{ title: 'Settings', url: '/settings', icon: Settings }
|
|
)
|
|
}
|
|
|
|
return baseItems
|
|
})
|
|
|
|
// Admin-specific navigation items
|
|
const adminItems = computed(() => [
|
|
{ title: 'Recovery Management', url: '/admin/deleted-items', icon: RotateCcw },
|
|
{ title: 'Role Management', url: '/admin/roles', icon: ShieldCheck },
|
|
])
|
|
|
|
// Developer-specific navigation items
|
|
const developerItems = computed(() => [
|
|
{ title: 'API Keys', url: '/developer/api-keys', icon: Key },
|
|
{ title: 'All Projects', url: '/developer/projects', icon: Database },
|
|
{ title: 'All Tasks', url: '/developer/tasks', icon: CheckSquare },
|
|
{ title: 'Usage Analytics', url: '/developer/analytics', icon: BarChart3 }
|
|
])
|
|
|
|
// Mock recent projects - this would come from a store in real implementation
|
|
const recentProjects = computed(() => [
|
|
{ id: 1, name: 'Project Alpha' },
|
|
{ id: 2, name: 'Project Beta' },
|
|
{ id: 3, name: 'Project Gamma' }
|
|
])
|
|
</script> |