import { Drawer, Select, Typography, Flex, Button, Input, Table, Tooltip, Alert, Space, message, Popconfirm, DeleteOutlined, ExclamationCircleFilled, PlusOutlined, } from '@/shared/antd-imports'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { deleteRateCard, fetchRateCardById, fetchRateCards, toggleRatecardDrawer, updateRateCard, } from '@/features/finance/finance-slice'; import { RatecardType, IJobType } from '@/types/project/ratecard.types'; import { IJobTitlesViewModel } from '@/types/job.types'; import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service'; import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service'; import { colors } from '@/styles/colors'; import CreateJobTitlesDrawer from '@/features/settings/job/CreateJobTitlesDrawer'; import { CURRENCY_OPTIONS, DEFAULT_CURRENCY } from '@/shared/currencies'; import { IOrganization } from '@/types/admin-center/admin-center.types'; interface PaginationType { current: number; pageSize: number; field: string; order: string; total: number; pageSizeOptions: string[]; size: 'small' | 'default'; } const RateCardDrawer = ({ type, ratecardId, onSaved, }: { type: 'create' | 'update'; ratecardId: string; onSaved?: () => void; }) => { const [ratecardsList, setRatecardsList] = useState([]); const [roles, setRoles] = useState([]); const [initialRoles, setInitialRoles] = useState([]); const [initialName, setInitialName] = useState('Untitled Rate Card'); const [initialCurrency, setInitialCurrency] = useState(DEFAULT_CURRENCY); const [addingRowIndex, setAddingRowIndex] = useState(null); const [organization, setOrganization] = useState(null); const { t } = useTranslation('settings/ratecard-settings'); const drawerLoading = useAppSelector(state => state.financeReducer.isFinanceDrawerloading); const drawerRatecard = useAppSelector(state => state.financeReducer.drawerRatecard); const isDrawerOpen = useAppSelector(state => state.financeReducer.isRatecardDrawerOpen); const dispatch = useAppDispatch(); const [isAddingRole, setIsAddingRole] = useState(false); const [selectedJobTitleId, setSelectedJobTitleId] = useState(undefined); const [searchQuery, setSearchQuery] = useState(''); const [currency, setCurrency] = useState(DEFAULT_CURRENCY); const [name, setName] = useState('Untitled Rate Card'); const [jobTitles, setJobTitles] = useState({}); const [pagination, setPagination] = useState({ current: 1, pageSize: 10000, field: 'name', order: 'desc', total: 0, pageSizeOptions: ['5', '10', '15', '20', '50', '100'], size: 'small', }); const [editingRowIndex, setEditingRowIndex] = useState(null); const [showUnsavedAlert, setShowUnsavedAlert] = useState(false); const [messageApi, contextHolder] = message.useMessage(); const [isCreatingJobTitle, setIsCreatingJobTitle] = useState(false); const [newJobTitleName, setNewJobTitleName] = useState(''); // Determine if we're using man days calculation method const isManDaysMethod = organization?.calculation_method === 'man_days'; // Detect changes const hasChanges = useMemo(() => { const rolesChanged = JSON.stringify(roles) !== JSON.stringify(initialRoles); const nameChanged = name !== initialName; const currencyChanged = currency !== initialCurrency; return rolesChanged || nameChanged || currencyChanged; }, [roles, name, currency, initialRoles, initialName, initialCurrency]); // Fetch organization details useEffect(() => { const fetchOrganization = async () => { try { const response = await adminCenterApiService.getOrganizationDetails(); if (response.done) { setOrganization(response.body); } } catch (error) { console.error('Failed to fetch organization details:', error); } }; if (isDrawerOpen) { fetchOrganization(); } }, [isDrawerOpen]); const getJobTitles = useMemo(() => { return async () => { const response = await jobTitlesApiService.getJobTitles( pagination.current, pagination.pageSize, pagination.field, pagination.order, searchQuery ); if (response.done) { setJobTitles(response.body); setPagination(prev => ({ ...prev, total: response.body.total || 0 })); } }; }, [pagination.current, pagination.pageSize, pagination.field, pagination.order, searchQuery]); useEffect(() => { getJobTitles(); }, []); const selectedRatecard = ratecardsList.find(ratecard => ratecard.id === ratecardId); useEffect(() => { if (type === 'update' && ratecardId) { dispatch(fetchRateCardById(ratecardId)); } }, [type, ratecardId, dispatch]); useEffect(() => { if (type === 'update' && drawerRatecard) { setRoles(drawerRatecard.jobRolesList || []); setInitialRoles(drawerRatecard.jobRolesList || []); setName(drawerRatecard.name || ''); setInitialName(drawerRatecard.name || ''); setCurrency(drawerRatecard.currency || DEFAULT_CURRENCY); setInitialCurrency(drawerRatecard.currency || DEFAULT_CURRENCY); } }, [drawerRatecard, type]); const handleAddAllRoles = () => { if (!jobTitles.data) return; const existingIds = new Set(roles.map(r => r.job_title_id)); const newRoles = jobTitles.data .filter(jt => jt.id && !existingIds.has(jt.id)) .map(jt => ({ jobtitle: jt.name, rate_card_id: ratecardId, job_title_id: jt.id || '', rate: 0, man_day_rate: 0, })); const mergedRoles = [...roles, ...newRoles].filter( (role, idx, arr) => arr.findIndex(r => r.job_title_id === role.job_title_id) === idx ); setRoles(mergedRoles); }; const handleAddRole = () => { if (Object.keys(jobTitles).length === 0) { // Allow inline job title creation setIsCreatingJobTitle(true); } else { // Add a new empty role to the table const newRole = { jobtitle: '', rate_card_id: ratecardId, job_title_id: '', rate: 0, man_day_rate: 0, }; setRoles([...roles, newRole]); setAddingRowIndex(roles.length); setIsAddingRole(true); } }; const handleCreateJobTitle = async () => { if (!newJobTitleName.trim()) { messageApi.warning(t('jobTitleNameRequired') || 'Job title name is required'); return; } try { // Create the job title using the API const response = await jobTitlesApiService.createJobTitle({ name: newJobTitleName.trim(), }); if (response.done) { // Refresh job titles await getJobTitles(); // Create a new role with the newly created job title const newRole = { jobtitle: newJobTitleName.trim(), rate_card_id: ratecardId, job_title_id: response.body.id, rate: 0, man_day_rate: 0, }; setRoles([...roles, newRole]); // Reset creation state setIsCreatingJobTitle(false); setNewJobTitleName(''); messageApi.success(t('jobTitleCreatedSuccess') || 'Job title created successfully'); } else { messageApi.error(t('jobTitleCreateError') || 'Failed to create job title'); } } catch (error) { console.error('Failed to create job title:', error); messageApi.error(t('jobTitleCreateError') || 'Failed to create job title'); } }; const handleCancelJobTitleCreation = () => { setIsCreatingJobTitle(false); setNewJobTitleName(''); }; const handleDeleteRole = (index: number) => { const updatedRoles = [...roles]; updatedRoles.splice(index, 1); setRoles(updatedRoles); }; const handleSelectJobTitle = (jobTitleId: string) => { if (roles.some(role => role.job_title_id === jobTitleId)) { setIsAddingRole(false); setSelectedJobTitleId(undefined); return; } const jobTitle = jobTitles.data?.find(jt => jt.id === jobTitleId); if (jobTitle) { const newRole = { jobtitle: jobTitle.name, rate_card_id: ratecardId, job_title_id: jobTitleId, rate: 0, man_day_rate: 0, }; setRoles([...roles, newRole]); } setIsAddingRole(false); setSelectedJobTitleId(undefined); }; const handleSave = async () => { if (type === 'update' && ratecardId) { try { const filteredRoles = roles.filter(role => role.jobtitle && role.jobtitle.trim() !== ''); await dispatch( updateRateCard({ id: ratecardId, body: { name, currency, jobRolesList: filteredRoles, }, }) as any ); await dispatch( fetchRateCards({ index: 1, size: 10, field: 'name', order: 'desc', search: '', }) as any ); if (onSaved) onSaved(); dispatch(toggleRatecardDrawer()); // Reset initial states after save setInitialRoles(filteredRoles); setInitialName(name); setInitialCurrency(currency); setShowUnsavedAlert(false); } catch (error) { console.error('Failed to update rate card', error); } } }; const columns = [ { title: t('jobTitleColumn'), dataIndex: 'jobtitle', render: (text: string, record: any, index: number) => { if (index === addingRowIndex || index === editingRowIndex) { return ( ); } return {record.jobtitle}; }, }, { title: isManDaysMethod ? `${t('ratePerManDayColumn', { ns: 'project-view-finance' }) || 'Rate per day'} (${currency})` : `${t('ratePerHourColumn')} (${currency})`, dataIndex: isManDaysMethod ? 'man_day_rate' : 'rate', align: 'right' as const, render: (text: number, record: any, index: number) => ( { const newValue = parseInt(e.target.value, 10) || 0; const updatedRoles = roles.map((role, idx) => idx === index ? { ...role, ...(isManDaysMethod ? { man_day_rate: newValue } : { rate: newValue }), } : role ); setRoles(updatedRoles); }} /> ), }, { title: t('actionsColumn') || 'Actions', dataIndex: 'actions', render: (_: any, __: any, index: number) => ( } okText={t('deleteConfirmationOk')} cancelText={t('deleteConfirmationCancel')} onConfirm={async () => { handleDeleteRole(index); }} > ) : ( {Object.keys(jobTitles).length === 0 ? t('noJobTitlesAvailable') : t('noRolesAdded')} ), }} /> {organization && ( )} ); }; export default RateCardDrawer;