diff --git a/worklenz-backend/src/controllers/admin-center-controller.ts b/worklenz-backend/src/controllers/admin-center-controller.ts index 97f1a134..354b3014 100644 --- a/worklenz-backend/src/controllers/admin-center-controller.ts +++ b/worklenz-backend/src/controllers/admin-center-controller.ts @@ -71,6 +71,27 @@ export default class AdminCenterController extends WorklenzControllerBase { contact_number, contact_number_secondary, (SELECT email FROM users WHERE id = organizations.user_id), + (SELECT name FROM users WHERE id = organizations.user_id) AS owner_name, + calculation_method, + hours_per_day + FROM organizations + WHERE user_id = $1;`; + const result = await db.query(q, [req.user?.owner_id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions() + public static async getAdminCenterSettings( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const q = `SELECT organization_name AS name, + contact_number, + contact_number_secondary, + calculation_method, + hours_per_day, + (SELECT email FROM users WHERE id = organizations.user_id), (SELECT name FROM users WHERE id = organizations.user_id) AS owner_name FROM organizations WHERE user_id = $1;`; diff --git a/worklenz-backend/src/controllers/holiday-controller.ts b/worklenz-backend/src/controllers/holiday-controller.ts index 5d098cb2..1e7d93a2 100644 --- a/worklenz-backend/src/controllers/holiday-controller.ts +++ b/worklenz-backend/src/controllers/holiday-controller.ts @@ -264,6 +264,27 @@ export default class HolidayController extends WorklenzControllerBase { @HandleExceptions() public static async populateCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + // Check if this organization has recently populated holidays (within last hour) + const recentPopulationCheck = ` + SELECT COUNT(*) as count + FROM organization_holidays + WHERE organization_id = (SELECT id FROM organizations WHERE user_id = $1) + AND created_at > NOW() - INTERVAL '1 hour' + `; + + const recentResult = await db.query(recentPopulationCheck, [req.user?.owner_id]); + const recentCount = parseInt(recentResult.rows[0]?.count || '0'); + + // If there are recent holidays added, skip population + if (recentCount > 10) { + return res.status(200).send(new ServerResponse(true, { + success: true, + message: "Holidays were recently populated, skipping to avoid duplicates", + total_populated: 0, + recently_populated: true + })); + } + const Holidays = require("date-holidays"); const countries = [ diff --git a/worklenz-backend/src/routes/apis/admin-center-api-router.ts b/worklenz-backend/src/routes/apis/admin-center-api-router.ts index c66a81c2..1459eb10 100644 --- a/worklenz-backend/src/routes/apis/admin-center-api-router.ts +++ b/worklenz-backend/src/routes/apis/admin-center-api-router.ts @@ -8,6 +8,7 @@ import teamOwnerOrAdminValidator from "../../middlewares/validators/team-owner-o const adminCenterApiRouter = express.Router(); // overview +adminCenterApiRouter.get("/settings", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getAdminCenterSettings)); adminCenterApiRouter.get("/organization", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationDetails)); adminCenterApiRouter.get("/organization/admins", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationAdmins)); adminCenterApiRouter.put("/organization", teamOwnerOrAdminValidator, organizationSettingsValidator, safeControllerFunction(AdminCenterController.updateOrganizationName)); diff --git a/worklenz-frontend/public/locales/alb/project-view-finance.json b/worklenz-frontend/public/locales/alb/project-view-finance.json index 088ff3f3..89074019 100644 --- a/worklenz-frontend/public/locales/alb/project-view-finance.json +++ b/worklenz-frontend/public/locales/alb/project-view-finance.json @@ -33,7 +33,7 @@ "saveButton": "Save", "jobTitleColumn": "Job Title", "ratePerHourColumn": "Rate per hour", - "ratePerManDayColumn": "Rate per man day", + "ratePerManDayColumn": "Tarifa për ditë-njeri", "calculationMethodText": "Calculation Method", "hourlyRatesText": "Hourly Rates", "manDaysText": "Man Days", diff --git a/worklenz-frontend/public/locales/alb/settings/ratecard-settings.json b/worklenz-frontend/public/locales/alb/settings/ratecard-settings.json index c2c97078..d2bfa3a7 100644 --- a/worklenz-frontend/public/locales/alb/settings/ratecard-settings.json +++ b/worklenz-frontend/public/locales/alb/settings/ratecard-settings.json @@ -48,5 +48,8 @@ "jobTitleCreatedSuccess": "Titulli i punës u krijua me sukses", "jobTitleCreateError": "Dështoi të krijohet titulli i punës", "createButton": "Krijo", - "cancelButton": "Anulo" + "cancelButton": "Anulo", + "discardButton": "Hidh poshtë", + "manDaysCalculationMessage": "Organizata po përdor llogaritjen e ditëve-njeri ({{hours}}h/ditë). Tarifat më sipër përfaqësojnë tarifa ditore.", + "hourlyCalculationMessage": "Organizata po përdor llogaritjen orore. Tarifat më sipër përfaqësojnë tarifa orore." } diff --git a/worklenz-frontend/public/locales/de/project-view-finance.json b/worklenz-frontend/public/locales/de/project-view-finance.json index 088ff3f3..381e3d9a 100644 --- a/worklenz-frontend/public/locales/de/project-view-finance.json +++ b/worklenz-frontend/public/locales/de/project-view-finance.json @@ -33,7 +33,7 @@ "saveButton": "Save", "jobTitleColumn": "Job Title", "ratePerHourColumn": "Rate per hour", - "ratePerManDayColumn": "Rate per man day", + "ratePerManDayColumn": "Satz pro Manntag", "calculationMethodText": "Calculation Method", "hourlyRatesText": "Hourly Rates", "manDaysText": "Man Days", diff --git a/worklenz-frontend/public/locales/de/settings/ratecard-settings.json b/worklenz-frontend/public/locales/de/settings/ratecard-settings.json index c8e22c03..220e2d56 100644 --- a/worklenz-frontend/public/locales/de/settings/ratecard-settings.json +++ b/worklenz-frontend/public/locales/de/settings/ratecard-settings.json @@ -48,5 +48,8 @@ "jobTitleCreatedSuccess": "Berufsbezeichnung erfolgreich erstellt", "jobTitleCreateError": "Berufsbezeichnung konnte nicht erstellt werden", "createButton": "Erstellen", - "cancelButton": "Abbrechen" + "cancelButton": "Abbrechen", + "discardButton": "Verwerfen", + "manDaysCalculationMessage": "Organisation verwendet Manntage-Berechnung ({{hours}}h/Tag). Die obigen Sätze stellen Tagessätze dar.", + "hourlyCalculationMessage": "Organisation verwendet Stunden-Berechnung. Die obigen Sätze stellen Stundensätze dar." } diff --git a/worklenz-frontend/public/locales/en/settings/ratecard-settings.json b/worklenz-frontend/public/locales/en/settings/ratecard-settings.json index 1abfe013..5db763c5 100644 --- a/worklenz-frontend/public/locales/en/settings/ratecard-settings.json +++ b/worklenz-frontend/public/locales/en/settings/ratecard-settings.json @@ -48,5 +48,8 @@ "jobTitleCreatedSuccess": "Job title created successfully", "jobTitleCreateError": "Failed to create job title", "createButton": "Create", - "cancelButton": "Cancel" + "cancelButton": "Cancel", + "discardButton": "Discard", + "manDaysCalculationMessage": "Organization is using man days calculation ({{hours}}h/day). Rates above represent daily rates.", + "hourlyCalculationMessage": "Organization is using hourly calculation. Rates above represent hourly rates." } diff --git a/worklenz-frontend/public/locales/es/project-view-finance.json b/worklenz-frontend/public/locales/es/project-view-finance.json index 088ff3f3..f6b4b5d6 100644 --- a/worklenz-frontend/public/locales/es/project-view-finance.json +++ b/worklenz-frontend/public/locales/es/project-view-finance.json @@ -33,7 +33,7 @@ "saveButton": "Save", "jobTitleColumn": "Job Title", "ratePerHourColumn": "Rate per hour", - "ratePerManDayColumn": "Rate per man day", + "ratePerManDayColumn": "Tarifa por día-hombre", "calculationMethodText": "Calculation Method", "hourlyRatesText": "Hourly Rates", "manDaysText": "Man Days", diff --git a/worklenz-frontend/public/locales/es/settings/ratecard-settings.json b/worklenz-frontend/public/locales/es/settings/ratecard-settings.json index 6040465f..5ee3a881 100644 --- a/worklenz-frontend/public/locales/es/settings/ratecard-settings.json +++ b/worklenz-frontend/public/locales/es/settings/ratecard-settings.json @@ -48,5 +48,8 @@ "jobTitleCreatedSuccess": "Título de trabajo creado con éxito", "jobTitleCreateError": "No se pudo crear el título de trabajo", "createButton": "Crear", - "cancelButton": "Cancelar" + "cancelButton": "Cancelar", + "discardButton": "Descartar", + "manDaysCalculationMessage": "La organización utiliza cálculo por días-hombre ({{hours}}h/día). Las tarifas anteriores representan tarifas diarias.", + "hourlyCalculationMessage": "La organización utiliza cálculo por horas. Las tarifas anteriores representan tarifas por hora." } diff --git a/worklenz-frontend/public/locales/pt/project-view-finance.json b/worklenz-frontend/public/locales/pt/project-view-finance.json index 088ff3f3..89e60a02 100644 --- a/worklenz-frontend/public/locales/pt/project-view-finance.json +++ b/worklenz-frontend/public/locales/pt/project-view-finance.json @@ -1,114 +1,114 @@ { - "financeText": "Finance", - "ratecardSingularText": "Rate Card", - "groupByText": "Group by", + "financeText": "Finanças", + "ratecardSingularText": "Tabela de Preços", + "groupByText": "Agrupar por", "statusText": "Status", - "phaseText": "Phase", - "priorityText": "Priority", - "exportButton": "Export", - "currencyText": "Currency", - "importButton": "Import", - "filterText": "Filter", - "billableOnlyText": "Billable Only", - "nonBillableOnlyText": "Non-Billable Only", - "allTasksText": "All Tasks", - "projectBudgetOverviewText": "Project Budget Overview", - "taskColumn": "Task", - "membersColumn": "Members", - "hoursColumn": "Estimated Hours", - "manDaysColumn": "Estimated Man Days", - "actualManDaysColumn": "Actual Man Days", - "effortVarianceColumn": "Effort Variance", - "totalTimeLoggedColumn": "Total Time Logged", - "costColumn": "Actual Cost", - "estimatedCostColumn": "Estimated Cost", - "fixedCostColumn": "Fixed Cost", - "totalBudgetedCostColumn": "Total Budgeted Cost", - "totalActualCostColumn": "Total Actual Cost", - "varianceColumn": "Variance", + "phaseText": "Fase", + "priorityText": "Prioridade", + "exportButton": "Exportar", + "currencyText": "Moeda", + "importButton": "Importar", + "filterText": "Filtrar", + "billableOnlyText": "Apenas Faturável", + "nonBillableOnlyText": "Apenas Não Faturável", + "allTasksText": "Todas as Tarefas", + "projectBudgetOverviewText": "Visão Geral do Orçamento do Projeto", + "taskColumn": "Tarefa", + "membersColumn": "Membros", + "hoursColumn": "Horas Estimadas", + "manDaysColumn": "Dias-Homem Estimados", + "actualManDaysColumn": "Dias-Homem Reais", + "effortVarianceColumn": "Variação do Esforço", + "totalTimeLoggedColumn": "Tempo Total Registrado", + "costColumn": "Custo Real", + "estimatedCostColumn": "Custo Estimado", + "fixedCostColumn": "Custo Fixo", + "totalBudgetedCostColumn": "Custo Total Orçado", + "totalActualCostColumn": "Custo Total Real", + "varianceColumn": "Variação", "totalText": "Total", - "noTasksFound": "No tasks found", - "addRoleButton": "+ Add Role", - "ratecardImportantNotice": "* This rate card is generated based on the company's standard job titles and rates. However, you have the flexibility to modify it according to the project. These changes will not impact the organization's standard job titles and rates.", - "saveButton": "Save", - "jobTitleColumn": "Job Title", - "ratePerHourColumn": "Rate per hour", - "ratePerManDayColumn": "Rate per man day", - "calculationMethodText": "Calculation Method", - "hourlyRatesText": "Hourly Rates", - "manDaysText": "Man Days", - "hoursPerDayText": "Hours per Day", - "ratecardPluralText": "Rate Cards", - "labourHoursColumn": "Labour Hours", - "actions": "Actions", - "selectJobTitle": "Select Job Title", - "ratecardsPluralText": "Rate Card Templates", - "deleteConfirm": "Are you sure ?", - "yes": "Yes", - "no": "No", - "alreadyImportedRateCardMessage": "A rate card has already been imported. Clear all imported rate cards to add a new one.", + "noTasksFound": "Nenhuma tarefa encontrada", + "addRoleButton": "+ Adicionar Função", + "ratecardImportantNotice": "* Esta tabela de preços é gerada com base nos cargos padrão e taxas da empresa. No entanto, você tem a flexibilidade de modificá-la de acordo com o projeto. Essas alterações não afetarão os cargos padrão e taxas da organização.", + "saveButton": "Salvar", + "jobTitleColumn": "Cargo", + "ratePerHourColumn": "Taxa por hora", + "ratePerManDayColumn": "Taxa por dia-homem", + "calculationMethodText": "Método de Cálculo", + "hourlyRatesText": "Taxas por Hora", + "manDaysText": "Dias-Homem", + "hoursPerDayText": "Horas por Dia", + "ratecardPluralText": "Tabelas de Preços", + "labourHoursColumn": "Horas de Trabalho", + "actions": "Ações", + "selectJobTitle": "Selecionar Cargo", + "ratecardsPluralText": "Modelos de Tabela de Preços", + "deleteConfirm": "Tem certeza?", + "yes": "Sim", + "no": "Não", + "alreadyImportedRateCardMessage": "Uma tabela de preços já foi importada. Limpe todas as tabelas de preços importadas para adicionar uma nova.", "budgetOverviewTooltips": { - "manualBudget": "Manual project budget amount set by project manager", - "totalActualCost": "Total actual cost including fixed costs", - "variance": "Difference between manual budget and actual cost", - "utilization": "Percentage of manual budget utilized", - "estimatedHours": "Total estimated hours from all tasks", - "fixedCosts": "Total fixed costs from all tasks", - "timeBasedCost": "Actual cost from time tracking (excluding fixed costs)", - "remainingBudget": "Remaining budget amount" + "manualBudget": "Valor do orçamento manual do projeto definido pelo gerente do projeto", + "totalActualCost": "Custo total real incluindo custos fixos", + "variance": "Diferença entre orçamento manual e custo real", + "utilization": "Porcentagem do orçamento manual utilizado", + "estimatedHours": "Total de horas estimadas de todas as tarefas", + "fixedCosts": "Total de custos fixos de todas as tarefas", + "timeBasedCost": "Custo real do rastreamento de tempo (excluindo custos fixos)", + "remainingBudget": "Valor do orçamento restante" }, "budgetModal": { - "title": "Edit Project Budget", - "description": "Set a manual budget for this project. This budget will be used for all financial calculations and should include both time-based costs and fixed costs.", - "placeholder": "Enter budget amount", - "saveButton": "Save", - "cancelButton": "Cancel" + "title": "Editar Orçamento do Projeto", + "description": "Defina um orçamento manual para este projeto. Este orçamento será usado para todos os cálculos financeiros e deve incluir tanto custos baseados em tempo quanto custos fixos.", + "placeholder": "Digite o valor do orçamento", + "saveButton": "Salvar", + "cancelButton": "Cancelar" }, "budgetStatistics": { - "manualBudget": "Manual Budget", - "totalActualCost": "Total Actual Cost", - "variance": "Variance", - "budgetUtilization": "Budget Utilization", - "estimatedHours": "Estimated Hours", - "fixedCosts": "Fixed Costs", - "timeBasedCost": "Time-based Cost", - "remainingBudget": "Remaining Budget", - "noManualBudgetSet": "(No Manual Budget Set)" + "manualBudget": "Orçamento Manual", + "totalActualCost": "Custo Total Real", + "variance": "Variação", + "budgetUtilization": "Utilização do Orçamento", + "estimatedHours": "Horas Estimadas", + "fixedCosts": "Custos Fixos", + "timeBasedCost": "Custo Baseado em Tempo", + "remainingBudget": "Orçamento Restante", + "noManualBudgetSet": "(Nenhum Orçamento Manual Definido)" }, "budgetSettingsDrawer": { - "title": "Project Budget Settings", - "budgetConfiguration": "Budget Configuration", - "projectBudget": "Project Budget", - "projectBudgetTooltip": "Total budget allocated for this project", - "currency": "Currency", - "costCalculationMethod": "Cost Calculation Method", - "calculationMethod": "Calculation Method", - "workingHoursPerDay": "Working Hours per Day", - "workingHoursPerDayTooltip": "Number of working hours in a day for man-day calculations", - "hourlyCalculationInfo": "Costs will be calculated using estimated hours × hourly rates", - "manDaysCalculationInfo": "Costs will be calculated using estimated man days × daily rates", - "importantNotes": "Important Notes", - "calculationMethodChangeNote": "• Changing the calculation method will affect how costs are calculated for all tasks in this project", - "immediateEffectNote": "• Changes take effect immediately and will recalculate all project totals", - "projectWideNote": "• Budget settings apply to the entire project and all its tasks", - "cancel": "Cancel", - "saveChanges": "Save Changes", - "budgetSettingsUpdated": "Budget settings updated successfully", - "budgetSettingsUpdateFailed": "Failed to update budget settings" + "title": "Configurações de Orçamento do Projeto", + "budgetConfiguration": "Configuração do Orçamento", + "projectBudget": "Orçamento do Projeto", + "projectBudgetTooltip": "Orçamento total alocado para este projeto", + "currency": "Moeda", + "costCalculationMethod": "Método de Cálculo de Custo", + "calculationMethod": "Método de Cálculo", + "workingHoursPerDay": "Horas de Trabalho por Dia", + "workingHoursPerDayTooltip": "Número de horas de trabalho em um dia para cálculos de dia-homem", + "hourlyCalculationInfo": "Os custos serão calculados usando horas estimadas × taxas por hora", + "manDaysCalculationInfo": "Os custos serão calculados usando dias-homem estimados × taxas diárias", + "importantNotes": "Notas Importantes", + "calculationMethodChangeNote": "• Alterar o método de cálculo afetará como os custos são calculados para todas as tarefas neste projeto", + "immediateEffectNote": "• As alterações entram em vigor imediatamente e recalcularão todos os totais do projeto", + "projectWideNote": "• As configurações de orçamento se aplicam a todo o projeto e todas as suas tarefas", + "cancel": "Cancelar", + "saveChanges": "Salvar Alterações", + "budgetSettingsUpdated": "Configurações de orçamento atualizadas com sucesso", + "budgetSettingsUpdateFailed": "Falha ao atualizar configurações de orçamento" }, "columnTooltips": { - "hours": "Total estimated hours for all tasks. Calculated from task time estimates.", - "manDays": "Total estimated man days for all tasks. Based on {{hoursPerDay}} hours per working day.", - "actualManDays": "Actual man days spent based on time logged. Calculated as: Total Time Logged ÷ {{hoursPerDay}} hours per day.", - "effortVariance": "Difference between estimated and actual man days. Positive values indicate over-estimation, negative values indicate under-estimation.", - "totalTimeLogged": "Total time actually logged by team members across all tasks.", - "estimatedCostHourly": "Estimated cost calculated as: Estimated Hours × Hourly Rates for assigned team members.", - "estimatedCostManDays": "Estimated cost calculated as: Estimated Man Days × Daily Rates for assigned team members.", - "actualCost": "Actual cost based on time logged. Calculated as: Time Logged × Hourly Rates for team members.", - "fixedCost": "Fixed costs that don't depend on time spent. Added manually per task.", - "totalBudgetHourly": "Total budgeted cost including estimated cost (Hours × Hourly Rates) + Fixed Costs.", - "totalBudgetManDays": "Total budgeted cost including estimated cost (Man Days × Daily Rates) + Fixed Costs.", - "totalActual": "Total actual cost including time-based cost + Fixed Costs.", - "variance": "Cost variance: Total Budgeted Costs - Total Actual Cost. Positive values indicate under-budget, negative values indicate over-budget." + "hours": "Total de horas estimadas para todas as tarefas. Calculado a partir das estimativas de tempo das tarefas.", + "manDays": "Total de dias-homem estimados para todas as tarefas. Baseado em {{hoursPerDay}} horas por dia de trabalho.", + "actualManDays": "Dias-homem reais gastos com base no tempo registrado. Calculado como: Tempo Total Registrado ÷ {{hoursPerDay}} horas por dia.", + "effortVariance": "Diferença entre dias-homem estimados e reais. Valores positivos indicam superestimação, valores negativos indicam subestimação.", + "totalTimeLogged": "Tempo total realmente registrado pelos membros da equipe em todas as tarefas.", + "estimatedCostHourly": "Custo estimado calculado como: Horas Estimadas × Taxas por Hora para membros da equipe designados.", + "estimatedCostManDays": "Custo estimado calculado como: Dias-Homem Estimados × Taxas Diárias para membros da equipe designados.", + "actualCost": "Custo real baseado no tempo registrado. Calculado como: Tempo Registrado × Taxas por Hora para membros da equipe.", + "fixedCost": "Custos fixos que não dependem do tempo gasto. Adicionados manualmente por tarefa.", + "totalBudgetHourly": "Custo total orçado incluindo custo estimado (Horas × Taxas por Hora) + Custos Fixos.", + "totalBudgetManDays": "Custo total orçado incluindo custo estimado (Dias-Homem × Taxas Diárias) + Custos Fixos.", + "totalActual": "Custo total real incluindo custo baseado em tempo + Custos Fixos.", + "variance": "Variação de custo: Custos Totais Orçados - Custo Total Real. Valores positivos indicam abaixo do orçamento, valores negativos indicam acima do orçamento." } } diff --git a/worklenz-frontend/public/locales/pt/settings/ratecard-settings.json b/worklenz-frontend/public/locales/pt/settings/ratecard-settings.json index 5d950eea..03faea44 100644 --- a/worklenz-frontend/public/locales/pt/settings/ratecard-settings.json +++ b/worklenz-frontend/public/locales/pt/settings/ratecard-settings.json @@ -48,5 +48,8 @@ "jobTitleCreatedSuccess": "Cargo criado com sucesso", "jobTitleCreateError": "Falha ao criar cargo", "createButton": "Criar", - "cancelButton": "Cancelar" + "cancelButton": "Cancelar", + "discardButton": "Descartar", + "manDaysCalculationMessage": "A organização está usando cálculo por dias-homem ({{hours}}h/dia). As taxas acima representam taxas diárias.", + "hourlyCalculationMessage": "A organização está usando cálculo por horas. As taxas acima representam taxas horárias." } diff --git a/worklenz-frontend/public/locales/zh/project-view-finance.json b/worklenz-frontend/public/locales/zh/project-view-finance.json index 088ff3f3..bf515e6a 100644 --- a/worklenz-frontend/public/locales/zh/project-view-finance.json +++ b/worklenz-frontend/public/locales/zh/project-view-finance.json @@ -1,114 +1,114 @@ { - "financeText": "Finance", - "ratecardSingularText": "Rate Card", - "groupByText": "Group by", - "statusText": "Status", - "phaseText": "Phase", - "priorityText": "Priority", - "exportButton": "Export", - "currencyText": "Currency", - "importButton": "Import", - "filterText": "Filter", - "billableOnlyText": "Billable Only", - "nonBillableOnlyText": "Non-Billable Only", - "allTasksText": "All Tasks", - "projectBudgetOverviewText": "Project Budget Overview", - "taskColumn": "Task", - "membersColumn": "Members", - "hoursColumn": "Estimated Hours", - "manDaysColumn": "Estimated Man Days", - "actualManDaysColumn": "Actual Man Days", - "effortVarianceColumn": "Effort Variance", - "totalTimeLoggedColumn": "Total Time Logged", - "costColumn": "Actual Cost", - "estimatedCostColumn": "Estimated Cost", - "fixedCostColumn": "Fixed Cost", - "totalBudgetedCostColumn": "Total Budgeted Cost", - "totalActualCostColumn": "Total Actual Cost", - "varianceColumn": "Variance", - "totalText": "Total", - "noTasksFound": "No tasks found", - "addRoleButton": "+ Add Role", - "ratecardImportantNotice": "* This rate card is generated based on the company's standard job titles and rates. However, you have the flexibility to modify it according to the project. These changes will not impact the organization's standard job titles and rates.", - "saveButton": "Save", - "jobTitleColumn": "Job Title", - "ratePerHourColumn": "Rate per hour", - "ratePerManDayColumn": "Rate per man day", - "calculationMethodText": "Calculation Method", - "hourlyRatesText": "Hourly Rates", - "manDaysText": "Man Days", - "hoursPerDayText": "Hours per Day", - "ratecardPluralText": "Rate Cards", - "labourHoursColumn": "Labour Hours", - "actions": "Actions", - "selectJobTitle": "Select Job Title", - "ratecardsPluralText": "Rate Card Templates", - "deleteConfirm": "Are you sure ?", - "yes": "Yes", - "no": "No", - "alreadyImportedRateCardMessage": "A rate card has already been imported. Clear all imported rate cards to add a new one.", + "financeText": "财务", + "ratecardSingularText": "费率卡", + "groupByText": "分组方式", + "statusText": "状态", + "phaseText": "阶段", + "priorityText": "优先级", + "exportButton": "导出", + "currencyText": "货币", + "importButton": "导入", + "filterText": "筛选", + "billableOnlyText": "仅可计费", + "nonBillableOnlyText": "仅不可计费", + "allTasksText": "所有任务", + "projectBudgetOverviewText": "项目预算概览", + "taskColumn": "任务", + "membersColumn": "成员", + "hoursColumn": "预估工时", + "manDaysColumn": "预估人天", + "actualManDaysColumn": "实际人天", + "effortVarianceColumn": "工作量偏差", + "totalTimeLoggedColumn": "总记录时间", + "costColumn": "实际成本", + "estimatedCostColumn": "预估成本", + "fixedCostColumn": "固定成本", + "totalBudgetedCostColumn": "总预算成本", + "totalActualCostColumn": "总实际成本", + "varianceColumn": "偏差", + "totalText": "总计", + "noTasksFound": "未找到任务", + "addRoleButton": "+ 添加角色", + "ratecardImportantNotice": "* 此费率卡基于公司标准职位和费率生成。但是,您可以根据项目灵活修改。这些更改不会影响组织的标准职位和费率。", + "saveButton": "保存", + "jobTitleColumn": "职位", + "ratePerHourColumn": "每小时费率", + "ratePerManDayColumn": "每人每日费率", + "calculationMethodText": "计算方法", + "hourlyRatesText": "小时费率", + "manDaysText": "人天", + "hoursPerDayText": "每日工时", + "ratecardPluralText": "费率卡", + "labourHoursColumn": "工时", + "actions": "操作", + "selectJobTitle": "选择职位", + "ratecardsPluralText": "费率卡模板", + "deleteConfirm": "您确定吗?", + "yes": "是", + "no": "否", + "alreadyImportedRateCardMessage": "已导入费率卡。清除所有已导入的费率卡以添加新的费率卡。", "budgetOverviewTooltips": { - "manualBudget": "Manual project budget amount set by project manager", - "totalActualCost": "Total actual cost including fixed costs", - "variance": "Difference between manual budget and actual cost", - "utilization": "Percentage of manual budget utilized", - "estimatedHours": "Total estimated hours from all tasks", - "fixedCosts": "Total fixed costs from all tasks", - "timeBasedCost": "Actual cost from time tracking (excluding fixed costs)", - "remainingBudget": "Remaining budget amount" + "manualBudget": "项目经理设置的手动项目预算金额", + "totalActualCost": "包括固定成本在内的总实际成本", + "variance": "手动预算与实际成本之间的差异", + "utilization": "手动预算使用百分比", + "estimatedHours": "所有任务的总预估工时", + "fixedCosts": "所有任务的总固定成本", + "timeBasedCost": "基于时间跟踪的实际成本(不包括固定成本)", + "remainingBudget": "剩余预算金额" }, "budgetModal": { - "title": "Edit Project Budget", - "description": "Set a manual budget for this project. This budget will be used for all financial calculations and should include both time-based costs and fixed costs.", - "placeholder": "Enter budget amount", - "saveButton": "Save", - "cancelButton": "Cancel" + "title": "编辑项目预算", + "description": "为此项目设置手动预算。此预算将用于所有财务计算,应包括基于时间的成本和固定成本。", + "placeholder": "输入预算金额", + "saveButton": "保存", + "cancelButton": "取消" }, "budgetStatistics": { - "manualBudget": "Manual Budget", - "totalActualCost": "Total Actual Cost", - "variance": "Variance", - "budgetUtilization": "Budget Utilization", - "estimatedHours": "Estimated Hours", - "fixedCosts": "Fixed Costs", - "timeBasedCost": "Time-based Cost", - "remainingBudget": "Remaining Budget", - "noManualBudgetSet": "(No Manual Budget Set)" + "manualBudget": "手动预算", + "totalActualCost": "总实际成本", + "variance": "偏差", + "budgetUtilization": "预算使用率", + "estimatedHours": "预估工时", + "fixedCosts": "固定成本", + "timeBasedCost": "基于时间的成本", + "remainingBudget": "剩余预算", + "noManualBudgetSet": "(未设置手动预算)" }, "budgetSettingsDrawer": { - "title": "Project Budget Settings", - "budgetConfiguration": "Budget Configuration", - "projectBudget": "Project Budget", - "projectBudgetTooltip": "Total budget allocated for this project", - "currency": "Currency", - "costCalculationMethod": "Cost Calculation Method", - "calculationMethod": "Calculation Method", - "workingHoursPerDay": "Working Hours per Day", - "workingHoursPerDayTooltip": "Number of working hours in a day for man-day calculations", - "hourlyCalculationInfo": "Costs will be calculated using estimated hours × hourly rates", - "manDaysCalculationInfo": "Costs will be calculated using estimated man days × daily rates", - "importantNotes": "Important Notes", - "calculationMethodChangeNote": "• Changing the calculation method will affect how costs are calculated for all tasks in this project", - "immediateEffectNote": "• Changes take effect immediately and will recalculate all project totals", - "projectWideNote": "• Budget settings apply to the entire project and all its tasks", - "cancel": "Cancel", - "saveChanges": "Save Changes", - "budgetSettingsUpdated": "Budget settings updated successfully", - "budgetSettingsUpdateFailed": "Failed to update budget settings" + "title": "项目预算设置", + "budgetConfiguration": "预算配置", + "projectBudget": "项目预算", + "projectBudgetTooltip": "为此项目分配的总预算", + "currency": "货币", + "costCalculationMethod": "成本计算方法", + "calculationMethod": "计算方法", + "workingHoursPerDay": "每日工作工时", + "workingHoursPerDayTooltip": "人天计算中每日的工作工时数", + "hourlyCalculationInfo": "成本将使用预估工时 × 小时费率计算", + "manDaysCalculationInfo": "成本将使用预估人天 × 日费率计算", + "importantNotes": "重要说明", + "calculationMethodChangeNote": "• 更改计算方法将影响此项目中所有任务的成本计算方式", + "immediateEffectNote": "• 更改立即生效并将重新计算所有项目总计", + "projectWideNote": "• 预算设置适用于整个项目及其所有任务", + "cancel": "取消", + "saveChanges": "保存更改", + "budgetSettingsUpdated": "预算设置更新成功", + "budgetSettingsUpdateFailed": "更新预算设置失败" }, "columnTooltips": { - "hours": "Total estimated hours for all tasks. Calculated from task time estimates.", - "manDays": "Total estimated man days for all tasks. Based on {{hoursPerDay}} hours per working day.", - "actualManDays": "Actual man days spent based on time logged. Calculated as: Total Time Logged ÷ {{hoursPerDay}} hours per day.", - "effortVariance": "Difference between estimated and actual man days. Positive values indicate over-estimation, negative values indicate under-estimation.", - "totalTimeLogged": "Total time actually logged by team members across all tasks.", - "estimatedCostHourly": "Estimated cost calculated as: Estimated Hours × Hourly Rates for assigned team members.", - "estimatedCostManDays": "Estimated cost calculated as: Estimated Man Days × Daily Rates for assigned team members.", - "actualCost": "Actual cost based on time logged. Calculated as: Time Logged × Hourly Rates for team members.", - "fixedCost": "Fixed costs that don't depend on time spent. Added manually per task.", - "totalBudgetHourly": "Total budgeted cost including estimated cost (Hours × Hourly Rates) + Fixed Costs.", - "totalBudgetManDays": "Total budgeted cost including estimated cost (Man Days × Daily Rates) + Fixed Costs.", - "totalActual": "Total actual cost including time-based cost + Fixed Costs.", - "variance": "Cost variance: Total Budgeted Costs - Total Actual Cost. Positive values indicate under-budget, negative values indicate over-budget." + "hours": "所有任务的总预估工时。根据任务时间估算计算。", + "manDays": "所有任务的总预估人天。基于每个工作日 {{hoursPerDay}} 小时。", + "actualManDays": "基于记录时间花费的实际人天。计算方式:总记录时间 ÷ {{hoursPerDay}} 小时/天。", + "effortVariance": "预估人天与实际人天之间的差异。正值表示过度估算,负值表示估算不足。", + "totalTimeLogged": "团队成员在所有任务中实际记录的总时间。", + "estimatedCostHourly": "预估成本计算方式:预估工时 × 分配团队成员的小时费率。", + "estimatedCostManDays": "预估成本计算方式:预估人天 × 分配团队成员的日费率。", + "actualCost": "基于记录时间的实际成本。计算方式:记录时间 × 团队成员的小时费率。", + "fixedCost": "不依赖时间花费的固定成本。每个任务手动添加。", + "totalBudgetHourly": "总预算成本包括预估成本(工时 × 小时费率)+ 固定成本。", + "totalBudgetManDays": "总预算成本包括预估成本(人天 × 日费率)+ 固定成本。", + "totalActual": "总实际成本包括基于时间的成本 + 固定成本。", + "variance": "成本偏差:总预算成本 - 总实际成本。正值表示预算内,负值表示超预算。" } } diff --git a/worklenz-frontend/public/locales/zh/settings/ratecard-settings.json b/worklenz-frontend/public/locales/zh/settings/ratecard-settings.json index 02fbce30..a58c2d5a 100644 --- a/worklenz-frontend/public/locales/zh/settings/ratecard-settings.json +++ b/worklenz-frontend/public/locales/zh/settings/ratecard-settings.json @@ -48,5 +48,8 @@ "jobTitleCreatedSuccess": "职位名称创建成功", "jobTitleCreateError": "职位名称创建失败", "createButton": "创建", - "cancelButton": "取消" + "cancelButton": "取消", + "discardButton": "放弃", + "manDaysCalculationMessage": "组织正在使用人日计算({{hours}}小时/天)。上述费率代表日费率。", + "hourlyCalculationMessage": "组织正在使用小时计算。上述费率代表小时费率。" } diff --git a/worklenz-frontend/src/api/admin-center/admin-center.api.service.ts b/worklenz-frontend/src/api/admin-center/admin-center.api.service.ts index e52ef115..c37c65a6 100644 --- a/worklenz-frontend/src/api/admin-center/admin-center.api.service.ts +++ b/worklenz-frontend/src/api/admin-center/admin-center.api.service.ts @@ -303,4 +303,9 @@ export const adminCenterApiService = { ); return response.data; }, + + async getAdminCenterSettings(): Promise> { + const response = await apiClient.get>(`${rootUrl}/settings`); + return response.data; + }, }; diff --git a/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.tsx b/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.tsx index 57bdd959..3568c44f 100644 --- a/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.tsx +++ b/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.tsx @@ -55,6 +55,8 @@ const HolidayCalendar: React.FC = ({ themeMode, workingDay const [editModalVisible, setEditModalVisible] = useState(false); const [selectedHoliday, setSelectedHoliday] = useState(null); const [currentDate, setCurrentDate] = useState(dayjs()); + const [isPopulatingHolidays, setIsPopulatingHolidays] = useState(false); + const [hasAttemptedPopulation, setHasAttemptedPopulation] = useState(false); const fetchHolidayTypes = async () => { try { @@ -69,9 +71,18 @@ const HolidayCalendar: React.FC = ({ themeMode, workingDay const populateHolidaysIfNeeded = async () => { // Check if we have holiday settings with a country code but no holidays - if (holidaySettings?.country_code && holidays.length === 0) { + // Also check if we haven't already attempted population and we're not currently populating + if ( + holidaySettings?.country_code && + holidays.length === 0 && + !hasAttemptedPopulation && + !isPopulatingHolidays + ) { try { console.log('🔄 No holidays found, attempting to populate official holidays...'); + setIsPopulatingHolidays(true); + setHasAttemptedPopulation(true); + const populateRes = await holidayApiService.populateCountryHolidays(); if (populateRes.done) { console.log('✅ Official holidays populated successfully'); @@ -80,6 +91,8 @@ const HolidayCalendar: React.FC = ({ themeMode, workingDay } } catch (error) { console.warn('⚠️ Could not populate official holidays:', error); + } finally { + setIsPopulatingHolidays(false); } } }; @@ -110,7 +123,12 @@ const HolidayCalendar: React.FC = ({ themeMode, workingDay // Check if we need to populate holidays when holiday settings are loaded useEffect(() => { populateHolidaysIfNeeded(); - }, [holidaySettings, holidays.length]); + }, [holidaySettings]); + + // Reset population attempt state when holiday settings change + useEffect(() => { + setHasAttemptedPopulation(false); + }, [holidaySettings?.country_code]); const customHolidays = useMemo(() => { return holidays.filter(holiday => holiday.source === 'custom'); @@ -300,6 +318,11 @@ const HolidayCalendar: React.FC = ({ themeMode, workingDay {holidaySettings.country_code} {holidaySettings.state_code && ` (${holidaySettings.state_code})`} + {isPopulatingHolidays && ( + + 🔄 Populating official holidays... + + )} )} diff --git a/worklenz-frontend/src/components/projects/project-finance/rate-card-drawer/RateCardDrawer.tsx b/worklenz-frontend/src/components/projects/project-finance/rate-card-drawer/RateCardDrawer.tsx index ecae8bdd..2af8687c 100644 --- a/worklenz-frontend/src/components/projects/project-finance/rate-card-drawer/RateCardDrawer.tsx +++ b/worklenz-frontend/src/components/projects/project-finance/rate-card-drawer/RateCardDrawer.tsx @@ -400,7 +400,7 @@ const RateCardDrawer = ({ handleDeleteRole(index); }} > - + } @@ -568,8 +568,10 @@ const RateCardDrawer = ({ { + const res = await adminCenterApiService.getAdminCenterSettings(); + return res.body; + } +); + export const fetchOrganizationAdmins = createAsyncThunk( 'adminCenter/fetchOrganizationAdmins', async () => { @@ -207,6 +215,17 @@ const adminCenterSlice = createSlice({ state.loadingOrganization = false; }); + builder.addCase(fetchAdminCenterSettings.pending, (state, action) => { + state.loadingOrganization = true; + }); + builder.addCase(fetchAdminCenterSettings.fulfilled, (state, action) => { + state.organization = action.payload; + state.loadingOrganization = false; + }); + builder.addCase(fetchAdminCenterSettings.rejected, (state, action) => { + state.loadingOrganization = false; + }); + builder.addCase(fetchOrganizationAdmins.pending, (state, action) => { state.loadingOrganizationAdmins = true; }); diff --git a/worklenz-frontend/src/pages/admin-center/settings/settings.tsx b/worklenz-frontend/src/pages/admin-center/settings/settings.tsx index c8991b49..d57d497d 100644 --- a/worklenz-frontend/src/pages/admin-center/settings/settings.tsx +++ b/worklenz-frontend/src/pages/admin-center/settings/settings.tsx @@ -17,6 +17,7 @@ import { PageHeader } from '@ant-design/pro-components'; import { useTranslation } from 'react-i18next'; import logger from '@/utils/errorLogger'; import { scheduleAPIService } from '@/api/schedule/schedule.api.service'; +import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service'; import { Settings } from '@/types/schedule/schedule-v2.types'; import OrganizationCalculationMethod from '@/components/admin-center/overview/organization-calculation-method/organization-calculation-method'; import HolidayCalendar from '@/components/admin-center/overview/holiday-calendar/holiday-calendar'; @@ -25,6 +26,7 @@ import { useAppSelector } from '@/hooks/useAppSelector'; import { RootState } from '@/app/store'; import { fetchOrganizationDetails, + fetchAdminCenterSettings, fetchOrganizationAdmins, fetchHolidaySettings, updateHolidaySettings, @@ -49,11 +51,11 @@ const SettingsPage: React.FC = () => { const { t } = useTranslation('admin-center/settings'); - const getOrganizationDetails = async () => { + const getAdminCenterSettings = async () => { try { - await dispatch(fetchOrganizationDetails()).unwrap(); + await dispatch(fetchAdminCenterSettings()).unwrap(); } catch (error) { - logger.error('Error getting organization details', error); + logger.error('Error getting admin center settings', error); } }; @@ -111,7 +113,7 @@ const SettingsPage: React.FC = () => { }; useEffect(() => { - getOrganizationDetails(); + getAdminCenterSettings(); getOrganizationAdmins(); getOrgWorkingSettings(); dispatch(fetchHolidaySettings()); @@ -202,10 +204,12 @@ const SettingsPage: React.FC = () => { - + {organization && ( + + )}