diff --git a/frontend/src/components/asset/EditableTaskStatus.vue b/frontend/src/components/asset/EditableTaskStatus.vue index 859db53..18ba4f6 100644 --- a/frontend/src/components/asset/EditableTaskStatus.vue +++ b/frontend/src/components/asset/EditableTaskStatus.vue @@ -169,8 +169,9 @@ import { User, Search, Check, X } from 'lucide-vue-next' import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue' import { TaskStatus } from '@/services/asset' import { taskService } from '@/services/task' -import { projectService, type ProjectMember } from '@/services/project' +import type { ProjectMember } from '@/services/project' import { useTaskStatusesStore } from '@/stores/taskStatuses' +import { useProjectMembersStore } from '@/stores/projectMembers' import type { CustomTaskStatus, SystemTaskStatus } from '@/services/customTaskStatus' import { useAvatarUrl } from '@/composables/useAvatarUrl' @@ -202,11 +203,12 @@ const { getAvatarUrl } = useAvatarUrl() // Use the shared task statuses store instead of direct API calls const taskStatusesStore = useTaskStatusesStore() +const projectMembersStore = useProjectMembersStore() const isUpdating = ref(false) const isAssigning = ref(false) -const isLoadingMembers = ref(false) -const projectMembers = ref([]) +const projectMembers = computed(() => projectMembersStore.getMembers(props.projectId) || []) +const isLoadingMembers = computed(() => projectMembersStore.isLoading(props.projectId)) const searchQuery = ref('') // Filtered project members based on search query @@ -306,17 +308,12 @@ const fetchStatuses = async () => { } } -// Load project members +// Load project members (shared cache across all EditableTaskStatus instances for this project) const loadProjectMembers = async () => { - if (projectMembers.value.length > 0) return // Already loaded - - isLoadingMembers.value = true try { - projectMembers.value = await projectService.getProjectMembers(props.projectId) + await projectMembersStore.fetchProjectMembers(props.projectId) } catch (error) { console.error('Failed to load project members:', error) - } finally { - isLoadingMembers.value = false } } @@ -400,10 +397,9 @@ onMounted(() => { loadProjectMembers() }) -// Refetch statuses when projectId changes +// Refetch statuses and members when projectId changes watch(() => props.projectId, () => { fetchStatuses() - // Clear project members when project changes - projectMembers.value = [] + loadProjectMembers() }) \ No newline at end of file diff --git a/frontend/src/components/episode/EpisodeDropdown.vue b/frontend/src/components/episode/EpisodeDropdown.vue index 441b6fc..be9b639 100644 --- a/frontend/src/components/episode/EpisodeDropdown.vue +++ b/frontend/src/components/episode/EpisodeDropdown.vue @@ -83,7 +83,7 @@ import { } from '@/components/ui/select' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' -import { useAuthStore } from '@/stores/auth' +import { usePermission } from '@/composables/usePermission' import { episodeService, type Episode } from '@/services/episode' interface Props { @@ -100,7 +100,7 @@ const props = defineProps() const emit = defineEmits() const router = useRouter() -const authStore = useAuthStore() +const { isCoordinatorOrAdmin } = usePermission() // Reactive state const episodes = ref([]) @@ -108,11 +108,7 @@ const isLoading = ref(false) const error = ref(null) // Computed properties -const canCreateEpisodes = computed(() => { - const userRole = authStore.userRole - const isAdmin = authStore.user?.is_admin - return userRole === 'coordinator' || isAdmin -}) +const canCreateEpisodes = computed(() => isCoordinatorOrAdmin.value) const sortedEpisodes = computed(() => { return [...episodes.value].sort((a, b) => { diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index 803abb4..c0dbd36 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -123,10 +123,12 @@ import { } 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 user = computed(() => authStore.user) @@ -150,7 +152,7 @@ const navigationItems = computed(() => { { title: 'My Tasks', url: '/tasks', icon: CheckSquare }, ] - if (userRole.value === 'coordinator' || authStore.isAdmin) { + if (isCoordinatorOrAdmin.value) { baseItems.push( { title: 'Projects', url: '/projects', icon: FolderOpen }, { title: 'Team', url: '/users', icon: Users } diff --git a/frontend/src/components/layout/ProjectSwitcher.vue b/frontend/src/components/layout/ProjectSwitcher.vue index 0f198bd..af9f0dd 100644 --- a/frontend/src/components/layout/ProjectSwitcher.vue +++ b/frontend/src/components/layout/ProjectSwitcher.vue @@ -135,11 +135,13 @@ import { } from "@/components/ui/sidebar"; import { useAuthStore } from "@/stores/auth"; import { useProjectsStore } from "@/stores/projects"; +import { usePermission } from "@/composables/usePermission"; const router = useRouter(); const { isMobile } = useSidebar(); const authStore = useAuthStore(); const projectsStore = useProjectsStore(); +const { isCoordinatorOrAdmin } = usePermission(); // Get projects and active project from store const projects = computed(() => projectsStore.availableProjects); @@ -148,10 +150,7 @@ 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 canCreateProjects = computed(() => isCoordinatorOrAdmin.value); const setActiveProject = (project: any) => { projectsStore.setActiveProject(project); diff --git a/frontend/src/components/project/TechnicalSpecsManager.vue b/frontend/src/components/project/TechnicalSpecsManager.vue index 7927625..2067c38 100644 --- a/frontend/src/components/project/TechnicalSpecsManager.vue +++ b/frontend/src/components/project/TechnicalSpecsManager.vue @@ -83,7 +83,7 @@ import { ref, computed, onMounted } from 'vue' import { Edit, AlertCircle, RefreshCw, Bell, X } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' -import { useAuthStore } from '@/stores/auth' +import { usePermission } from '@/composables/usePermission' import { useToast } from '@/components/ui/toast/use-toast' import { projectService, type ProjectTechnicalSpecs } from '@/services/project' import { notificationService } from '@/services/notifications' @@ -97,7 +97,7 @@ interface Props { const props = defineProps() -const authStore = useAuthStore() +const { isCoordinatorOrAdmin } = usePermission() const { toast } = useToast() // State @@ -108,11 +108,7 @@ const isEditing = ref(false) const showNotification = ref(false) // Computed properties -const canEdit = computed(() => { - const userRole = authStore.userRole - const isAdmin = authStore.user?.is_admin - return userRole === 'coordinator' || isAdmin -}) +const canEdit = computed(() => isCoordinatorOrAdmin.value) // Methods const loadSpecs = async () => { diff --git a/frontend/src/components/project/TechnicalSpecsPanel.vue b/frontend/src/components/project/TechnicalSpecsPanel.vue index 54ebab2..199028d 100644 --- a/frontend/src/components/project/TechnicalSpecsPanel.vue +++ b/frontend/src/components/project/TechnicalSpecsPanel.vue @@ -55,7 +55,7 @@ import { Settings, ChevronDown, AlertCircle, RefreshCw } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' -import { useAuthStore } from '@/stores/auth' +import { usePermission } from '@/composables/usePermission' import { projectService, type ProjectTechnicalSpecs } from '@/services/project' import TechnicalSpecsDisplay from './TechnicalSpecsDisplay.vue' import DepartmentSpecsPanel from './DepartmentSpecsPanel.vue' @@ -73,7 +73,7 @@ interface Emits { const props = defineProps() defineEmits() -const authStore = useAuthStore() +const { isCoordinatorOrAdmin } = usePermission() // State const isOpen = ref(props.defaultOpen || false) @@ -82,11 +82,7 @@ const error = ref(null) const specs = ref() // Computed properties -const canEdit = computed(() => { - const userRole = authStore.userRole - const isAdmin = authStore.user?.is_admin - return userRole === 'coordinator' || isAdmin -}) +const canEdit = computed(() => isCoordinatorOrAdmin.value) const hasSpecs = computed(() => { if (!specs.value) return false diff --git a/frontend/src/components/shot/EditableTaskStatus.vue b/frontend/src/components/shot/EditableTaskStatus.vue index 7945567..0b84d5c 100644 --- a/frontend/src/components/shot/EditableTaskStatus.vue +++ b/frontend/src/components/shot/EditableTaskStatus.vue @@ -169,8 +169,9 @@ import { User, Search, Check, X } from 'lucide-vue-next' import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue' import { TaskStatus } from '@/services/shot' import { taskService } from '@/services/task' -import { projectService, type ProjectMember } from '@/services/project' +import type { ProjectMember } from '@/services/project' import { useTaskStatusesStore } from '@/stores/taskStatuses' +import { useProjectMembersStore } from '@/stores/projectMembers' interface StatusOption { id: string @@ -198,11 +199,12 @@ const emit = defineEmits() // Use the shared task statuses store instead of direct API calls const taskStatusesStore = useTaskStatusesStore() +const projectMembersStore = useProjectMembersStore() const isUpdating = ref(false) const isAssigning = ref(false) -const isLoadingMembers = ref(false) -const projectMembers = ref([]) +const projectMembers = computed(() => projectMembersStore.getMembers(props.projectId) || []) +const isLoadingMembers = computed(() => projectMembersStore.isLoading(props.projectId)) const searchQuery = ref('') // Filtered project members based on search query @@ -306,27 +308,18 @@ const fetchStatuses = async () => { } } -// Load project members +// Load project members (shared cache across all EditableTaskStatus instances for this project) const loadProjectMembers = async () => { - if (projectMembers.value.length > 0) return // Already loaded - - isLoadingMembers.value = true try { - console.log('Loading project members for project:', props.projectId) - projectMembers.value = await projectService.getProjectMembers(props.projectId) - console.log('Loaded project members:', projectMembers.value) + await projectMembersStore.fetchProjectMembers(props.projectId) } catch (error) { console.error('Failed to load project members:', error) - } finally { - isLoadingMembers.value = false } } // Ensure members are loaded when popover is about to open const ensureMembersLoaded = () => { - console.log('Ensuring project members are loaded') if (projectMembers.value.length === 0) { - console.log('Loading project members on button click') loadProjectMembers() } } @@ -407,7 +400,6 @@ onMounted(() => { // Refetch statuses when projectId changes watch(() => props.projectId, () => { fetchStatuses() - // Clear project members when project changes - projectMembers.value = [] + loadProjectMembers() }) diff --git a/frontend/src/components/shot/ShotBrowser.vue b/frontend/src/components/shot/ShotBrowser.vue index 54d8734..4d17ecf 100644 --- a/frontend/src/components/shot/ShotBrowser.vue +++ b/frontend/src/components/shot/ShotBrowser.vue @@ -449,44 +449,64 @@ const loadShots = async () => { const loadEpisodes = async () => { if (!props.projectId) return - + try { const data = await episodeService.getEpisodes(props.projectId) episodes.value = data } catch (err) { console.error('Failed to load episodes:', err) + toast({ + title: 'Failed to load episodes', + description: err instanceof Error ? err.message : 'Episode filtering may be unavailable', + variant: 'destructive' + }) } } const loadTaskTypes = async () => { if (!props.projectId) return - + try { const data = await customTaskTypeService.getAllTaskTypes(props.projectId) allTaskTypes.value = data.shot_task_types || [] } catch (err) { console.error('Failed to load task types:', err) + toast({ + title: 'Failed to load task types', + description: err instanceof Error ? err.message : 'Task columns may be unavailable', + variant: 'destructive' + }) } } const loadTaskStatuses = async () => { if (!props.projectId) return - + try { await taskStatusesStore.fetchProjectStatuses(props.projectId) } catch (err) { console.error('Failed to load task statuses:', err) + toast({ + title: 'Failed to load task statuses', + description: err instanceof Error ? err.message : 'Task status options may be unavailable', + variant: 'destructive' + }) } } const loadProjectContext = async () => { if (!props.projectId) return - + try { const project = await projectService.getProject(props.projectId) projectContext.value = project } catch (err) { console.error('Failed to load project context:', err) + toast({ + title: 'Failed to load project context', + description: err instanceof Error ? err.message : 'Some shot validation checks may be unavailable', + variant: 'destructive' + }) } } diff --git a/frontend/src/components/shot/ShotDetailPanel.vue b/frontend/src/components/shot/ShotDetailPanel.vue index 950d164..b7084a5 100644 --- a/frontend/src/components/shot/ShotDetailPanel.vue +++ b/frontend/src/components/shot/ShotDetailPanel.vue @@ -21,7 +21,7 @@ {{ formatStatus(shot.status) }} - + Deleted {{ formatDeletedDate(shot.deleted_at) }} @@ -350,9 +350,9 @@ import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue' import { shotService, ShotStatus, type Shot, type TaskStatusInfo } from '@/services/shot' import { taskService } from '@/services/task' import { projectService, type ProjectMember } from '@/services/project' -import { useAuthStore } from '@/stores/auth' import { useTaskStatusesStore } from '@/stores/taskStatuses' import { useAvatarUrl } from '@/composables/useAvatarUrl' +import { usePermission } from '@/composables/usePermission' // Use TaskStatusInfo from shot service instead of local Task interface interface Task extends TaskStatusInfo { @@ -384,9 +384,9 @@ interface Emits { const props = defineProps() const emit = defineEmits() -const authStore = useAuthStore() const taskStatusesStore = useTaskStatusesStore() const { getAvatarUrl } = useAvatarUrl() +const { isAdmin, isCoordinatorOrAdmin } = usePermission() // Reactive state const shot = ref(null) @@ -432,25 +432,17 @@ const taskStatusCounts = computed(() => { -const canCreateTask = computed(() => { - return authStore.user?.role === 'coordinator' || authStore.user?.is_admin -}) +const canCreateTask = computed(() => isCoordinatorOrAdmin.value) -const canCreateNote = computed(() => { - return authStore.user?.role === 'coordinator' || authStore.user?.is_admin -}) +const canCreateNote = computed(() => isCoordinatorOrAdmin.value) -const canLinkAssets = computed(() => { - return authStore.user?.role === 'coordinator' || authStore.user?.is_admin -}) +const canLinkAssets = computed(() => isCoordinatorOrAdmin.value) const canUploadReferences = computed(() => { return true // All users can upload references }) -const canEditDesign = computed(() => { - return authStore.user?.role === 'coordinator' || authStore.user?.is_admin -}) +const canEditDesign = computed(() => isCoordinatorOrAdmin.value) const availableTaskTypes = computed(() => { const existingTypes = new Set(tasks.value.map(task => task.task_type)) diff --git a/frontend/src/components/task/TaskDetailPanel.vue b/frontend/src/components/task/TaskDetailPanel.vue index 408355c..bb69552 100644 --- a/frontend/src/components/task/TaskDetailPanel.vue +++ b/frontend/src/components/task/TaskDetailPanel.vue @@ -320,6 +320,7 @@ const emit = defineEmits<{ const { toast } = useToast() const authStore = useAuthStore() +const { isCoordinatorOrAdmin } = usePermission() const task = ref(null) const loading = ref(false) @@ -350,10 +351,7 @@ const canSubmitWork = computed(() => { ) }) -const canReassign = computed(() => { - if (!authStore.user) return false - return authStore.user.is_admin || authStore.user.role === 'coordinator' -}) +const canReassign = computed(() => isCoordinatorOrAdmin.value) async function loadTask() { loading.value = true @@ -478,6 +476,7 @@ function getUserInitials(member: ProjectMember): string { } import { useAvatarUrl } from '@/composables/useAvatarUrl' +import { usePermission } from '@/composables/usePermission' const { getAvatarUrl } = useAvatarUrl() diff --git a/frontend/src/components/task/TaskList.vue b/frontend/src/components/task/TaskList.vue index 96da93c..72531ba 100644 --- a/frontend/src/components/task/TaskList.vue +++ b/frontend/src/components/task/TaskList.vue @@ -299,7 +299,7 @@