Merge pull request #145 from shancds/feature/project-finance
Feature/project finance
This commit is contained in:
@@ -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."
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user