feat(project-currency): implement project-specific currency support
- Added a currency column to the projects table to allow different projects to use different currencies. - Updated existing projects to default to 'USD' if no currency is set. - Enhanced project finance controller to handle currency retrieval and updates. - Introduced API endpoints for updating project currency with validation. - Updated frontend components to display and manage project currency effectively.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
-- Migration: Add currency column to projects table
|
||||
-- Date: 2025-01-17
|
||||
-- Description: Adds project-specific currency support to allow different projects to use different currencies
|
||||
|
||||
-- Add currency column to projects table
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'USD';
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN projects.currency IS 'Project-specific currency code (e.g., USD, EUR, GBP, JPY, etc.)';
|
||||
|
||||
-- Add constraint to ensure currency codes are uppercase and 3 characters
|
||||
ALTER TABLE projects
|
||||
ADD CONSTRAINT projects_currency_format_check
|
||||
CHECK (currency ~ '^[A-Z]{3}$');
|
||||
|
||||
-- Update existing projects to have a default currency if they don't have one
|
||||
UPDATE projects
|
||||
SET currency = 'USD'
|
||||
WHERE currency IS NULL;
|
||||
@@ -783,9 +783,12 @@ CREATE TABLE IF NOT EXISTS projects (
|
||||
estimated_working_days INTEGER DEFAULT 0,
|
||||
use_manual_progress BOOLEAN DEFAULT FALSE,
|
||||
use_weighted_progress BOOLEAN DEFAULT FALSE,
|
||||
use_time_progress BOOLEAN DEFAULT FALSE
|
||||
use_time_progress BOOLEAN DEFAULT FALSE,
|
||||
currency VARCHAR(3) DEFAULT 'USD'
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN projects.currency IS 'Project-specific currency code (e.g., USD, EUR, GBP, JPY, etc.)';
|
||||
|
||||
ALTER TABLE projects
|
||||
ADD CONSTRAINT projects_pk
|
||||
PRIMARY KEY (id);
|
||||
|
||||
@@ -5401,7 +5401,8 @@ BEGIN
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
estimated_working_days = (_body ->> 'working_days')::INTEGER,
|
||||
estimated_man_days = (_body ->> 'man_days')::INTEGER,
|
||||
hours_per_day = (_body ->> 'hours_per_day')::INTEGER
|
||||
hours_per_day = (_body ->> 'hours_per_day')::INTEGER,
|
||||
currency = COALESCE(UPPER((_body ->> 'currency')::TEXT), currency)
|
||||
WHERE id = (_body ->> 'id')::UUID
|
||||
AND team_id = _team_id
|
||||
RETURNING id INTO _project_id;
|
||||
|
||||
@@ -51,6 +51,20 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
||||
const projectId = req.params.project_id;
|
||||
const groupBy = req.query.group_by || "status";
|
||||
|
||||
// Get project information including currency
|
||||
const projectQuery = `
|
||||
SELECT id, name, currency
|
||||
FROM projects
|
||||
WHERE id = $1
|
||||
`;
|
||||
const projectResult = await db.query(projectQuery, [projectId]);
|
||||
|
||||
if (projectResult.rows.length === 0) {
|
||||
return res.status(404).send(new ServerResponse(false, null, "Project not found"));
|
||||
}
|
||||
|
||||
const project = projectResult.rows[0];
|
||||
|
||||
// First, get the project rate cards for this project
|
||||
const rateCardQuery = `
|
||||
SELECT
|
||||
@@ -339,10 +353,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
||||
};
|
||||
});
|
||||
|
||||
// Include project rate cards in the response for reference
|
||||
// Include project rate cards and currency in the response for reference
|
||||
const responseData = {
|
||||
groups: groupedTasks,
|
||||
project_rate_cards: projectRateCards,
|
||||
project: {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
currency: project.currency || "USD"
|
||||
}
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, responseData));
|
||||
@@ -703,10 +722,12 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
||||
const projectId = req.params.project_id;
|
||||
const groupBy = (req.query.groupBy as string) || "status";
|
||||
|
||||
// Get project name for filename
|
||||
const projectNameQuery = `SELECT name FROM projects WHERE id = $1`;
|
||||
const projectNameResult = await db.query(projectNameQuery, [projectId]);
|
||||
const projectName = projectNameResult.rows[0]?.name || "Unknown Project";
|
||||
// Get project name and currency for filename and export
|
||||
const projectQuery = `SELECT name, currency FROM projects WHERE id = $1`;
|
||||
const projectResult = await db.query(projectQuery, [projectId]);
|
||||
const project = projectResult.rows[0];
|
||||
const projectName = project?.name || "Unknown Project";
|
||||
const projectCurrency = project?.currency || "USD";
|
||||
|
||||
// First, get the project rate cards for this project
|
||||
const rateCardQuery = `
|
||||
@@ -1025,7 +1046,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
||||
// Add title row
|
||||
worksheet.getCell(
|
||||
"A1"
|
||||
).value = `Finance Data Export - ${projectName} - ${moment().format(
|
||||
).value = `Finance Data Export - ${projectName} (${projectCurrency}) - ${moment().format(
|
||||
"MMM DD, YYYY"
|
||||
)}`;
|
||||
worksheet.mergeCells("A1:L1");
|
||||
@@ -1096,4 +1117,77 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
||||
// Send the Excel file as a response
|
||||
res.end(buffer);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateProjectCurrency(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const projectId = req.params.project_id;
|
||||
const { currency } = req.body;
|
||||
|
||||
// Validate currency format (3-character uppercase code)
|
||||
if (!currency || typeof currency !== "string" || !/^[A-Z]{3}$/.test(currency)) {
|
||||
return res
|
||||
.status(400)
|
||||
.send(new ServerResponse(false, null, "Invalid currency format. Currency must be a 3-character uppercase code (e.g., USD, EUR, GBP)"));
|
||||
}
|
||||
|
||||
// Check if project exists and user has access
|
||||
const projectCheckQuery = `
|
||||
SELECT p.id, p.name, p.currency as current_currency
|
||||
FROM projects p
|
||||
WHERE p.id = $1 AND p.team_id = $2
|
||||
`;
|
||||
|
||||
const projectCheckResult = await db.query(projectCheckQuery, [projectId, req.user?.team_id]);
|
||||
|
||||
if (projectCheckResult.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.send(new ServerResponse(false, null, "Project not found or access denied"));
|
||||
}
|
||||
|
||||
const project = projectCheckResult.rows[0];
|
||||
|
||||
// Update project currency
|
||||
const updateQuery = `
|
||||
UPDATE projects
|
||||
SET currency = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND team_id = $3
|
||||
RETURNING id, name, currency;
|
||||
`;
|
||||
|
||||
const result = await db.query(updateQuery, [currency, projectId, req.user?.team_id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(500)
|
||||
.send(new ServerResponse(false, null, "Failed to update project currency"));
|
||||
}
|
||||
|
||||
const updatedProject = result.rows[0];
|
||||
|
||||
// Log the currency change for audit purposes
|
||||
const logQuery = `
|
||||
INSERT INTO project_logs (team_id, project_id, description)
|
||||
VALUES ($1, $2, $3)
|
||||
`;
|
||||
|
||||
const logDescription = `Project currency changed from ${project.current_currency || "USD"} to ${currency}`;
|
||||
|
||||
try {
|
||||
await db.query(logQuery, [req.user?.team_id, projectId, logDescription]);
|
||||
} catch (error) {
|
||||
console.error("Failed to log currency change:", error);
|
||||
// Don't fail the request if logging fails
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {
|
||||
id: updatedProject.id,
|
||||
name: updatedProject.name,
|
||||
currency: updatedProject.currency,
|
||||
message: `Project currency updated to ${currency}`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,6 +395,7 @@ export default class ProjectsController extends WorklenzControllerBase {
|
||||
projects.folder_id,
|
||||
projects.phase_label,
|
||||
projects.category_id,
|
||||
projects.currency,
|
||||
(projects.estimated_man_days) AS man_days,
|
||||
(projects.estimated_working_days) AS working_days,
|
||||
(projects.hours_per_day) AS hours_per_day,
|
||||
|
||||
@@ -14,6 +14,7 @@ projectFinanceApiRouter.get(
|
||||
safeControllerFunction(ProjectfinanceController.getTaskBreakdown)
|
||||
);
|
||||
projectFinanceApiRouter.put("/task/:task_id/fixed-cost", ProjectfinanceController.updateTaskFixedCost);
|
||||
projectFinanceApiRouter.put("/project/:project_id/currency", ProjectfinanceController.updateProjectCurrency);
|
||||
projectFinanceApiRouter.get("/project/:project_id/export", ProjectfinanceController.exportFinanceData);
|
||||
|
||||
export default projectFinanceApiRouter;
|
||||
@@ -50,6 +50,17 @@ export const projectFinanceApiService = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateProjectCurrency: async (
|
||||
projectId: string,
|
||||
currency: string
|
||||
): Promise<IServerResponse<any>> => {
|
||||
const response = await apiClient.put<IServerResponse<any>>(
|
||||
`${rootUrl}/project/${projectId}/currency`,
|
||||
{ currency }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
exportFinanceData: async (
|
||||
projectId: string,
|
||||
groupBy: 'status' | 'priority' | 'phases' = 'status'
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IProjectViewModel } from '@/types/project/projectViewModel.types';
|
||||
import { ITeamMemberOverviewGetResponse } from '@/types/project/project-insights.types';
|
||||
import { IProjectMembersViewModel } from '@/types/projectMember.types';
|
||||
import { IProjectManager } from '@/types/project/projectManager.types';
|
||||
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
||||
|
||||
const rootUrl = `${API_BASE_URL}/projects`;
|
||||
|
||||
@@ -120,5 +121,14 @@ export const projectsApiService = {
|
||||
const response = await apiClient.get<IServerResponse<IProjectManager[]>>(`${url}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateProjectPhaseLabel: async (projectId: string, phaseLabel: string) => {
|
||||
const q = toQueryString({ id: projectId, current_project_id: projectId });
|
||||
const response = await apiClient.put<IServerResponse<ITaskPhase>>(
|
||||
`${rootUrl}/label/${projectId}${q}`,
|
||||
{ name: phaseLabel }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -116,6 +116,11 @@ const projectSlice = createSlice({
|
||||
state.project.phase_label = action.payload;
|
||||
}
|
||||
},
|
||||
updateProjectCurrency: (state, action: PayloadAction<string>) => {
|
||||
if (state.project) {
|
||||
state.project.currency = action.payload;
|
||||
}
|
||||
},
|
||||
addTask: (
|
||||
state,
|
||||
action: PayloadAction<{ task: IProjectTask; groupId: string; insert?: boolean }>
|
||||
@@ -214,7 +219,8 @@ export const {
|
||||
setCreateTaskTemplateDrawerOpen,
|
||||
setProjectView,
|
||||
updatePhaseLabel,
|
||||
setRefreshTimestamp
|
||||
setRefreshTimestamp,
|
||||
updateProjectCurrency
|
||||
} = projectSlice.actions;
|
||||
|
||||
export default projectSlice.reducer;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { projectFinanceApiService } from '@/api/project-finance-ratecard/project-finance.api.service';
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { IProjectFinanceGroup, IProjectFinanceTask, IProjectRateCard } from '@/types/project/project-finance.types';
|
||||
import { IProjectFinanceGroup, IProjectFinanceTask, IProjectRateCard, IProjectFinanceProject } from '@/types/project/project-finance.types';
|
||||
import { parseTimeToSeconds } from '@/utils/timeUtils';
|
||||
|
||||
type FinanceTabType = 'finance' | 'ratecard';
|
||||
@@ -12,6 +12,7 @@ interface ProjectFinanceState {
|
||||
loading: boolean;
|
||||
taskGroups: IProjectFinanceGroup[];
|
||||
projectRateCards: IProjectRateCard[];
|
||||
project: IProjectFinanceProject | null;
|
||||
}
|
||||
|
||||
// Utility functions for frontend calculations
|
||||
@@ -67,6 +68,7 @@ const initialState: ProjectFinanceState = {
|
||||
loading: false,
|
||||
taskGroups: [],
|
||||
projectRateCards: [],
|
||||
project: null,
|
||||
};
|
||||
|
||||
export const fetchProjectFinances = createAsyncThunk(
|
||||
@@ -173,6 +175,7 @@ export const projectFinancesSlice = createSlice({
|
||||
state.loading = false;
|
||||
state.taskGroups = action.payload.groups;
|
||||
state.projectRateCards = action.payload.project_rate_cards;
|
||||
state.project = action.payload.project;
|
||||
})
|
||||
.addCase(fetchProjectFinances.rejected, (state) => {
|
||||
state.loading = false;
|
||||
@@ -181,6 +184,7 @@ export const projectFinancesSlice = createSlice({
|
||||
// Update data without changing loading state for silent refresh
|
||||
state.taskGroups = action.payload.groups;
|
||||
state.projectRateCards = action.payload.project_rate_cards;
|
||||
state.project = action.payload.project;
|
||||
})
|
||||
.addCase(updateTaskFixedCostAsync.fulfilled, (state, action) => {
|
||||
const { taskId, groupId, fixedCost } = action.payload;
|
||||
|
||||
@@ -27,7 +27,7 @@ import TeamMembersSettings from '@/pages/settings/team-members/team-members-sett
|
||||
import TeamsSettings from '../../pages/settings/teams/teams-settings';
|
||||
import ChangePassword from '@/pages/settings/change-password/change-password';
|
||||
import LanguageAndRegionSettings from '@/pages/settings/language-and-region/language-and-region-settings';
|
||||
import RatecardSettings from '@/pages/settings/ratecard/ratecard-settings';
|
||||
import RatecardSettings from '@/pages/settings/rate-card/rate-card-settings';
|
||||
import AppearanceSettings from '@/pages/settings/appearance/appearance-settings';
|
||||
|
||||
// type of menu item in settings sidebar
|
||||
|
||||
@@ -76,7 +76,7 @@ const FinanceTableWrapper: React.FC<FinanceTableWrapperProps> = ({ activeTablesL
|
||||
}, [editingFixedCost]);
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const { currency } = useAppSelector(state => state.financeReducer);
|
||||
const currency = useAppSelector(state => state.projectFinances.project?.currency || "").toUpperCase();
|
||||
const taskGroups = useAppSelector(state => state.projectFinances.taskGroups);
|
||||
|
||||
// Use Redux store data for totals calculation to ensure reactivity
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { fetchProjectFinances, setActiveTab, setActiveGroup } from '@/features/projects/finance/project-finance.slice';
|
||||
import { changeCurrency, toggleImportRatecardsDrawer } from '@/features/finance/finance-slice';
|
||||
import { updateProjectCurrency } from '@/features/project/project.slice';
|
||||
import { projectFinanceApiService } from '@/api/project-finance-ratecard/project-finance.api.service';
|
||||
import { RootState } from '@/app/store';
|
||||
import FinanceTableWrapper from './finance-tab/finance-table/finance-table-wrapper';
|
||||
@@ -14,14 +15,16 @@ 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';
|
||||
import { CURRENCY_OPTIONS, DEFAULT_CURRENCY } from '@/shared/constants/currencies';
|
||||
|
||||
const ProjectViewFinance = () => {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation('project-view-finance');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [updatingCurrency, setUpdatingCurrency] = useState(false);
|
||||
|
||||
const { activeTab, activeGroup, loading, taskGroups } = useAppSelector((state: RootState) => state.projectFinances);
|
||||
const { activeTab, activeGroup, loading, taskGroups, project: financeProject } = useAppSelector((state: RootState) => state.projectFinances);
|
||||
const { refreshTimestamp, project } = useAppSelector((state: RootState) => state.projectReducer);
|
||||
const phaseList = useAppSelector((state) => state.phaseReducer.phaseList);
|
||||
|
||||
@@ -30,6 +33,12 @@ const ProjectViewFinance = () => {
|
||||
const currentSession = auth.getCurrentSession();
|
||||
const hasEditPermission = hasFinanceEditPermission(currentSession, project);
|
||||
|
||||
// Get project-specific currency from finance API response, fallback to project reducer, then default
|
||||
const projectCurrency = (financeProject?.currency || project?.currency || DEFAULT_CURRENCY).toLowerCase();
|
||||
|
||||
// Show loading state for currency selector until finance data is loaded
|
||||
const currencyLoading = loading || updatingCurrency || !financeProject;
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
dispatch(fetchProjectFinances({ projectId, groupBy: activeGroup }));
|
||||
@@ -71,6 +80,30 @@ const ProjectViewFinance = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCurrencyChange = async (currency: string) => {
|
||||
if (!projectId || !hasEditPermission) {
|
||||
message.error('You do not have permission to change the project currency');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setUpdatingCurrency(true);
|
||||
const upperCaseCurrency = currency.toUpperCase();
|
||||
await projectFinanceApiService.updateProjectCurrency(projectId, upperCaseCurrency);
|
||||
|
||||
// Update both global currency state and project-specific currency
|
||||
dispatch(changeCurrency(currency));
|
||||
dispatch(updateProjectCurrency(upperCaseCurrency));
|
||||
|
||||
message.success('Project currency updated successfully');
|
||||
} catch (error) {
|
||||
console.error('Currency update failed:', error);
|
||||
message.error('Failed to update project currency');
|
||||
} finally {
|
||||
setUpdatingCurrency(false);
|
||||
}
|
||||
};
|
||||
|
||||
const groupDropdownMenuItems = [
|
||||
{ key: 'status', value: 'status', label: t('statusText') },
|
||||
{ key: 'priority', value: 'priority', label: t('priorityText') },
|
||||
@@ -130,13 +163,11 @@ const ProjectViewFinance = () => {
|
||||
<Flex gap={8} align="center">
|
||||
<Typography.Text>{t('currencyText')}</Typography.Text>
|
||||
<Select
|
||||
defaultValue={'lkr'}
|
||||
options={[
|
||||
{ value: 'lkr', label: 'LKR' },
|
||||
{ value: 'usd', label: 'USD' },
|
||||
{ value: 'inr', label: 'INR' },
|
||||
]}
|
||||
onChange={(value) => dispatch(changeCurrency(value))}
|
||||
value={projectCurrency}
|
||||
loading={currencyLoading}
|
||||
disabled={!hasEditPermission}
|
||||
options={CURRENCY_OPTIONS}
|
||||
onChange={handleCurrencyChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Avatar, Button, Input, Popconfirm, Table, TableProps, Select, Flex } from 'antd';
|
||||
import { Avatar, Button, Input, Popconfirm, Table, TableProps, Select, Flex, InputRef } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import CustomAvatar from '../../../../../../components/CustomAvatar';
|
||||
import { DeleteOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
@@ -31,7 +31,7 @@ const RatecardTable: React.FC = () => {
|
||||
// Redux state
|
||||
const rolesRedux = useAppSelector((state) => state.projectFinanceRateCard.rateCardRoles) || [];
|
||||
const isLoading = useAppSelector((state) => state.projectFinanceRateCard.isLoading);
|
||||
const currency = useAppSelector((state) => state.financeReducer.currency).toUpperCase();
|
||||
const currency = useAppSelector((state) => state.projectFinances.project?.currency || "USD").toUpperCase();
|
||||
const rateInputRefs = React.useRef<Array<HTMLInputElement | null>>([]);
|
||||
|
||||
// Auth and permissions
|
||||
@@ -244,7 +244,9 @@ const RatecardTable: React.FC = () => {
|
||||
align: 'right',
|
||||
render: (value: number, record: JobRoleType, index: number) => (
|
||||
<Input
|
||||
ref={el => rateInputRefs.current[index] = el}
|
||||
ref={(el: InputRef | null) => {
|
||||
if (el) rateInputRefs.current[index] = el as unknown as HTMLInputElement;
|
||||
}}
|
||||
type="number"
|
||||
value={roles[index]?.rate ?? 0}
|
||||
min={0}
|
||||
|
||||
54
worklenz-frontend/src/shared/constants/currencies.ts
Normal file
54
worklenz-frontend/src/shared/constants/currencies.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export interface CurrencyOption {
|
||||
value: string;
|
||||
label: string;
|
||||
symbol?: string;
|
||||
}
|
||||
|
||||
export const CURRENCY_OPTIONS: CurrencyOption[] = [
|
||||
{ value: 'usd', label: 'USD - US Dollar', symbol: '$' },
|
||||
{ value: 'eur', label: 'EUR - Euro', symbol: '€' },
|
||||
{ value: 'gbp', label: 'GBP - British Pound', symbol: '£' },
|
||||
{ value: 'jpy', label: 'JPY - Japanese Yen', symbol: '¥' },
|
||||
{ value: 'cad', label: 'CAD - Canadian Dollar', symbol: 'C$' },
|
||||
{ value: 'aud', label: 'AUD - Australian Dollar', symbol: 'A$' },
|
||||
{ value: 'chf', label: 'CHF - Swiss Franc', symbol: 'CHF' },
|
||||
{ value: 'cny', label: 'CNY - Chinese Yuan', symbol: '¥' },
|
||||
{ value: 'inr', label: 'INR - Indian Rupee', symbol: '₹' },
|
||||
{ value: 'lkr', label: 'LKR - Sri Lankan Rupee', symbol: 'Rs' },
|
||||
{ value: 'sgd', label: 'SGD - Singapore Dollar', symbol: 'S$' },
|
||||
{ value: 'hkd', label: 'HKD - Hong Kong Dollar', symbol: 'HK$' },
|
||||
{ value: 'nzd', label: 'NZD - New Zealand Dollar', symbol: 'NZ$' },
|
||||
{ value: 'sek', label: 'SEK - Swedish Krona', symbol: 'kr' },
|
||||
{ value: 'nok', label: 'NOK - Norwegian Krone', symbol: 'kr' },
|
||||
{ value: 'dkk', label: 'DKK - Danish Krone', symbol: 'kr' },
|
||||
{ value: 'pln', label: 'PLN - Polish Zloty', symbol: 'zł' },
|
||||
{ value: 'czk', label: 'CZK - Czech Koruna', symbol: 'Kč' },
|
||||
{ value: 'huf', label: 'HUF - Hungarian Forint', symbol: 'Ft' },
|
||||
{ value: 'rub', label: 'RUB - Russian Ruble', symbol: '₽' },
|
||||
{ value: 'brl', label: 'BRL - Brazilian Real', symbol: 'R$' },
|
||||
{ value: 'mxn', label: 'MXN - Mexican Peso', symbol: '$' },
|
||||
{ value: 'zar', label: 'ZAR - South African Rand', symbol: 'R' },
|
||||
{ value: 'krw', label: 'KRW - South Korean Won', symbol: '₩' },
|
||||
{ value: 'thb', label: 'THB - Thai Baht', symbol: '฿' },
|
||||
{ value: 'myr', label: 'MYR - Malaysian Ringgit', symbol: 'RM' },
|
||||
{ value: 'idr', label: 'IDR - Indonesian Rupiah', symbol: 'Rp' },
|
||||
{ value: 'php', label: 'PHP - Philippine Peso', symbol: '₱' },
|
||||
{ value: 'vnd', label: 'VND - Vietnamese Dong', symbol: '₫' },
|
||||
{ value: 'aed', label: 'AED - UAE Dirham', symbol: 'د.إ' },
|
||||
{ value: 'sar', label: 'SAR - Saudi Riyal', symbol: '﷼' },
|
||||
{ value: 'egp', label: 'EGP - Egyptian Pound', symbol: '£' },
|
||||
{ value: 'try', label: 'TRY - Turkish Lira', symbol: '₺' },
|
||||
{ value: 'ils', label: 'ILS - Israeli Shekel', symbol: '₪' },
|
||||
];
|
||||
|
||||
export const DEFAULT_CURRENCY = 'usd';
|
||||
|
||||
export const getCurrencySymbol = (currencyCode: string): string => {
|
||||
const currency = CURRENCY_OPTIONS.find(c => c.value === currencyCode.toLowerCase());
|
||||
return currency?.symbol || currencyCode.toUpperCase();
|
||||
};
|
||||
|
||||
export const getCurrencyLabel = (currencyCode: string): string => {
|
||||
const currency = CURRENCY_OPTIONS.find(c => c.value === currencyCode.toLowerCase());
|
||||
return currency?.label || currencyCode.toUpperCase();
|
||||
};
|
||||
@@ -63,9 +63,16 @@ export interface IProjectRateCard {
|
||||
job_title_name: string;
|
||||
}
|
||||
|
||||
export interface IProjectFinanceProject {
|
||||
id: string;
|
||||
name: string;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface IProjectFinanceResponse {
|
||||
groups: IProjectFinanceGroup[];
|
||||
project_rate_cards: IProjectRateCard[];
|
||||
project: IProjectFinanceProject;
|
||||
}
|
||||
|
||||
export interface ITaskBreakdownMember {
|
||||
|
||||
@@ -65,4 +65,5 @@ export interface IProjectViewModel extends IProject {
|
||||
use_manual_progress?: boolean;
|
||||
use_weighted_progress?: boolean;
|
||||
use_time_progress?: boolean;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user