feat(finance-permissions): implement permission checks for finance data editing
- Added permission checks for editing finance data, including fixed costs and rate cards. - Introduced utility functions to determine user permissions based on roles (admin, project manager). - Updated finance and rate card components to conditionally render UI elements based on user permissions. - Displayed alerts for users with limited access to inform them of their editing capabilities.
This commit is contained in:
@@ -22,6 +22,8 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { setSelectedTaskId, setShowTaskDrawer, fetchTask } from '@/features/task-drawer/task-drawer.slice';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { parseTimeToSeconds } from '@/utils/timeUtils';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { canEditFixedCost } from '@/utils/finance-permissions';
|
||||
import './finance-table.css';
|
||||
|
||||
type FinanceTableProps = {
|
||||
@@ -48,6 +50,12 @@ const FinanceTable = ({
|
||||
const taskGroups = useAppSelector((state) => state.projectFinances.taskGroups);
|
||||
const activeGroup = useAppSelector((state) => state.projectFinances.activeGroup);
|
||||
|
||||
// Auth and permissions
|
||||
const auth = useAuthService();
|
||||
const currentSession = auth.getCurrentSession();
|
||||
const { project } = useAppSelector((state) => state.projectReducer);
|
||||
const hasEditPermission = canEditFixedCost(currentSession, project);
|
||||
|
||||
// Update local state when table.tasks or Redux store changes
|
||||
useEffect(() => {
|
||||
const updatedGroup = taskGroups.find(g => g.group_id === table.group_id);
|
||||
@@ -110,6 +118,8 @@ const FinanceTable = ({
|
||||
return <Typography.Text>{formattedTotals.total_time_logged}</Typography.Text>;
|
||||
case FinanceTableColumnKeys.ESTIMATED_COST:
|
||||
return <Typography.Text>{formatNumber(formattedTotals.estimated_cost)}</Typography.Text>;
|
||||
case FinanceTableColumnKeys.COST:
|
||||
return <Typography.Text>{formatNumber(formattedTotals.estimated_cost)}</Typography.Text>;
|
||||
case FinanceTableColumnKeys.FIXED_COST:
|
||||
return <Typography.Text>{formatNumber(formattedTotals.fixed_cost)}</Typography.Text>;
|
||||
case FinanceTableColumnKeys.TOTAL_BUDGET:
|
||||
@@ -269,7 +279,7 @@ const FinanceTable = ({
|
||||
case FinanceTableColumnKeys.ESTIMATED_COST:
|
||||
return <Typography.Text>{formatNumber(task.estimated_cost)}</Typography.Text>;
|
||||
case FinanceTableColumnKeys.FIXED_COST:
|
||||
return selectedTask?.id === task.id ? (
|
||||
return selectedTask?.id === task.id && hasEditPermission ? (
|
||||
<InputNumber
|
||||
value={editingFixedCostValue !== null ? editingFixedCostValue : task.fixed_cost}
|
||||
onChange={(value) => {
|
||||
@@ -295,12 +305,17 @@ const FinanceTable = ({
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{ cursor: 'pointer', width: '100%', display: 'block' }}
|
||||
onClick={(e) => {
|
||||
style={{
|
||||
cursor: hasEditPermission ? 'pointer' : 'default',
|
||||
width: '100%',
|
||||
display: 'block',
|
||||
opacity: hasEditPermission ? 1 : 0.7
|
||||
}}
|
||||
onClick={hasEditPermission ? (e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedTask(task);
|
||||
setEditingFixedCostValue(task.fixed_cost);
|
||||
}}
|
||||
} : undefined}
|
||||
>
|
||||
{formatNumber(task.fixed_cost)}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, ConfigProvider, Flex, Select, Typography, message } from 'antd';
|
||||
import { Button, ConfigProvider, Flex, Select, Typography, message, Alert } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -12,6 +12,8 @@ import { RootState } from '@/app/store';
|
||||
import FinanceTableWrapper from './finance-tab/finance-table/finance-table-wrapper';
|
||||
import RatecardTable from './ratecard-tab/reatecard-table/ratecard-table';
|
||||
import ImportRatecardsDrawer from '@/features/finance/ratecard-drawer/import-ratecards-drawer';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { hasFinanceEditPermission } from '@/utils/finance-permissions';
|
||||
|
||||
const ProjectViewFinance = () => {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
@@ -23,6 +25,11 @@ const ProjectViewFinance = () => {
|
||||
const { refreshTimestamp, project } = useAppSelector((state: RootState) => state.projectReducer);
|
||||
const phaseList = useAppSelector((state) => state.phaseReducer.phaseList);
|
||||
|
||||
// Auth and permissions
|
||||
const auth = useAuthService();
|
||||
const currentSession = auth.getCurrentSession();
|
||||
const hasEditPermission = hasFinanceEditPermission(currentSession, project);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
dispatch(fetchProjectFinances({ projectId, groupBy: activeGroup }));
|
||||
@@ -146,10 +153,28 @@ const ProjectViewFinance = () => {
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'finance' ? (
|
||||
<div>
|
||||
{!hasEditPermission && (
|
||||
<Alert
|
||||
message="Limited Access"
|
||||
description="You can view finance data but cannot edit fixed costs. Only project managers, team admins, and team owners can make changes."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
<FinanceTableWrapper activeTablesList={taskGroups} loading={loading} />
|
||||
</div>
|
||||
) : (
|
||||
<Flex vertical gap={8}>
|
||||
{!hasEditPermission && (
|
||||
<Alert
|
||||
message="Limited Access"
|
||||
description="You can view rate card data but cannot edit rates or manage member assignments. Only project managers, team admins, and team owners can make changes."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
<RatecardTable />
|
||||
<Typography.Text
|
||||
type="danger"
|
||||
|
||||
@@ -19,6 +19,8 @@ import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.se
|
||||
import RateCardAssigneeSelector from '@/components/project-ratecard/ratecard-assignee-selector';
|
||||
import { projectsApiService } from '@/api/projects/projects.api.service';
|
||||
import { IProjectMemberViewModel } from '@/types/projectMember.types';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { canEditRateCard, canAddMembersToRateCard } from '@/utils/finance-permissions';
|
||||
import { parse } from 'path';
|
||||
|
||||
const RatecardTable: React.FC = () => {
|
||||
@@ -31,6 +33,14 @@ const RatecardTable: React.FC = () => {
|
||||
const isLoading = useAppSelector((state) => state.projectFinanceRateCard.isLoading);
|
||||
const currency = useAppSelector((state) => state.financeReducer.currency).toUpperCase();
|
||||
const rateInputRefs = React.useRef<Array<HTMLInputElement | null>>([]);
|
||||
|
||||
// Auth and permissions
|
||||
const auth = useAuthService();
|
||||
const currentSession = auth.getCurrentSession();
|
||||
const { project } = useAppSelector((state) => state.projectReducer);
|
||||
const hasEditPermission = canEditRateCard(currentSession, project);
|
||||
const canAddMembers = canAddMembersToRateCard(currentSession, project);
|
||||
|
||||
// Local state for editing
|
||||
const [roles, setRoles] = useState<JobRoleType[]>(rolesRedux);
|
||||
const [addingRow, setAddingRow] = useState<boolean>(false);
|
||||
@@ -238,6 +248,7 @@ const RatecardTable: React.FC = () => {
|
||||
type="number"
|
||||
value={roles[index]?.rate ?? 0}
|
||||
min={0}
|
||||
disabled={!hasEditPermission}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
@@ -245,10 +256,12 @@ const RatecardTable: React.FC = () => {
|
||||
padding: 0,
|
||||
width: 80,
|
||||
textAlign: 'right',
|
||||
opacity: hasEditPermission ? 1 : 0.7,
|
||||
cursor: hasEditPermission ? 'text' : 'not-allowed'
|
||||
}}
|
||||
onChange={(e) => handleRateChange(e.target.value, index)}
|
||||
onBlur={(e) => handleRateBlur(e.target.value, index)}
|
||||
onPressEnter={(e) => handleRateBlur(e.target.value, index)}
|
||||
onChange={hasEditPermission ? (e) => handleRateChange(e.target.value, index) : undefined}
|
||||
onBlur={hasEditPermission ? (e) => handleRateBlur(e.target.value, index) : undefined}
|
||||
onPressEnter={hasEditPermission ? (e) => handleRateBlur(e.target.value, index) : undefined}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -265,6 +278,7 @@ const RatecardTable: React.FC = () => {
|
||||
) : null;
|
||||
})}
|
||||
</Avatar.Group>
|
||||
{canAddMembers && (
|
||||
<div>
|
||||
<RateCardAssigneeSelector
|
||||
projectId={projectId as string}
|
||||
@@ -274,6 +288,7 @@ const RatecardTable: React.FC = () => {
|
||||
assignedMembers={assignedMembers} // Pass assigned members here
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -282,6 +297,7 @@ const RatecardTable: React.FC = () => {
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
render: (_: any, record: JobRoleType, index: number) => (
|
||||
hasEditPermission ? (
|
||||
<Popconfirm
|
||||
title={t('deleteConfirm')}
|
||||
onConfirm={() => handleDelete(record, index)}
|
||||
@@ -290,6 +306,7 @@ const RatecardTable: React.FC = () => {
|
||||
>
|
||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
) : null
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -315,9 +332,11 @@ const RatecardTable: React.FC = () => {
|
||||
loading={isLoading || isLoadingMembers}
|
||||
footer={() => (
|
||||
<Flex gap={0}>
|
||||
{hasEditPermission && (
|
||||
<Button type="dashed" onClick={handleAddRole} style={{ width: 'fit-content' }}>
|
||||
{t('addRoleButton')}
|
||||
</Button>
|
||||
)}
|
||||
{/* <Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
|
||||
58
worklenz-frontend/src/utils/finance-permissions.ts
Normal file
58
worklenz-frontend/src/utils/finance-permissions.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ILocalSession } from '@/types/auth/local-session.types';
|
||||
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
|
||||
|
||||
/**
|
||||
* Checks if the current user has permission to edit finance data
|
||||
* Only users with project admin, admin or owner roles should be able to:
|
||||
* - Change fixed cost values
|
||||
* - Add members to rate cards
|
||||
* - Change rate per hour values
|
||||
*/
|
||||
export const hasFinanceEditPermission = (
|
||||
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
|
||||
*/
|
||||
export const canEditFixedCost = (
|
||||
currentSession: ILocalSession | null,
|
||||
currentProject?: IProjectViewModel | null
|
||||
): boolean => {
|
||||
return hasFinanceEditPermission(currentSession, currentProject);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the current user can edit rate card data
|
||||
*/
|
||||
export const canEditRateCard = (
|
||||
currentSession: ILocalSession | null,
|
||||
currentProject?: IProjectViewModel | null
|
||||
): boolean => {
|
||||
return hasFinanceEditPermission(currentSession, currentProject);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the current user can add members to rate cards
|
||||
*/
|
||||
export const canAddMembersToRateCard = (
|
||||
currentSession: ILocalSession | null,
|
||||
currentProject?: IProjectViewModel | null
|
||||
): boolean => {
|
||||
return hasFinanceEditPermission(currentSession, currentProject);
|
||||
};
|
||||
Reference in New Issue
Block a user