From 915980dcdf5f30cef343baf4963a4f6c0bbe813f Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Wed, 4 Jun 2025 11:53:33 +0530 Subject: [PATCH] feat(project-view): implement finance tab visibility based on user permissions - Added permission checks to conditionally display the finance tab in the project view based on user roles. - Introduced `hasFinanceViewPermission` utility to determine access rights for the finance tab. - Updated tab management logic to handle redirection and default tab selection when permissions change. --- .../projects/projectView/project-view.tsx | 56 ++++++++++++++++++- .../src/utils/finance-permissions.ts | 23 ++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/worklenz-frontend/src/pages/projects/projectView/project-view.tsx b/worklenz-frontend/src/pages/projects/projectView/project-view.tsx index a625fcf7..9edebbb1 100644 --- a/worklenz-frontend/src/pages/projects/projectView/project-view.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/project-view.tsx @@ -19,6 +19,8 @@ import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice'; import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice'; import { tabItems } from '@/lib/project/project-view-constants'; import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice'; +import { useAuthService } from '@/hooks/useAuth'; +import { hasFinanceViewPermission } from '@/utils/finance-permissions'; const DeleteStatusDrawer = React.lazy(() => import('@/components/project-task-filters/delete-status-drawer/delete-status-drawer')); const PhaseDrawer = React.lazy(() => import('@features/projects/singleProject/phase/PhaseDrawer')); @@ -51,7 +53,33 @@ const ProjectView = () => { const selectedProject = useAppSelector(state => state.projectReducer.project); useDocumentTitle(selectedProject?.name || 'Project View'); - const [activeTab, setActiveTab] = useState(searchParams.get('tab') || tabItems[0].key); + + // Auth and permissions + const auth = useAuthService(); + const currentSession = auth.getCurrentSession(); + const hasFinancePermission = hasFinanceViewPermission(currentSession, selectedProject); + + // Filter tab items based on permissions + const filteredTabItems = useMemo(() => { + return tabItems.filter(item => { + // Hide finance tab if user doesn't have permission + if (item.key === 'finance' && !hasFinancePermission) { + return false; + } + return true; + }); + }, [hasFinancePermission]); + + // Get the default tab from filtered items + const defaultTab = useMemo(() => { + const requestedTab = searchParams.get('tab'); + if (requestedTab && filteredTabItems.some(item => item.key === requestedTab)) { + return requestedTab; + } + return filteredTabItems[0]?.key || 'tasks-list'; + }, [searchParams, filteredTabItems]); + + const [activeTab, setActiveTab] = useState(defaultTab); const [pinnedTab, setPinnedTab] = useState(searchParams.get('pinned_tab') || ''); const [taskid, setTaskId] = useState(searchParams.get('task') || ''); @@ -85,6 +113,28 @@ const ProjectView = () => { }; }, [dispatch, navigate, projectId, taskid, resetProjectData]); + // Redirect if user is on finance tab but doesn't have permission + useEffect(() => { + if (activeTab === 'finance' && !hasFinancePermission) { + const fallbackTab = filteredTabItems[0]?.key || 'tasks-list'; + setActiveTab(fallbackTab); + navigate({ + pathname: location.pathname, + search: new URLSearchParams({ + tab: fallbackTab, + pinned_tab: pinnedTab, + }).toString(), + }, { replace: true }); + } + }, [activeTab, hasFinancePermission, filteredTabItems, navigate, location.pathname, pinnedTab]); + + // Update active tab if default tab changes due to permission changes + useEffect(() => { + if (activeTab !== defaultTab && !filteredTabItems.some(item => item.key === activeTab)) { + setActiveTab(defaultTab); + } + }, [defaultTab, activeTab, filteredTabItems]); + const pinToDefaultTab = useCallback(async (itemKey: string) => { if (!itemKey || !projectId) return; @@ -136,7 +186,7 @@ const ProjectView = () => { }); }, [dispatch, location.pathname, navigate, pinnedTab]); - const tabMenuItems = useMemo(() => tabItems.map(item => ({ + const tabMenuItems = useMemo(() => filteredTabItems.map(item => ({ key: item.key, label: ( @@ -179,7 +229,7 @@ const ProjectView = () => { {item.element} ), - })), [pinnedTab, pinToDefaultTab]); + })), [pinnedTab, pinToDefaultTab, filteredTabItems]); const portalElements = useMemo(() => ( <> diff --git a/worklenz-frontend/src/utils/finance-permissions.ts b/worklenz-frontend/src/utils/finance-permissions.ts index 9214f3e8..19d73f7f 100644 --- a/worklenz-frontend/src/utils/finance-permissions.ts +++ b/worklenz-frontend/src/utils/finance-permissions.ts @@ -27,6 +27,29 @@ export const hasFinanceEditPermission = ( return false; }; +/** + * Checks if the current user has permission to view finance data + * Only project managers, admins, and owners should be able to view the finance tab + */ +export const hasFinanceViewPermission = ( + currentSession: ILocalSession | null, + currentProject?: IProjectViewModel | null +): boolean => { + if (!currentSession) return false; + + // Team owner or admin always have permission + if (currentSession.owner || currentSession.is_admin) { + return true; + } + + // Project manager has permission + if (currentProject?.project_manager?.id === currentSession.team_member_id) { + return true; + } + + return false; +}; + /** * Checks if the current user can edit fixed costs */