Merge pull request #145 from shancds/feature/project-finance

Feature/project finance
This commit is contained in:
Chamika J
2025-05-28 13:02:34 +05:30
committed by GitHub
5 changed files with 180 additions and 149 deletions

View File

@@ -35,7 +35,8 @@
"ratecardsPluralText": "Rate Card Templates", "ratecardsPluralText": "Rate Card Templates",
"deleteConfirm": "Are you sure ?", "deleteConfirm": "Are you sure ?",
"yes": "Yes", "yes": "Yes",
"no": "No" "no": "No",
"alreadyImportedRateCardMessage": "A rate card has already been imported. Clear all imported rate cards to add a new one."
} }

View File

@@ -23,7 +23,8 @@ const RateCardAssigneeSelector = ({
onChange, onChange,
selectedMemberIds = [], selectedMemberIds = [],
memberlist = [], memberlist = [],
}: RateCardAssigneeSelectorProps) => { assignedMembers = [], // New prop: List of all assigned member IDs across all job titles
}: RateCardAssigneeSelectorProps & { assignedMembers: string[] }) => {
const membersInputRef = useRef<InputRef>(null); const membersInputRef = useRef<InputRef>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [members, setMembers] = useState<IProjectMemberViewModel[]>(memberlist); const [members, setMembers] = useState<IProjectMemberViewModel[]>(memberlist);
@@ -46,33 +47,39 @@ const RateCardAssigneeSelector = ({
/> />
<List style={{ padding: 0, maxHeight: 200, overflow: 'auto' }}> <List style={{ padding: 0, maxHeight: 200, overflow: 'auto' }}>
{filteredMembers.length ? ( {filteredMembers.length ? (
filteredMembers.map((member) => ( filteredMembers.map((member) => {
<List.Item const isAssignedToAnotherJobTitle =
key={member.id} assignedMembers.includes(member.id || '') &&
style={{ !selectedMemberIds.includes(member.id || ''); // Check if the member is assigned elsewhere
display: 'flex',
gap: 8, return (
alignItems: 'center', <List.Item
padding: '4px 8px', key={member.id}
border: 'none', style={{
opacity: member.pending_invitation ? 0.5 : 1, display: 'flex',
justifyContent: 'flex-start', gap: 8,
textAlign: 'left', alignItems: 'center',
}} padding: '4px 8px',
> border: 'none',
<Checkbox opacity: member.pending_invitation || isAssignedToAnotherJobTitle ? 0.5 : 1,
checked={selectedMemberIds.includes(member.id || '')} justifyContent: 'flex-start',
disabled={member.pending_invitation} textAlign: 'left',
onChange={() => onChange?.(member.id || '')} }}
/> >
<SingleAvatar <Checkbox
avatarUrl={member.avatar_url} checked={selectedMemberIds.includes(member.id || '')}
name={member.name} disabled={member.pending_invitation || isAssignedToAnotherJobTitle}
email={member.email} onChange={() => onChange?.(member.id || '')}
/> />
<span>{member.name}</span> <SingleAvatar
</List.Item> avatarUrl={member.avatar_url}
)) name={member.name}
email={member.email}
/>
<span>{member.name}</span>
</List.Item>
);
})
) : ( ) : (
<Empty description="No members found" /> <Empty description="No members found" />
)} )}

View File

@@ -1,4 +1,4 @@
import { Drawer, Typography, Button, Table, Menu, Flex, Spin } from 'antd'; import { Drawer, Typography, Button, Table, Menu, Flex, Spin, Alert } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAppSelector } from '../../../hooks/useAppSelector'; import { useAppSelector } from '../../../hooks/useAppSelector';
@@ -85,32 +85,45 @@ const ImportRatecardsDrawer: React.FC = () => {
} }
footer={ footer={
<div style={{ textAlign: 'right' }}> <div style={{ textAlign: 'right' }}>
<Button {/* Alert message */}
type="primary" {rolesRedux.length !== 0 ? (
disabled={rolesRedux.length !== 0} <div style={{ textAlign: 'right' }}>
onClick={() => { <Alert
if (!projectId) { message={t('alreadyImportedRateCardMessage') || 'A rate card has already been imported. Clear all imported rate cards to add a new one.'}
// Handle missing project id (show error, etc.) type="warning"
return; showIcon
} style={{ marginBottom: 16 }}
if (drawerRatecard?.jobRolesList?.length) { />
dispatch( </div>
insertProjectRateCardRoles({ ) : (
project_id: projectId, <div style={{ textAlign: 'right' }}>
roles: drawerRatecard.jobRolesList <Button
.filter((role) => typeof role.rate !== 'undefined') type="primary"
.map((role) => ({ onClick={() => {
...role, if (!projectId) {
rate: Number(role.rate), // Handle missing project id (show error, etc.)
})), return;
}) }
); if (drawerRatecard?.jobRolesList?.length) {
} dispatch(
dispatch(toggleImportRatecardsDrawer()); insertProjectRateCardRoles({
}} project_id: projectId,
> roles: drawerRatecard.jobRolesList
{t('import')} .filter((role) => typeof role.rate !== 'undefined')
</Button> .map((role) => ({
...role,
rate: Number(role.rate),
})),
})
);
}
dispatch(toggleImportRatecardsDrawer());
}}
>
{t('import')}
</Button>
</div>
)}
</div> </div>
} }
open={isDrawerOpen} open={isDrawerOpen}

View File

@@ -307,16 +307,21 @@ const RatecardDrawer = ({
}, },
]; ];
const handleDrawerClose = () => { const handleDrawerClose = async() => {
if (!name || name.trim() === '' || name === 'Untitled Rate Card') { if (!name || name.trim() === '') {
messageApi.open({ messageApi.open({
type: 'warning', type: 'warning',
content: t('ratecardNameRequired') || 'Rate card name is required.', content: t('ratecardNameRequired') || 'Rate card name is required.',
}); });
return; return;
} else if (hasChanges) { } else if (hasChanges) {
setShowUnsavedAlert(true); setShowUnsavedAlert(true);
} else { }
else if (name === 'Untitled Rate Card' && roles.length === 0){
await dispatch(deleteRateCard(ratecardId));
dispatch(toggleRatecardDrawer());
}
else {
dispatch(toggleRatecardDrawer()); dispatch(toggleRatecardDrawer());
} }
}; };
@@ -339,93 +344,93 @@ const RatecardDrawer = ({
return ( return (
<> <>
{contextHolder} {contextHolder}
<Drawer <Drawer
loading={drawerLoading} loading={drawerLoading}
onClose={handleDrawerClose} onClose={handleDrawerClose}
title={ title={
<Flex align="center" justify="space-between"> <Flex align="center" justify="space-between">
<Typography.Text style={{ fontWeight: 500, fontSize: 16 }}> <Typography.Text style={{ fontWeight: 500, fontSize: 16 }}>
<Input <Input
value={name} value={name}
placeholder="Enter rate card name" placeholder="Enter rate card name"
style={{ style={{
fontWeight: 500, fontWeight: 500,
fontSize: 16, fontSize: 16,
background: 'transparent', background: 'transparent',
border: 'none', border: 'none',
boxShadow: 'none', boxShadow: 'none',
padding: 0, padding: 0,
}} }}
onChange={e => { onChange={e => {
setName(e.target.value); setName(e.target.value);
}} }}
/> />
</Typography.Text> </Typography.Text>
<Flex gap={8} align="center"> <Flex gap={8} align="center">
<Typography.Text>{t('currency')}</Typography.Text> <Typography.Text>{t('currency')}</Typography.Text>
<Select <Select
value={currency} value={currency}
options={[ options={[
{ value: 'USD', label: 'USD' }, { value: 'USD', label: 'USD' },
{ value: 'LKR', label: 'LKR' }, { value: 'LKR', label: 'LKR' },
{ value: 'INR', label: 'INR' }, { value: 'INR', label: 'INR' },
]} ]}
onChange={(value) => setCurrency(value)} onChange={(value) => setCurrency(value)}
/> />
<Button onClick={handleAddAllRoles} type="default"> <Button onClick={handleAddAllRoles} type="default">
{t('addAllButton') || 'Add All'} {t('addAllButton') || 'Add All'}
</Button>
</Flex>
</Flex>
}
open={isDrawerOpen}
width={700}
footer={
<Flex justify="end" gap={16} style={{ marginTop: 16 }}>
<Button style={{ marginBottom: 24 }} onClick={handleSave} type="primary" disabled={name === '' || (name === 'Untitled Rate Card' && roles.length === 0)}>
{t('saveButton')}
</Button> </Button>
</Flex> </Flex>
</Flex> }
} >
open={isDrawerOpen} {showUnsavedAlert && (
width={700} <Alert
footer={ message={t('unsavedChangesTitle') || 'Unsaved Changes'}
<Flex justify="end" gap={16} style={{ marginTop: 16 }}> type="warning"
<Button style={{ marginBottom: 24 }} onClick={handleSave} type="primary" disabled={name === '' || (name === 'Untitled Rate Card' && roles.length === 0)}> showIcon
{t('saveButton')} closable
</Button> onClose={() => setShowUnsavedAlert(false)}
</Flex> action={
} <Space direction="horizontal">
> <Button size="small" type="primary" onClick={handleConfirmSave}>
{showUnsavedAlert && ( Save
<Alert </Button>
message={t('unsavedChangesTitle') || 'Unsaved Changes'} <Button size="small" danger onClick={handleConfirmDiscard}>
type="warning" Discard
showIcon </Button>
closable </Space>
onClose={() => setShowUnsavedAlert(false)} }
action={ style={{ marginBottom: 16 }}
<Space direction="horizontal"> />
<Button size="small" type="primary" onClick={handleConfirmSave}>
Save
</Button>
<Button size="small" danger onClick={handleConfirmDiscard}>
Discard
</Button>
</Space>
}
style={{ marginBottom: 16 }}
/>
)}
<Table
dataSource={roles}
columns={columns}
rowKey={(record) => record.job_title_id}
pagination={false}
footer={() => (
<Button
type="dashed"
onClick={handleAddRole}
block
style={{ margin: 0, padding: 0 }}
>
{t('addRoleButton')}
</Button>
)} )}
/> <Table
</Drawer> dataSource={roles}
columns={columns}
rowKey={(record) => record.job_title_id}
pagination={false}
footer={() => (
<Button
type="dashed"
onClick={handleAddRole}
block
style={{ margin: 0, padding: 0 }}
>
{t('addRoleButton')}
</Button>
)}
/>
</Drawer>
</> </>
); );

View File

@@ -191,6 +191,10 @@ const RatecardTable: React.FC = () => {
} }
}; };
const assignedMembers = roles
.flatMap((role) => role.members || [])
.filter((memberId, index, self) => self.indexOf(memberId) === index);
// Columns // Columns
const columns: TableProps<JobRoleType>['columns'] = [ const columns: TableProps<JobRoleType>['columns'] = [
{ {
@@ -267,6 +271,7 @@ const RatecardTable: React.FC = () => {
selectedMemberIds={memberscol || []} selectedMemberIds={memberscol || []}
onChange={(memberId) => handleMemberChange(memberId, index, record)} onChange={(memberId) => handleMemberChange(memberId, index, record)}
memberlist={members} memberlist={members}
assignedMembers={assignedMembers} // Pass assigned members here
/> />
</div> </div>
</div> </div>