feat: Add Ratecard management functionality with localization support

This commit is contained in:
shancds
2025-05-18 22:28:46 +05:30
parent 6847eec603
commit 2b3b0ba635
9 changed files with 653 additions and 1 deletions

View File

@@ -0,0 +1,163 @@
[
{
"id": "c2669c5f-a019-445b-b703-b941bbefdab7",
"type": "low",
"name": "Low",
"color_code": "#c2e4d0",
"color_code_dark": "#46d980",
"tasks": [
{
"id": "4be5ef5c-1234-4247-b159-6d8df2b37d04",
"task": "Testing and QA",
"isBillable": false,
"hours": 180,
"cost": 18000,
"fixedCost": 2500,
"totalBudget": 20000,
"totalActual": 21000,
"variance": -1000,
"members": [
{
"memberId": "6",
"name": "Eve Adams",
"jobId": "J006",
"jobRole": "QA Engineer",
"hourlyRate": 100
}
]
},
{
"id": "6be5ef5c-1234-4247-b159-6d8df2b37d06",
"task": "Project Documentation",
"isBillable": false,
"hours": 100,
"cost": 10000,
"fixedCost": 1000,
"totalBudget": 12000,
"totalActual": 12500,
"variance": -500,
"members": [
{
"memberId": "8",
"name": "Grace Lee",
"jobId": "J008",
"jobRole": "Technical Writer",
"hourlyRate": 100
}
]
}
]
},
{
"id": "d3f9c5f1-b019-445b-b703-b941bbefdab8",
"type": "medium",
"name": "Medium",
"color_code": "#f9e3b1",
"color_code_dark": "#ffc227",
"tasks": [
{
"id": "1be5ef5c-1234-4247-b159-6d8df2b37d01",
"task": "UI Design",
"isBillable": true,
"hours": 120,
"cost": 12000,
"fixedCost": 1500,
"totalBudget": 14000,
"totalActual": 13500,
"variance": 500,
"members": [
{
"memberId": "1",
"name": "John Doe",
"jobId": "J001",
"jobRole": "UI/UX Designer",
"hourlyRate": 100
},
{
"memberId": "2",
"name": "Jane Smith",
"jobId": "J002",
"jobRole": "Frontend Developer",
"hourlyRate": 120
}
]
},
{
"id": "2be5ef5c-1234-4247-b159-6d8df2b37d02",
"task": "API Integration",
"isBillable": true,
"hours": 200,
"cost": 20000,
"fixedCost": 3000,
"totalBudget": 25000,
"totalActual": 26000,
"variance": -1000,
"members": [
{
"memberId": "3",
"name": "Alice Johnson",
"jobId": "J003",
"jobRole": "Backend Developer",
"hourlyRate": 100
}
]
}
]
},
{
"id": "e3f9c5f1-b019-445b-b703-b941bbefdab9",
"type": "high",
"name": "High",
"color_code": "#f6bfc0",
"color_code_dark": "#ff4141",
"tasks": [
{
"id": "5be5ef5c-1234-4247-b159-6d8df2b37d05",
"task": "Database Migration",
"isBillable": true,
"hours": 250,
"cost": 37500,
"fixedCost": 4000,
"totalBudget": 42000,
"totalActual": 41000,
"variance": 1000,
"members": [
{
"memberId": "7",
"name": "Frank Harris",
"jobId": "J007",
"jobRole": "Database Administrator",
"hourlyRate": 150
}
]
},
{
"id": "3be5ef5c-1234-4247-b159-6d8df2b37d03",
"task": "Performance Optimization",
"isBillable": true,
"hours": 300,
"cost": 45000,
"fixedCost": 5000,
"totalBudget": 50000,
"totalActual": 47000,
"variance": 3000,
"members": [
{
"memberId": "4",
"name": "Bob Brown",
"jobId": "J004",
"jobRole": "Performance Engineer",
"hourlyRate": 150
},
{
"memberId": "5",
"name": "Charlie Davis",
"jobId": "J005",
"jobRole": "Full Stack Developer",
"hourlyRate": 130
}
]
}
]
}
]

View File

@@ -0,0 +1,163 @@
[
{
"id": "c2669c5f-a019-445b-b703-b941bbefdab7",
"type": "todo",
"name": "To Do",
"color_code": "#d8d7d8",
"color_code_dark": "#989898",
"tasks": [
{
"id": "1be5ef5c-1234-4247-b159-6d8df2b37d01",
"task": "UI Design",
"isBillable": true,
"hours": 120,
"cost": 12000,
"fixedCost": 1500,
"totalBudget": 14000,
"totalActual": 13500,
"variance": 500,
"members": [
{
"memberId": "1",
"name": "John Doe",
"jobId": "J001",
"jobRole": "UI/UX Designer",
"hourlyRate": 100
},
{
"memberId": "2",
"name": "Jane Smith",
"jobId": "J002",
"jobRole": "Frontend Developer",
"hourlyRate": 120
}
]
},
{
"id": "2be5ef5c-1234-4247-b159-6d8df2b37d02",
"task": "API Integration",
"isBillable": true,
"hours": 200,
"cost": 20000,
"fixedCost": 3000,
"totalBudget": 25000,
"totalActual": 26000,
"variance": -1000,
"members": [
{
"memberId": "3",
"name": "Alice Johnson",
"jobId": "J003",
"jobRole": "Backend Developer",
"hourlyRate": 100
}
]
}
]
},
{
"id": "d3f9c5f1-b019-445b-b703-b941bbefdab8",
"type": "doing",
"name": "In Progress",
"color_code": "#c0d5f6",
"color_code_dark": "#4190ff",
"tasks": [
{
"id": "3be5ef5c-1234-4247-b159-6d8df2b37d03",
"task": "Performance Optimization",
"isBillable": true,
"hours": 300,
"cost": 45000,
"fixedCost": 5000,
"totalBudget": 50000,
"totalActual": 47000,
"variance": 3000,
"members": [
{
"memberId": "4",
"name": "Bob Brown",
"jobId": "J004",
"jobRole": "Performance Engineer",
"hourlyRate": 150
},
{
"memberId": "5",
"name": "Charlie Davis",
"jobId": "J005",
"jobRole": "Full Stack Developer",
"hourlyRate": 130
}
]
},
{
"id": "4be5ef5c-1234-4247-b159-6d8df2b37d04",
"task": "Testing and QA",
"isBillable": false,
"hours": 180,
"cost": 18000,
"fixedCost": 2500,
"totalBudget": 20000,
"totalActual": 21000,
"variance": -1000,
"members": [
{
"memberId": "6",
"name": "Eve Adams",
"jobId": "J006",
"jobRole": "QA Engineer",
"hourlyRate": 100
}
]
}
]
},
{
"id": "e3f9c5f1-b019-445b-b703-b941bbefdab9",
"type": "done",
"name": "Done",
"color_code": "#c2e4d0",
"color_code_dark": "#46d980",
"tasks": [
{
"id": "5be5ef5c-1234-4247-b159-6d8df2b37d05",
"task": "Database Migration",
"isBillable": true,
"hours": 250,
"cost": 37500,
"fixedCost": 4000,
"totalBudget": 42000,
"totalActual": 41000,
"variance": 1000,
"members": [
{
"memberId": "7",
"name": "Frank Harris",
"jobId": "J007",
"jobRole": "Database Administrator",
"hourlyRate": 150
}
]
},
{
"id": "6be5ef5c-1234-4247-b159-6d8df2b37d06",
"task": "Project Documentation",
"isBillable": false,
"hours": 100,
"cost": 10000,
"fixedCost": 1000,
"totalBudget": 12000,
"totalActual": 12500,
"variance": -500,
"members": [
{
"memberId": "8",
"name": "Grace Lee",
"jobId": "J008",
"jobRole": "Technical Writer",
"hourlyRate": 100
}
]
}
]
}
]

View File

@@ -0,0 +1,51 @@
[
{
"ratecardId": "RC001",
"ratecardName": "Rate Card 1",
"jobRolesList": [
{
"jobId": "J001",
"jobTitle": "Project Manager",
"ratePerHour": 100
},
{
"jobId": "J002",
"jobTitle": "Senior Software Engineer",
"ratePerHour": 120
},
{
"jobId": "J003",
"jobTitle": "Junior Software Engineer",
"ratePerHour": 80
},
{
"jobId": "J004",
"jobTitle": "UI/UX Designer",
"ratePerHour": 50
}
],
"createdDate": "2024-12-01T00:00:00.000Z"
},
{
"ratecardId": "RC002",
"ratecardName": "Rate Card 2",
"jobRolesList": [
{
"jobId": "J001",
"jobTitle": "Project Manager",
"ratePerHour": 80
},
{
"jobId": "J002",
"jobTitle": "Senior Software Engineer",
"ratePerHour": 100
},
{
"jobId": "J003",
"jobTitle": "Junior Software Engineer",
"ratePerHour": 60
}
],
"createdDate": "2024-12-15T00:00:00.000Z"
}
]

View File

@@ -0,0 +1,20 @@
{
"nameColumn": "Name",
"createdColumn": "Created",
"noProjectsAvailable": "No projects available",
"deleteConfirmationTitle": "Are you sure?",
"deleteConfirmationOk": "Yes",
"deleteConfirmationCancel": "Cancel",
"searchPlaceholder": "Search by name",
"createRatecard": "Create Rate Card",
"jobTitleColumn": "Job title",
"ratePerHourColumn": "Rate per hour",
"saveButton": "Save",
"addRoleButton": "+ Add Role",
"createRatecardSuccessMessage": "Create Rate Card success!",
"createRatecardErrorMessage": "Create Rate Card failed!",
"updateRatecardSuccessMessage": "Update Rate Card success!",
"updateRatecardErrorMessage": "Update Rate Card failed!",
"currency": "Currency"
}

View File

@@ -0,0 +1,20 @@
{
"nameColumn": "Nombre",
"createdColumn": "Creado",
"noProjectsAvailable": "No hay proyectos disponibles",
"deleteConfirmationTitle": "¿Estás seguro?",
"deleteConfirmationOk": "Sí",
"deleteConfirmationCancel": "Cancelar",
"searchPlaceholder": "Buscar por nombre",
"createRatecard": "Crear Tarifa",
"jobTitleColumn": "Puesto de trabajo",
"ratePerHourColumn": "Tarifa por hora",
"saveButton": "Guardar",
"addRoleButton": "+ Agregar Rol",
"createRatecardSuccessMessage": "¡Tarifa creada con éxito!",
"createRatecardErrorMessage": "¡Error al crear la tarifa!",
"updateRatecardSuccessMessage": "¡Tarifa actualizada con éxito!",
"updateRatecardErrorMessage": "¡Error al actualizar la tarifa!",
"currency": "Moneda"
}

View File

@@ -0,0 +1,20 @@
{
"nameColumn": "Nome",
"createdColumn": "Criado",
"noProjectsAvailable": "Nenhum projeto disponível",
"deleteConfirmationTitle": "Tem certeza?",
"deleteConfirmationOk": "Sim",
"deleteConfirmationCancel": "Cancelar",
"searchPlaceholder": "Pesquisar por nome",
"createRatecard": "Criar Tabela de Preços",
"jobTitleColumn": "Cargo",
"ratePerHourColumn": "Taxa por hora",
"saveButton": "Salvar",
"addRoleButton": "+ Adicionar Função",
"createRatecardSuccessMessage": "Tabela de Preços criada com sucesso!",
"createRatecardErrorMessage": "Falha ao criar Tabela de Preços!",
"updateRatecardSuccessMessage": "Tabela de Preços atualizada com sucesso!",
"updateRatecardErrorMessage": "Falha ao atualizar Tabela de Preços!",
"currency": "Moeda"
}

View File

@@ -20,7 +20,7 @@ const RatecardDrawer = ({
const [roles, setRoles] = useState<JobType[]>([]); const [roles, setRoles] = useState<JobType[]>([]);
// localization // localization
const { t } = useTranslation('ratecard-settings'); const { t } = useTranslation('settings/ratecard-settings');
// get drawer state from client reducer // get drawer state from client reducer
const isDrawerOpen = useAppSelector( const isDrawerOpen = useAppSelector(

View File

@@ -1,5 +1,6 @@
import { import {
BankOutlined, BankOutlined,
DollarCircleOutlined,
FileZipOutlined, FileZipOutlined,
GlobalOutlined, GlobalOutlined,
GroupOutlined, GroupOutlined,
@@ -25,6 +26,7 @@ import TeamMembersSettings from '@/pages/settings/team-members/team-members-sett
import TeamsSettings from '../../pages/settings/teams/teams-settings'; import TeamsSettings from '../../pages/settings/teams/teams-settings';
import ChangePassword from '@/pages/settings/change-password/change-password'; import ChangePassword from '@/pages/settings/change-password/change-password';
import LanguageAndRegionSettings from '@/pages/settings/language-and-region/language-and-region-settings'; import LanguageAndRegionSettings from '@/pages/settings/language-and-region/language-and-region-settings';
import RatecardSettings from '@/pages/settings/ratecard/ratecard-settings';
// type of menu item in settings sidebar // type of menu item in settings sidebar
type SettingMenuItems = { type SettingMenuItems = {
@@ -123,6 +125,13 @@ export const settingsItems: SettingMenuItems[] = [
element: React.createElement(TeamMembersSettings), element: React.createElement(TeamMembersSettings),
adminOnly: true, adminOnly: true,
}, },
{
key: 'ratecard',
name: 'Ratecard',
endpoint: 'ratecard',
icon: React.createElement(DollarCircleOutlined),
element: React.createElement(RatecardSettings),
},
{ {
key: 'teams', key: 'teams',
name: 'teams', name: 'teams',

View File

@@ -0,0 +1,206 @@
import {
DeleteOutlined,
EditOutlined,
ExclamationCircleFilled,
SearchOutlined,
} from '@ant-design/icons';
import {
Button,
Card,
Flex,
Input,
Popconfirm,
Table,
TableProps,
Tooltip,
Typography,
} from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import { colors } from '../../../styles/colors';
import { useAppDispatch } from '../../../hooks/useAppDispatch';
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '../../../hooks/useDoumentTItle';
import { durationDateFormat } from '../../../utils/durationDateFormat';
import { toggleRatecardDrawer } from '../../../features/finance/finance-slice';
import RatecardDrawer from '../../../features/finance/ratecard-drawer/ratecard-drawer';
import { fetchData } from '../../../utils/fetchData';
import { RatecardType } from '@/types/project/ratecard.types';
const RatecardSettings = () => {
const [ratecardsList, setRatecardsList] = useState<RatecardType[]>([]);
// get currently selected ratecard id
const [selectedRatecardId, setSelectedRatecardId] = useState<string | null>(
null
);
const [ratecardDrawerType, setRatecardDrawerType] = useState<
'create' | 'update'
>('create');
// localization
const { t } = useTranslation('/settings/ratecard-settings');
useDocumentTitle('Manage Rate Cards');
// Fetch rate cards data
useEffect(() => {
fetchData('/finance-mock-data/ratecards-data.json', setRatecardsList);
}, []);
const dispatch = useAppDispatch();
// this is for get the current string that type on search bar
const [searchQuery, setSearchQuery] = useState<string>('');
// used useMemo hook for re render the list when searching
const filteredRatecardsData = useMemo(() => {
return ratecardsList.filter((item) =>
item.ratecardName.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [ratecardsList, searchQuery]);
// function to create ratecard
const onRatecardCreate = () => {
setRatecardDrawerType('create');
dispatch(toggleRatecardDrawer());
};
// function to update a ratecard
const onRatecardUpdate = (id: string) => {
setRatecardDrawerType('update');
setSelectedRatecardId(id);
dispatch(toggleRatecardDrawer());
};
// table columns
const columns: TableProps['columns'] = [
{
key: 'rateName',
title: t('nameColumn'),
onCell: (record) => ({
onClick: () => {
setSelectedRatecardId(record.ratecardId);
// dispatch(toggleUpdateRateDrawer());
},
}),
render: (record) => (
<Typography.Text className="group-hover:text-[#1890ff]">
{record.ratecardName}
</Typography.Text>
),
},
{
key: 'created',
title: t('createdColumn'),
onCell: (record) => ({
onClick: () => {
setSelectedRatecardId(record.ratecardId);
// dispatch(toggleUpdateRateDrawer());
},
}),
render: (record) => (
<Typography.Text>
{durationDateFormat(record.createdDate)}
</Typography.Text>
),
},
{
key: 'actionBtns',
width: 80,
render: (record) => (
<Flex
gap={8}
style={{ padding: 0 }}
className="hidden group-hover:block"
>
<Tooltip title="Edit">
<Button
size="small"
icon={<EditOutlined />}
onClick={() => {
onRatecardUpdate(record.ratecardId);
}}
/>
</Tooltip>
<Popconfirm
title={t('deleteConfirmationTitle')}
icon={
<ExclamationCircleFilled
style={{ color: colors.vibrantOrange }}
/>
}
okText={t('deleteConfirmationOk')}
cancelText={t('deleteConfirmationCancel')}
// onConfirm={() => dispatch(deleteRatecard(record.ratecardId))}
>
<Tooltip title="Delete">
<Button
shape="default"
icon={<DeleteOutlined />}
size="small"
style={{ marginInlineStart: 8 }}
/>
</Tooltip>
</Popconfirm>
</Flex>
),
},
];
return (
<Card
style={{ width: '100%' }}
title={
<Flex justify="flex-end">
<Flex
gap={8}
align="center"
justify="flex-end"
style={{ width: '100%', maxWidth: 400 }}
>
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.currentTarget.value)}
placeholder={t('searchPlaceholder')}
style={{ maxWidth: 232 }}
suffix={<SearchOutlined />}
/>
<Button type="primary" onClick={onRatecardCreate}>
{t('createRatecard')}
</Button>
</Flex>
</Flex>
}
>
<Table
className="custom-two-colors-row-table"
dataSource={filteredRatecardsData}
columns={columns}
rowKey={(record) => record.rateId}
pagination={{
showSizeChanger: true,
defaultPageSize: 20,
pageSizeOptions: ['5', '10', '15', '20', '50', '100'],
size: 'small',
}}
onRow={(record) => {
return {
className: 'group',
style: {
cursor: 'pointer',
height: 36,
},
};
}}
/>
{/* rate drawers */}
<RatecardDrawer
type={ratecardDrawerType}
ratecardId={selectedRatecardId || ''}
/>
</Card>
);
};
export default RatecardSettings;