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:
chamikaJ
2025-05-30 16:26:16 +05:30
parent aeed75ca31
commit 6a4bf4d672
4 changed files with 145 additions and 28 deletions

View File

@@ -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>

View File

@@ -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"

View File

@@ -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 />}

View 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);
};