From d6686d64bef90c5249e2e16aa74582eb5187e2fb Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Wed, 4 Jun 2025 11:30:51 +0530 Subject: [PATCH] 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. --- .../20250117000000-add-project-currency.sql | 20 ++++ worklenz-backend/database/sql/1_tables.sql | 5 +- worklenz-backend/database/sql/4_functions.sql | 3 +- .../controllers/project-finance-controller.ts | 106 +++++++++++++++++- .../src/controllers/projects-controller.ts | 1 + .../routes/apis/project-finance-api-router.ts | 1 + .../project-finance.api.service.ts | 11 ++ .../src/api/projects/projects.api.service.ts | 10 ++ .../src/features/project/project.slice.ts | 8 +- .../projects/finance/project-finance.slice.ts | 6 +- .../src/lib/settings/settings-constants.ts | 2 +- .../finance-table/finance-table-wrapper.tsx | 2 +- .../finance/project-view-finance.tsx | 47 ++++++-- .../reatecard-table/ratecard-table.tsx | 8 +- .../rate-card-settings.tsx} | 0 .../src/shared/constants/currencies.ts | 54 +++++++++ .../types/project/project-finance.types.ts | 7 ++ .../types/project/projectViewModel.types.ts | 1 + 18 files changed, 269 insertions(+), 23 deletions(-) create mode 100644 worklenz-backend/database/migrations/20250117000000-add-project-currency.sql rename worklenz-frontend/src/pages/settings/{ratecard/ratecard-settings.tsx => rate-card/rate-card-settings.tsx} (100%) create mode 100644 worklenz-frontend/src/shared/constants/currencies.ts diff --git a/worklenz-backend/database/migrations/20250117000000-add-project-currency.sql b/worklenz-backend/database/migrations/20250117000000-add-project-currency.sql new file mode 100644 index 00000000..f9910f76 --- /dev/null +++ b/worklenz-backend/database/migrations/20250117000000-add-project-currency.sql @@ -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; \ No newline at end of file diff --git a/worklenz-backend/database/sql/1_tables.sql b/worklenz-backend/database/sql/1_tables.sql index 670d12fa..27d89b57 100644 --- a/worklenz-backend/database/sql/1_tables.sql +++ b/worklenz-backend/database/sql/1_tables.sql @@ -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); diff --git a/worklenz-backend/database/sql/4_functions.sql b/worklenz-backend/database/sql/4_functions.sql index 88ac6a0b..f422ca47 100644 --- a/worklenz-backend/database/sql/4_functions.sql +++ b/worklenz-backend/database/sql/4_functions.sql @@ -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; diff --git a/worklenz-backend/src/controllers/project-finance-controller.ts b/worklenz-backend/src/controllers/project-finance-controller.ts index cb82c432..b76aa0b6 100644 --- a/worklenz-backend/src/controllers/project-finance-controller.ts +++ b/worklenz-backend/src/controllers/project-finance-controller.ts @@ -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 { + 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}` + })); + } } diff --git a/worklenz-backend/src/controllers/projects-controller.ts b/worklenz-backend/src/controllers/projects-controller.ts index 9a2f2d74..1f0c4efc 100644 --- a/worklenz-backend/src/controllers/projects-controller.ts +++ b/worklenz-backend/src/controllers/projects-controller.ts @@ -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, diff --git a/worklenz-backend/src/routes/apis/project-finance-api-router.ts b/worklenz-backend/src/routes/apis/project-finance-api-router.ts index aae00f22..7ae0dcca 100644 --- a/worklenz-backend/src/routes/apis/project-finance-api-router.ts +++ b/worklenz-backend/src/routes/apis/project-finance-api-router.ts @@ -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; \ No newline at end of file diff --git a/worklenz-frontend/src/api/project-finance-ratecard/project-finance.api.service.ts b/worklenz-frontend/src/api/project-finance-ratecard/project-finance.api.service.ts index 2292ade8..be199572 100644 --- a/worklenz-frontend/src/api/project-finance-ratecard/project-finance.api.service.ts +++ b/worklenz-frontend/src/api/project-finance-ratecard/project-finance.api.service.ts @@ -50,6 +50,17 @@ export const projectFinanceApiService = { return response.data; }, + updateProjectCurrency: async ( + projectId: string, + currency: string + ): Promise> => { + const response = await apiClient.put>( + `${rootUrl}/project/${projectId}/currency`, + { currency } + ); + return response.data; + }, + exportFinanceData: async ( projectId: string, groupBy: 'status' | 'priority' | 'phases' = 'status' diff --git a/worklenz-frontend/src/api/projects/projects.api.service.ts b/worklenz-frontend/src/api/projects/projects.api.service.ts index 0297dd22..27cf992d 100644 --- a/worklenz-frontend/src/api/projects/projects.api.service.ts +++ b/worklenz-frontend/src/api/projects/projects.api.service.ts @@ -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>(`${url}`); return response.data; }, + + updateProjectPhaseLabel: async (projectId: string, phaseLabel: string) => { + const q = toQueryString({ id: projectId, current_project_id: projectId }); + const response = await apiClient.put>( + `${rootUrl}/label/${projectId}${q}`, + { name: phaseLabel } + ); + return response.data; + }, }; diff --git a/worklenz-frontend/src/features/project/project.slice.ts b/worklenz-frontend/src/features/project/project.slice.ts index b1a333ab..99d05dad 100644 --- a/worklenz-frontend/src/features/project/project.slice.ts +++ b/worklenz-frontend/src/features/project/project.slice.ts @@ -116,6 +116,11 @@ const projectSlice = createSlice({ state.project.phase_label = action.payload; } }, + updateProjectCurrency: (state, action: PayloadAction) => { + 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; diff --git a/worklenz-frontend/src/features/projects/finance/project-finance.slice.ts b/worklenz-frontend/src/features/projects/finance/project-finance.slice.ts index 0f47b755..2c177606 100644 --- a/worklenz-frontend/src/features/projects/finance/project-finance.slice.ts +++ b/worklenz-frontend/src/features/projects/finance/project-finance.slice.ts @@ -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; diff --git a/worklenz-frontend/src/lib/settings/settings-constants.ts b/worklenz-frontend/src/lib/settings/settings-constants.ts index f3717492..b26b4b88 100644 --- a/worklenz-frontend/src/lib/settings/settings-constants.ts +++ b/worklenz-frontend/src/lib/settings/settings-constants.ts @@ -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 diff --git a/worklenz-frontend/src/pages/projects/projectView/finance/finance-tab/finance-table/finance-table-wrapper.tsx b/worklenz-frontend/src/pages/projects/projectView/finance/finance-tab/finance-table/finance-table-wrapper.tsx index 62d4a38c..2da99b06 100644 --- a/worklenz-frontend/src/pages/projects/projectView/finance/finance-tab/finance-table/finance-table-wrapper.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/finance/finance-tab/finance-table/finance-table-wrapper.tsx @@ -76,7 +76,7 @@ const FinanceTableWrapper: React.FC = ({ 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 diff --git a/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance.tsx b/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance.tsx index 2872d1ac..4bd41192 100644 --- a/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance.tsx @@ -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 = () => { {t('currencyText')} 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} diff --git a/worklenz-frontend/src/pages/settings/ratecard/ratecard-settings.tsx b/worklenz-frontend/src/pages/settings/rate-card/rate-card-settings.tsx similarity index 100% rename from worklenz-frontend/src/pages/settings/ratecard/ratecard-settings.tsx rename to worklenz-frontend/src/pages/settings/rate-card/rate-card-settings.tsx diff --git a/worklenz-frontend/src/shared/constants/currencies.ts b/worklenz-frontend/src/shared/constants/currencies.ts new file mode 100644 index 00000000..748e6b99 --- /dev/null +++ b/worklenz-frontend/src/shared/constants/currencies.ts @@ -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(); +}; \ No newline at end of file diff --git a/worklenz-frontend/src/types/project/project-finance.types.ts b/worklenz-frontend/src/types/project/project-finance.types.ts index ac3ab56d..f52fea18 100644 --- a/worklenz-frontend/src/types/project/project-finance.types.ts +++ b/worklenz-frontend/src/types/project/project-finance.types.ts @@ -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 { diff --git a/worklenz-frontend/src/types/project/projectViewModel.types.ts b/worklenz-frontend/src/types/project/projectViewModel.types.ts index a35d59b3..2c3706dc 100644 --- a/worklenz-frontend/src/types/project/projectViewModel.types.ts +++ b/worklenz-frontend/src/types/project/projectViewModel.types.ts @@ -65,4 +65,5 @@ export interface IProjectViewModel extends IProject { use_manual_progress?: boolean; use_weighted_progress?: boolean; use_time_progress?: boolean; + currency?: string; }