From 4b54f2cc1736ca8281d51ce185c62aa949bf2cd0 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Thu, 24 Jul 2025 12:53:46 +0530 Subject: [PATCH] feat(admin-center): implement organization calculation method settings - Added functionality to update the organization's calculation method (hourly or man-days) in the Admin Center. - Created a new component for managing the calculation method, including UI elements for selection and saving changes. - Updated API service to handle the new endpoint for updating the calculation method. - Enhanced localization files to support new keys related to the calculation method settings. - Introduced a settings page to manage organization working days and hours alongside the calculation method. --- .../controllers/admin-center-controller.ts | 578 ++++++++++++++---- .../schedule-v2/schedule-controller.ts | 15 +- .../routes/apis/admin-center-api-router.ts | 1 + .../locales/alb/admin-center/overview.json | 26 +- .../locales/alb/admin-center/settings.json | 17 + .../locales/alb/admin-center/sidebar.json | 1 + .../locales/de/admin-center/overview.json | 26 +- .../locales/de/admin-center/settings.json | 17 + .../locales/de/admin-center/sidebar.json | 1 + .../locales/en/admin-center/overview.json | 26 +- .../locales/en/admin-center/settings.json | 17 + .../locales/en/admin-center/sidebar.json | 1 + .../locales/es/admin-center/overview.json | 26 +- .../locales/es/admin-center/settings.json | 17 + .../locales/es/admin-center/sidebar.json | 1 + .../locales/pt/admin-center/overview.json | 26 +- .../locales/pt/admin-center/settings.json | 17 + .../locales/pt/admin-center/sidebar.json | 1 + .../locales/zh/admin-center/overview.json | 26 +- .../locales/zh/admin-center/settings.json | 17 + .../locales/zh/admin-center/sidebar.json | 1 + .../admin-center/admin-center.api.service.ts | 12 + .../organization-calculation-method.tsx | 120 ++++ .../admin-center/admin-center-constants.ts | 9 + .../pages/admin-center/overview/overview.tsx | 12 +- .../src/pages/admin-center/settings/index.ts | 1 + .../pages/admin-center/settings/settings.tsx | 156 +++++ .../types/admin-center/admin-center.types.ts | 7 +- 28 files changed, 1033 insertions(+), 142 deletions(-) create mode 100644 worklenz-frontend/public/locales/alb/admin-center/settings.json create mode 100644 worklenz-frontend/public/locales/de/admin-center/settings.json create mode 100644 worklenz-frontend/public/locales/en/admin-center/settings.json create mode 100644 worklenz-frontend/public/locales/es/admin-center/settings.json create mode 100644 worklenz-frontend/public/locales/pt/admin-center/settings.json create mode 100644 worklenz-frontend/public/locales/zh/admin-center/settings.json create mode 100644 worklenz-frontend/src/components/admin-center/overview/organization-calculation-method/organization-calculation-method.tsx create mode 100644 worklenz-frontend/src/pages/admin-center/settings/index.ts create mode 100644 worklenz-frontend/src/pages/admin-center/settings/settings.tsx diff --git a/worklenz-backend/src/controllers/admin-center-controller.ts b/worklenz-backend/src/controllers/admin-center-controller.ts index be334aff..3689f52d 100644 --- a/worklenz-backend/src/controllers/admin-center-controller.ts +++ b/worklenz-backend/src/controllers/admin-center-controller.ts @@ -1,30 +1,45 @@ -import {IWorkLenzRequest} from "../interfaces/worklenz-request"; -import {IWorkLenzResponse} from "../interfaces/worklenz-response"; +import { IWorkLenzRequest } from "../interfaces/worklenz-request"; +import { IWorkLenzResponse } from "../interfaces/worklenz-response"; import db from "../config/db"; -import {ServerResponse} from "../models/server-response"; +import { ServerResponse } from "../models/server-response"; import WorklenzControllerBase from "./worklenz-controller-base"; import HandleExceptions from "../decorators/handle-exceptions"; -import {calculateMonthDays, getColor, log_error, megabytesToBytes} from "../shared/utils"; +import { + calculateMonthDays, + getColor, + log_error, + megabytesToBytes, +} from "../shared/utils"; import moment from "moment"; -import {calculateStorage} from "../shared/s3"; -import {checkTeamSubscriptionStatus, getActiveTeamMemberCount, getCurrentProjectsCount, getFreePlanSettings, getOwnerIdByTeam, getTeamMemberCount, getUsedStorage} from "../shared/paddle-utils"; +import { calculateStorage } from "../shared/s3"; +import { + checkTeamSubscriptionStatus, + getActiveTeamMemberCount, + getCurrentProjectsCount, + getFreePlanSettings, + getOwnerIdByTeam, + getTeamMemberCount, + getUsedStorage, +} from "../shared/paddle-utils"; import { addModifier, cancelSubscription, changePlan, generatePayLinkRequest, pauseOrResumeSubscription, - updateUsers + updateUsers, } from "../shared/paddle-requests"; -import {statusExclude} from "../shared/constants"; -import {NotificationsService} from "../services/notifications/notifications.service"; -import {SocketEvents} from "../socket.io/events"; -import {IO} from "../shared/io"; +import { statusExclude } from "../shared/constants"; +import { NotificationsService } from "../services/notifications/notifications.service"; +import { SocketEvents } from "../socket.io/events"; +import { IO } from "../shared/io"; export default class AdminCenterController extends WorklenzControllerBase { - - public static async checkIfUserActiveInOtherTeams(owner_id: string, email: string) { + public static async checkIfUserActiveInOtherTeams( + owner_id: string, + email: string + ) { if (!owner_id) throw new Error("Owner not found."); const q = `SELECT EXISTS(SELECT tmi.team_member_id @@ -41,7 +56,10 @@ export default class AdminCenterController extends WorklenzControllerBase { // organization @HandleExceptions() - public static async getOrganizationDetails(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getOrganizationDetails( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { // const q = `SELECT organization_name AS name, // contact_number, // contact_number_secondary, @@ -62,7 +80,10 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async getOrganizationAdmins(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getOrganizationAdmins( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT u.name, email, owner AS is_owner FROM users u LEFT JOIN team_members tm ON u.id = tm.user_id @@ -77,8 +98,14 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async getOrganizationUsers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const { searchQuery, size, offset } = this.toPaginationOptions(req.query, ["outer_tmiv.name", "outer_tmiv.email"]); + public static async getOrganizationUsers( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { searchQuery, size, offset } = this.toPaginationOptions(req.query, [ + "outer_tmiv.name", + "outer_tmiv.email", + ]); const q = `SELECT ROW_TO_JSON(rec) AS users FROM (SELECT COUNT(*) AS total, @@ -113,8 +140,11 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async updateOrganizationName(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {name} = req.body; + public static async updateOrganizationName( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { name } = req.body; // const q = `UPDATE users_data // SET organization_name = $1 // WHERE user_id = $2;`; @@ -126,8 +156,11 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async updateOwnerContactNumber(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {contact_number} = req.body; + public static async updateOwnerContactNumber( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { contact_number } = req.body; const q = `UPDATE organizations SET contact_number = $1 WHERE user_id = $2;`; @@ -136,7 +169,59 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async updateOrganizationCalculationMethod( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { calculation_method, hours_per_day } = req.body; + + // Validate calculation method + if (!["hourly", "man_days"].includes(calculation_method)) { + return res + .status(400) + .send( + new ServerResponse( + false, + null, + "Invalid calculation method. Must be \"hourly\" or \"man_days\"" + ) + ); + } + + const updateQuery = ` + UPDATE organizations + SET calculation_method = $1, + hours_per_day = COALESCE($2, hours_per_day), + updated_at = NOW() + WHERE user_id = $3 + RETURNING id, organization_name, calculation_method, hours_per_day; + `; + + const result = await db.query(updateQuery, [ + calculation_method, + hours_per_day, + req.user?.owner_id, + ]); + + if (result.rows.length === 0) { + return res + .status(404) + .send(new ServerResponse(false, null, "Organization not found")); + } + + return res.status(200).send( + new ServerResponse(true, { + organization: result.rows[0], + message: "Organization calculation method updated successfully", + }) + ); + } + + @HandleExceptions() + public static async create( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = ``; const result = await db.query(q, []); const [data] = result.rows; @@ -144,14 +229,21 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async getOrganizationTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {searchQuery, size, offset} = this.toPaginationOptions(req.query, ["name"]); + public static async getOrganizationTeams( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { searchQuery, size, offset } = this.toPaginationOptions(req.query, [ + "name", + ]); let size_changed = size; if (offset == 0) size_changed = size_changed - 1; - const currentTeamClosure = offset == 0 ? `, + const currentTeamClosure = + offset == 0 + ? `, (SELECT COALESCE(ROW_TO_JSON(c), '{}'::JSON) FROM (SELECT id, name, @@ -168,7 +260,8 @@ export default class AdminCenterController extends WorklenzControllerBase { LEFT JOIN users u on team_members.user_id = u.id WHERE team_id = teams.id) rec) AS team_members FROM teams - WHERE user_id = $1 AND teams.id = $4) c) AS current_team_data` : ``; + WHERE user_id = $1 AND teams.id = $4) c) AS current_team_data` + : ``; const q = `SELECT ROW_TO_JSON(rec) AS teams FROM (SELECT COUNT(*) AS total, @@ -194,26 +287,38 @@ export default class AdminCenterController extends WorklenzControllerBase { ${currentTeamClosure} FROM teams WHERE user_id = $1 ${searchQuery}) rec;`; - const result = await db.query(q, [req.user?.owner_id, size_changed, offset, req.user?.team_id]); + const result = await db.query(q, [ + req.user?.owner_id, + size_changed, + offset, + req.user?.team_id, + ]); const [obj] = result.rows; for (const team of obj.teams?.data || []) { team.names = this.createTagList(team?.team_members); - team.names.map((a: any) => a.color_code = getColor(a.name)); + team.names.map((a: any) => (a.color_code = getColor(a.name))); } if (obj.teams.current_team_data) { - obj.teams.current_team_data.names = this.createTagList(obj.teams.current_team_data?.team_members); - obj.teams.current_team_data.names.map((a: any) => a.color_code = getColor(a.name)); + obj.teams.current_team_data.names = this.createTagList( + obj.teams.current_team_data?.team_members + ); + obj.teams.current_team_data.names.map( + (a: any) => (a.color_code = getColor(a.name)) + ); } return res.status(200).send(new ServerResponse(true, obj.teams || {})); } @HandleExceptions() - public static async getTeamDetails(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {id} = req.params; + public static async getTeamDetails( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { id } = req.params; const q = `SELECT id, name, @@ -249,47 +354,63 @@ export default class AdminCenterController extends WorklenzControllerBase { const [obj] = result.rows; obj.names = this.createTagList(obj?.team_members); - obj.names.map((a: any) => a.color_code = getColor(a.name)); + obj.names.map((a: any) => (a.color_code = getColor(a.name))); return res.status(200).send(new ServerResponse(true, obj || {})); } @HandleExceptions() - public static async updateTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {id} = req.params; - const {name, teamMembers} = req.body; + public static async updateTeam( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { id } = req.params; + const { name, teamMembers } = req.body; try { // Update team name const updateNameQuery = `UPDATE teams SET name = $1 WHERE id = $2 RETURNING id;`; const nameResult = await db.query(updateNameQuery, [name, id]); - + if (!nameResult.rows.length) { - return res.status(404).send(new ServerResponse(false, null, "Team not found")); + return res + .status(404) + .send(new ServerResponse(false, null, "Team not found")); } // Update team member roles if provided if (teamMembers?.length) { // Use Promise.all to handle all role updates concurrently - await Promise.all(teamMembers.map(async (member: { role_name: string; user_id: string; }) => { - const roleQuery = ` + await Promise.all( + teamMembers.map( + async (member: { role_name: string; user_id: string }) => { + const roleQuery = ` UPDATE team_members SET role_id = (SELECT id FROM roles WHERE roles.team_id = $1 AND name = $2) WHERE user_id = $3 AND team_id = $1 RETURNING id;`; - await db.query(roleQuery, [id, member.role_name, member.user_id]); - })); + await db.query(roleQuery, [id, member.role_name, member.user_id]); + } + ) + ); } - return res.status(200).send(new ServerResponse(true, null, "Team updated successfully")); + return res + .status(200) + .send(new ServerResponse(true, null, "Team updated successfully")); } catch (error) { log_error("Error updating team:", error); - return res.status(500).send(new ServerResponse(false, null, "Failed to update team")); + return res + .status(500) + .send(new ServerResponse(false, null, "Failed to update team")); } } @HandleExceptions() - public static async getBillingInfo(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getBillingInfo( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT get_billing_info($1) AS billing_info;`; const result = await db.query(q, [req.user?.owner_id]); const [data] = result.rows; @@ -297,7 +418,10 @@ export default class AdminCenterController extends WorklenzControllerBase { const validTillDate = moment(data.billing_info.trial_expire_date); const daysDifference = validTillDate.diff(moment(), "days"); - const dateString = calculateMonthDays(moment().format("YYYY-MM-DD"), data.billing_info.trial_expire_date); + const dateString = calculateMonthDays( + moment().format("YYYY-MM-DD"), + data.billing_info.trial_expire_date + ); data.billing_info.expire_date_string = dateString; @@ -309,10 +433,14 @@ export default class AdminCenterController extends WorklenzControllerBase { data.billing_info.expire_date_string = `Your trial plan expires in ${dateString}.`; } - if (data.billing_info.billing_type === "year") data.billing_info.unit_price_per_month = data.billing_info.unit_price / 12; + if (data.billing_info.billing_type === "year") + data.billing_info.unit_price_per_month = + data.billing_info.unit_price / 12; const teamMemberData = await getTeamMemberCount(req.user?.owner_id ?? ""); - const subscriptionData = await checkTeamSubscriptionStatus(req.user?.team_id ?? ""); + const subscriptionData = await checkTeamSubscriptionStatus( + req.user?.team_id ?? "" + ); data.billing_info.total_used = teamMemberData.user_count; data.billing_info.total_seats = subscriptionData.quantity; @@ -321,7 +449,10 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async getBillingTransactions(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getBillingTransactions( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT subscription_payment_id, event_time::date, (next_bill_date::DATE - INTERVAL '1 day')::DATE AS next_bill_date, @@ -339,7 +470,10 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async getBillingCharges(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getBillingCharges( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT (SELECT name FROM licensing_pricing_plans lpp WHERE id = lus.plan_id), unit_price::numeric, currency, @@ -365,11 +499,21 @@ export default class AdminCenterController extends WorklenzControllerBase { LIMIT 1)::INT;`; const countResult = await db.query(countQ, [req.user?.owner_id]); - return res.status(200).send(new ServerResponse(true, {plan_charges: result.rows, modifiers: countResult.rows})); + return res + .status(200) + .send( + new ServerResponse(true, { + plan_charges: result.rows, + modifiers: countResult.rows, + }) + ); } @HandleExceptions() - public static async getBillingModifiers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getBillingModifiers( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT created_at FROM licensing_user_subscription_modifiers WHERE subscription_id = (SELECT subscription_id @@ -383,7 +527,10 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async getBillingConfiguration(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getBillingConfiguration( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT name, email, organization_name AS company_name, @@ -404,8 +551,20 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async updateBillingConfiguration(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {company_name, phone, address_line_1, address_line_2, city, state, postal_code, country} = req.body; + public static async updateBillingConfiguration( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { + company_name, + phone, + address_line_1, + address_line_2, + city, + state, + postal_code, + country, + } = req.body; const q = `UPDATE organizations SET organization_name = $1, contact_number = $2, @@ -416,24 +575,47 @@ export default class AdminCenterController extends WorklenzControllerBase { postal_code = $7, country = $8 WHERE user_id = $9;`; - const result = await db.query(q, [company_name, phone, address_line_1, address_line_2, city, state, postal_code, country, req.user?.owner_id]); + const result = await db.query(q, [ + company_name, + phone, + address_line_1, + address_line_2, + city, + state, + postal_code, + country, + req.user?.owner_id, + ]); const [data] = result.rows; - return res.status(200).send(new ServerResponse(true, data, "Configuration Updated")); + return res + .status(200) + .send(new ServerResponse(true, data, "Configuration Updated")); } @HandleExceptions() - public static async upgradePlan(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {plan} = req.query; + public static async upgradePlan( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { plan } = req.query; const obj = await getTeamMemberCount(req.user?.owner_id ?? ""); - const axiosResponse = await generatePayLinkRequest(obj, plan as string, req.user?.owner_id, req.user?.id); + const axiosResponse = await generatePayLinkRequest( + obj, + plan as string, + req.user?.owner_id, + req.user?.id + ); return res.status(200).send(new ServerResponse(true, axiosResponse.body)); } @HandleExceptions() - public static async getPlans(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getPlans( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT ls.default_monthly_plan AS monthly_plan_id, lp_monthly.name AS monthly_plan_name, @@ -455,8 +637,10 @@ export default class AdminCenterController extends WorklenzControllerBase { const obj = await getTeamMemberCount(req.user?.owner_id ?? ""); - data.team_member_limit = data.team_member_limit === 0 ? "Unlimited" : data.team_member_limit; - data.projects_limit = data.projects_limit === 0 ? "Unlimited" : data.projects_limit; + data.team_member_limit = + data.team_member_limit === 0 ? "Unlimited" : data.team_member_limit; + data.projects_limit = + data.projects_limit === 0 ? "Unlimited" : data.projects_limit; data.free_tier_storage = `${data.free_tier_storage}MB`; data.current_user_count = obj.user_count; data.annual_price = (data.annual_price / 12).toFixed(2); @@ -465,7 +649,10 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async purchaseStorage(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async purchaseStorage( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT subscription_id FROM licensing_user_subscriptions lus WHERE user_id = $1;`; @@ -478,8 +665,11 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async changePlan(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {plan} = req.query; + public static async changePlan( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { plan } = req.query; const q = `SELECT subscription_id FROM licensing_user_subscriptions lus @@ -487,14 +677,23 @@ export default class AdminCenterController extends WorklenzControllerBase { const result = await db.query(q, [req.user?.owner_id]); const [data] = result.rows; - const axiosResponse = await changePlan(plan as string, data.subscription_id); + const axiosResponse = await changePlan( + plan as string, + data.subscription_id + ); return res.status(200).send(new ServerResponse(true, axiosResponse.body)); } @HandleExceptions() - public static async cancelPlan(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - if (!req.user?.owner_id) return res.status(200).send(new ServerResponse(false, "Invalid Request.")); + public static async cancelPlan( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + if (!req.user?.owner_id) + return res + .status(200) + .send(new ServerResponse(false, "Invalid Request.")); const q = `SELECT subscription_id FROM licensing_user_subscriptions lus @@ -502,14 +701,23 @@ export default class AdminCenterController extends WorklenzControllerBase { const result = await db.query(q, [req.user?.owner_id]); const [data] = result.rows; - const axiosResponse = await cancelSubscription(data.subscription_id, req.user?.owner_id); + const axiosResponse = await cancelSubscription( + data.subscription_id, + req.user?.owner_id + ); return res.status(200).send(new ServerResponse(true, axiosResponse.body)); } @HandleExceptions() - public static async pauseSubscription(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - if (!req.user?.owner_id) return res.status(200).send(new ServerResponse(false, "Invalid Request.")); + public static async pauseSubscription( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + if (!req.user?.owner_id) + return res + .status(200) + .send(new ServerResponse(false, "Invalid Request.")); const q = `SELECT subscription_id FROM licensing_user_subscriptions lus @@ -517,14 +725,24 @@ export default class AdminCenterController extends WorklenzControllerBase { const result = await db.query(q, [req.user?.owner_id]); const [data] = result.rows; - const axiosResponse = await pauseOrResumeSubscription(data.subscription_id, req.user?.owner_id, true); + const axiosResponse = await pauseOrResumeSubscription( + data.subscription_id, + req.user?.owner_id, + true + ); return res.status(200).send(new ServerResponse(true, axiosResponse.body)); } @HandleExceptions() - public static async resumeSubscription(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - if (!req.user?.owner_id) return res.status(200).send(new ServerResponse(false, "Invalid Request.")); + public static async resumeSubscription( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + if (!req.user?.owner_id) + return res + .status(200) + .send(new ServerResponse(false, "Invalid Request.")); const q = `SELECT subscription_id FROM licensing_user_subscriptions lus @@ -532,13 +750,20 @@ export default class AdminCenterController extends WorklenzControllerBase { const result = await db.query(q, [req.user?.owner_id]); const [data] = result.rows; - const axiosResponse = await pauseOrResumeSubscription(data.subscription_id, req.user?.owner_id, false); + const axiosResponse = await pauseOrResumeSubscription( + data.subscription_id, + req.user?.owner_id, + false + ); return res.status(200).send(new ServerResponse(true, axiosResponse.body)); } @HandleExceptions() - public static async getBillingStorageInfo(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getBillingStorageInfo( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT trial_in_progress, trial_expire_date, ud.storage, @@ -557,7 +782,10 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async getAccountStorage(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getAccountStorage( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const teamsQ = `SELECT id FROM teams WHERE user_id = $1;`; @@ -577,14 +805,19 @@ export default class AdminCenterController extends WorklenzControllerBase { storage.used += await calculateStorage(team.id); } - storage.remaining = (storage.total * 1024 * 1024 * 1024) - storage.used; - storage.used_percent = Math.ceil((storage.used / (storage.total * 1024 * 1024 * 1024)) * 10000) / 100; + storage.remaining = storage.total * 1024 * 1024 * 1024 - storage.used; + storage.used_percent = + Math.ceil((storage.used / (storage.total * 1024 * 1024 * 1024)) * 10000) / + 100; return res.status(200).send(new ServerResponse(true, storage)); } @HandleExceptions() - public static async getCountries(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getCountries( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const q = `SELECT id, name, code FROM countries ORDER BY name;`; @@ -594,7 +827,10 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async switchToFreePlan(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async switchToFreePlan( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const { id: teamId } = req.params; const limits = await getFreePlanSettings(); @@ -604,18 +840,45 @@ export default class AdminCenterController extends WorklenzControllerBase { if (parseInt(limits.team_member_limit) !== 0) { const teamMemberCount = await getTeamMemberCount(ownerId); if (parseInt(teamMemberCount) > parseInt(limits.team_member_limit)) { - return res.status(200).send(new ServerResponse(false, [], `Sorry, the free plan cannot have more than ${limits.team_member_limit} members.`)); + return res + .status(200) + .send( + new ServerResponse( + false, + [], + `Sorry, the free plan cannot have more than ${limits.team_member_limit} members.` + ) + ); } } const projectsCount = await getCurrentProjectsCount(ownerId); if (parseInt(projectsCount) > parseInt(limits.projects_limit)) { - return res.status(200).send(new ServerResponse(false, [], `Sorry, the free plan cannot have more than ${limits.projects_limit} projects.`)); + return res + .status(200) + .send( + new ServerResponse( + false, + [], + `Sorry, the free plan cannot have more than ${limits.projects_limit} projects.` + ) + ); } const usedStorage = await getUsedStorage(ownerId); - if (parseInt(usedStorage) > megabytesToBytes(parseInt(limits.free_tier_storage))) { - return res.status(200).send(new ServerResponse(false, [], `Sorry, the free plan cannot exceed ${limits.free_tier_storage}MB of storage.`)); + if ( + parseInt(usedStorage) > + megabytesToBytes(parseInt(limits.free_tier_storage)) + ) { + return res + .status(200) + .send( + new ServerResponse( + false, + [], + `Sorry, the free plan cannot exceed ${limits.free_tier_storage}MB of storage.` + ) + ); } const update_q = `UPDATE organizations @@ -626,13 +889,32 @@ export default class AdminCenterController extends WorklenzControllerBase { WHERE user_id = $1;`; await db.query(update_q, [ownerId]); - return res.status(200).send(new ServerResponse(true, [], "Your plan has been successfully switched to the Free Plan.")); + return res + .status(200) + .send( + new ServerResponse( + true, + [], + "Your plan has been successfully switched to the Free Plan." + ) + ); } - return res.status(200).send(new ServerResponse(false, [], "Failed to switch to the Free Plan. Please try again later.")); + return res + .status(200) + .send( + new ServerResponse( + false, + [], + "Failed to switch to the Free Plan. Please try again later." + ) + ); } @HandleExceptions() - public static async redeem(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async redeem( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const { code } = req.body; const q = `SELECT * FROM licensing_coupon_codes WHERE coupon_code = $1 AND is_redeemed IS FALSE AND is_refunded IS FALSE;`; @@ -640,14 +922,26 @@ export default class AdminCenterController extends WorklenzControllerBase { const [data] = result.rows; if (!result.rows.length) - return res.status(200).send(new ServerResponse(false, [], "Redeem Code verification Failed! Please try again.")); + return res + .status(200) + .send( + new ServerResponse( + false, + [], + "Redeem Code verification Failed! Please try again." + ) + ); const checkQ = `SELECT sum(team_members_limit) AS team_member_total FROM licensing_coupon_codes WHERE redeemed_by = $1 AND is_redeemed IS TRUE;`; const checkResult = await db.query(checkQ, [req.user?.owner_id]); const [total] = checkResult.rows; if (parseInt(total.team_member_total) > 50) - return res.status(200).send(new ServerResponse(false, [], "Maximum number of codes redeemed!")); + return res + .status(200) + .send( + new ServerResponse(false, [], "Maximum number of codes redeemed!") + ); const updateQ = `UPDATE licensing_coupon_codes SET is_redeemed = TRUE, redeemed_at = CURRENT_TIMESTAMP, @@ -663,16 +957,28 @@ export default class AdminCenterController extends WorklenzControllerBase { WHERE user_id = $1;`; await db.query(updateQ2, [req.user?.owner_id]); - return res.status(200).send(new ServerResponse(true, [], "Code redeemed successfully!")); + return res + .status(200) + .send(new ServerResponse(true, [], "Code redeemed successfully!")); } @HandleExceptions() - public static async deleteTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {id} = req.params; + public static async deleteTeam( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { id } = req.params; if (id == req.user?.team_id) { - return res.status(200).send(new ServerResponse(true, [], "Please switch to another team before attempting deletion.") - .withTitle("Unable to remove the presently active team!")); + return res + .status(200) + .send( + new ServerResponse( + true, + [], + "Please switch to another team before attempting deletion." + ).withTitle("Unable to remove the presently active team!") + ); } const q = `DELETE FROM teams WHERE id = $1;`; @@ -682,16 +988,26 @@ export default class AdminCenterController extends WorklenzControllerBase { } @HandleExceptions() - public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const {id} = req.params; - const {teamId} = req.body; + public static async deleteById( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { id } = req.params; + const { teamId } = req.body; - if (!id || !teamId) return res.status(200).send(new ServerResponse(false, "Required fields are missing.")); + if (!id || !teamId) + return res + .status(200) + .send(new ServerResponse(false, "Required fields are missing.")); // check subscription status const subscriptionData = await checkTeamSubscriptionStatus(teamId); if (statusExclude.includes(subscriptionData.subscription_status)) { - return res.status(200).send(new ServerResponse(false, "Please check your subscription status.")); + return res + .status(200) + .send( + new ServerResponse(false, "Please check your subscription status.") + ); } const q = `SELECT remove_team_member($1, $2, $3) AS member;`; @@ -702,17 +1018,32 @@ export default class AdminCenterController extends WorklenzControllerBase { // if (subscriptionData.status === "trialing") break; if (!subscriptionData.is_credit && !subscriptionData.is_custom) { - if (subscriptionData.subscription_status === "active" && subscriptionData.quantity > 0) { - + if ( + subscriptionData.subscription_status === "active" && + subscriptionData.quantity > 0 + ) { const obj = await getActiveTeamMemberCount(req.user?.owner_id ?? ""); - const userActiveInOtherTeams = await this.checkIfUserActiveInOtherTeams(req.user?.owner_id as string, req.query?.email as string); + const userActiveInOtherTeams = await this.checkIfUserActiveInOtherTeams( + req.user?.owner_id as string, + req.query?.email as string + ); if (!userActiveInOtherTeams) { - const response = await updateUsers(subscriptionData.subscription_id, obj.user_count); - if (!response.body.subscription_id) return res.status(200).send(new ServerResponse(false, response.message || "Please check your subscription.")); + const response = await updateUsers( + subscriptionData.subscription_id, + obj.user_count + ); + if (!response.body.subscription_id) + return res + .status(200) + .send( + new ServerResponse( + false, + response.message || "Please check your subscription." + ) + ); } - } } @@ -720,26 +1051,39 @@ export default class AdminCenterController extends WorklenzControllerBase { receiver_socket_id: data.socket_id, message, team: data.team, - team_id: id + team_id: id, }); - IO.emitByUserId(data.member.id, req.user?.id || null, SocketEvents.TEAM_MEMBER_REMOVED, { - teamId: id, - message - }); + IO.emitByUserId( + data.member.id, + req.user?.id || null, + SocketEvents.TEAM_MEMBER_REMOVED, + { + teamId: id, + message, + } + ); return res.status(200).send(new ServerResponse(true, result.rows)); } @HandleExceptions() - public static async getFreePlanLimits(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getFreePlanLimits( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { const limits = await getFreePlanSettings(); return res.status(200).send(new ServerResponse(true, limits || {})); } - + @HandleExceptions() - public static async getOrganizationProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const { searchQuery, size, offset } = this.toPaginationOptions(req.query, ["p.name"]); + public static async getOrganizationProjects( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { searchQuery, size, offset } = this.toPaginationOptions(req.query, [ + "p.name", + ]); const countQ = `SELECT COUNT(*) AS total FROM projects p @@ -770,7 +1114,7 @@ export default class AdminCenterController extends WorklenzControllerBase { const response = { total: countResult.rows[0]?.total ?? 0, - data: result.rows ?? [] + data: result.rows ?? [], }; return res.status(200).send(new ServerResponse(true, response)); diff --git a/worklenz-backend/src/controllers/schedule-v2/schedule-controller.ts b/worklenz-backend/src/controllers/schedule-v2/schedule-controller.ts index a008c06d..6af8e411 100644 --- a/worklenz-backend/src/controllers/schedule-v2/schedule-controller.ts +++ b/worklenz-backend/src/controllers/schedule-v2/schedule-controller.ts @@ -53,13 +53,13 @@ export default class ScheduleControllerV2 extends WorklenzControllerBase { const [workingDays] = workingDaysResults.rows; // get organization working hours - const getDataHoursq = `SELECT working_hours FROM organizations WHERE user_id = $1 GROUP BY id LIMIT 1;`; + const getDataHoursq = `SELECT hours_per_day FROM organizations WHERE user_id = $1 GROUP BY id LIMIT 1;`; const workingHoursResults = await db.query(getDataHoursq, [req.user?.owner_id]); const [workingHours] = workingHoursResults.rows; - return res.status(200).send(new ServerResponse(true, { workingDays: workingDays?.working_days, workingHours: workingHours?.working_hours })); + return res.status(200).send(new ServerResponse(true, { workingDays: workingDays?.working_days, workingHours: workingHours?.hours_per_day })); } @HandleExceptions() @@ -74,18 +74,13 @@ export default class ScheduleControllerV2 extends WorklenzControllerBase { .map(day => `${day.toLowerCase()} = ${workingDays.includes(day)}`) .join(", "); - const updateQuery = ` - UPDATE public.organization_working_days + const updateQuery = `UPDATE public.organization_working_days SET ${setClause}, updated_at = CURRENT_TIMESTAMP - WHERE organization_id IN ( - SELECT organization_id FROM organizations - WHERE user_id = $1 - ); - `; + WHERE organization_id IN (SELECT id FROM organizations WHERE user_id = $1);`; await db.query(updateQuery, [req.user?.owner_id]); - const getDataHoursq = `UPDATE organizations SET working_hours = $1 WHERE user_id = $2;`; + const getDataHoursq = `UPDATE organizations SET hours_per_day = $1 WHERE user_id = $2;`; await db.query(getDataHoursq, [workingHours, req.user?.owner_id]); 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 e49fba65..318bcf63 100644 --- a/worklenz-backend/src/routes/apis/admin-center-api-router.ts +++ b/worklenz-backend/src/routes/apis/admin-center-api-router.ts @@ -11,6 +11,7 @@ const adminCenterApiRouter = express.Router(); adminCenterApiRouter.get("/organization", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationDetails)); adminCenterApiRouter.get("/organization/admins", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationAdmins)); adminCenterApiRouter.put("/organization", teamOwnerOrAdminValidator, organizationSettingsValidator, safeControllerFunction(AdminCenterController.updateOrganizationName)); +adminCenterApiRouter.put("/organization/calculation-method", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOrganizationCalculationMethod)); adminCenterApiRouter.put("/organization/owner/contact-number", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOwnerContactNumber)); // users diff --git a/worklenz-frontend/public/locales/alb/admin-center/overview.json b/worklenz-frontend/public/locales/alb/admin-center/overview.json index 296eae4c..9a562e12 100644 --- a/worklenz-frontend/public/locales/alb/admin-center/overview.json +++ b/worklenz-frontend/public/locales/alb/admin-center/overview.json @@ -4,5 +4,29 @@ "owner": "Pronari i Organizatës", "admins": "Administruesit e Organizatës", "contactNumber": "Shto Numrin e Kontaktit", - "edit": "Redakto" + "edit": "Redakto", + "organizationWorkingDaysAndHours": "Ditët dhe Orët e Punës së Organizatës", + "workingDays": "Ditët e Punës", + "workingHours": "Orët e Punës", + "monday": "E Hënë", + "tuesday": "E Martë", + "wednesday": "E Mërkurë", + "thursday": "E Enjte", + "friday": "E Premte", + "saturday": "E Shtunë", + "sunday": "E Dielë", + "hours": "orë", + "saveButton": "Ruaj", + "saved": "Cilësimet u ruajtën me sukses", + "errorSaving": "Gabim gjatë ruajtjes së cilësimeve", + "organizationCalculationMethod": "Metoda e Llogaritjes së Organizatës", + "calculationMethod": "Metoda e Llogaritjes", + "hourlyRates": "Normat Orërore", + "manDays": "Ditët e Njeriut", + "saveChanges": "Ruaj Ndryshimet", + "hourlyCalculationDescription": "Të gjitha kostot e projektit do të llogariten duke përdorur orët e vlerësuara × normat orërore", + "manDaysCalculationDescription": "Të gjitha kostot e projektit do të llogariten duke përdorur ditët e vlerësuara të njeriut × normat ditore", + "calculationMethodTooltip": "Ky cilësim zbatohet për të gjitha projektet në organizatën tuaj", + "calculationMethodUpdated": "Metoda e llogaritjes së organizatës u përditësua me sukses", + "calculationMethodUpdateError": "Dështoi përditësimi i metodës së llogaritjes" } diff --git a/worklenz-frontend/public/locales/alb/admin-center/settings.json b/worklenz-frontend/public/locales/alb/admin-center/settings.json new file mode 100644 index 00000000..33ee2e6e --- /dev/null +++ b/worklenz-frontend/public/locales/alb/admin-center/settings.json @@ -0,0 +1,17 @@ +{ + "settings": "Cilësimet", + "organizationWorkingDaysAndHours": "Ditët dhe Orët e Punës së Organizatës", + "workingDays": "Ditët e Punës", + "workingHours": "Orët e Punës", + "hours": "orë", + "monday": "E Hënë", + "tuesday": "E Martë", + "wednesday": "E Mërkurë", + "thursday": "E Enjte", + "friday": "E Premte", + "saturday": "E Shtunë", + "sunday": "E Dielë", + "saveButton": "Ruaj", + "saved": "Cilësimet u ruajtën me sukses", + "errorSaving": "Gabim gjatë ruajtjes së cilësimeve" +} \ No newline at end of file diff --git a/worklenz-frontend/public/locales/alb/admin-center/sidebar.json b/worklenz-frontend/public/locales/alb/admin-center/sidebar.json index 584a9a10..b63de15b 100644 --- a/worklenz-frontend/public/locales/alb/admin-center/sidebar.json +++ b/worklenz-frontend/public/locales/alb/admin-center/sidebar.json @@ -4,5 +4,6 @@ "teams": "Ekipet", "billing": "Faturimi", "projects": "Projektet", + "settings": "Cilësimet", "adminCenter": "Qendra Administrative" } diff --git a/worklenz-frontend/public/locales/de/admin-center/overview.json b/worklenz-frontend/public/locales/de/admin-center/overview.json index 0330d788..23bc7faa 100644 --- a/worklenz-frontend/public/locales/de/admin-center/overview.json +++ b/worklenz-frontend/public/locales/de/admin-center/overview.json @@ -4,5 +4,29 @@ "owner": "Organisationsinhaber", "admins": "Organisationsadministratoren", "contactNumber": "Kontaktnummer hinzufügen", - "edit": "Bearbeiten" + "edit": "Bearbeiten", + "organizationWorkingDaysAndHours": "Arbeitstage und -stunden der Organisation", + "workingDays": "Arbeitstage", + "workingHours": "Arbeitsstunden", + "monday": "Montag", + "tuesday": "Dienstag", + "wednesday": "Mittwoch", + "thursday": "Donnerstag", + "friday": "Freitag", + "saturday": "Samstag", + "sunday": "Sonntag", + "hours": "Stunden", + "saveButton": "Speichern", + "saved": "Einstellungen erfolgreich gespeichert", + "errorSaving": "Fehler beim Speichern der Einstellungen", + "organizationCalculationMethod": "Organisations-Berechnungsmethode", + "calculationMethod": "Berechnungsmethode", + "hourlyRates": "Stundensätze", + "manDays": "Mann-Tage", + "saveChanges": "Änderungen speichern", + "hourlyCalculationDescription": "Alle Projektkosten werden anhand geschätzter Stunden × Stundensätze berechnet", + "manDaysCalculationDescription": "Alle Projektkosten werden anhand geschätzter Mann-Tage × Tagessätze berechnet", + "calculationMethodTooltip": "Diese Einstellung gilt für alle Projekte in Ihrer Organisation", + "calculationMethodUpdated": "Organisations-Berechnungsmethode erfolgreich aktualisiert", + "calculationMethodUpdateError": "Fehler beim Aktualisieren der Berechnungsmethode" } diff --git a/worklenz-frontend/public/locales/de/admin-center/settings.json b/worklenz-frontend/public/locales/de/admin-center/settings.json new file mode 100644 index 00000000..41e6515e --- /dev/null +++ b/worklenz-frontend/public/locales/de/admin-center/settings.json @@ -0,0 +1,17 @@ +{ + "settings": "Einstellungen", + "organizationWorkingDaysAndHours": "Arbeitstage und -stunden der Organisation", + "workingDays": "Arbeitstage", + "workingHours": "Arbeitsstunden", + "hours": "Stunden", + "monday": "Montag", + "tuesday": "Dienstag", + "wednesday": "Mittwoch", + "thursday": "Donnerstag", + "friday": "Freitag", + "saturday": "Samstag", + "sunday": "Sonntag", + "saveButton": "Speichern", + "saved": "Einstellungen erfolgreich gespeichert", + "errorSaving": "Fehler beim Speichern der Einstellungen" +} \ No newline at end of file diff --git a/worklenz-frontend/public/locales/de/admin-center/sidebar.json b/worklenz-frontend/public/locales/de/admin-center/sidebar.json index 670595a3..ad375398 100644 --- a/worklenz-frontend/public/locales/de/admin-center/sidebar.json +++ b/worklenz-frontend/public/locales/de/admin-center/sidebar.json @@ -4,5 +4,6 @@ "teams": "Teams", "billing": "Abrechnung", "projects": "Projekte", + "settings": "Einstellungen", "adminCenter": "Admin-Center" } diff --git a/worklenz-frontend/public/locales/en/admin-center/overview.json b/worklenz-frontend/public/locales/en/admin-center/overview.json index efc42855..24ee6a36 100644 --- a/worklenz-frontend/public/locales/en/admin-center/overview.json +++ b/worklenz-frontend/public/locales/en/admin-center/overview.json @@ -4,5 +4,29 @@ "owner": "Organization Owner", "admins": "Organization Admins", "contactNumber": "Add Contact Number", - "edit": "Edit" + "edit": "Edit", + "organizationWorkingDaysAndHours": "Organization Working Days & Hours", + "workingDays": "Working Days", + "workingHours": "Working Hours", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday", + "hours": "hours", + "saveButton": "Save", + "saved": "Settings saved successfully", + "errorSaving": "Error saving settings", + "organizationCalculationMethod": "Organization Calculation Method", + "calculationMethod": "Calculation Method", + "hourlyRates": "Hourly Rates", + "manDays": "Man Days", + "saveChanges": "Save Changes", + "hourlyCalculationDescription": "All project costs will be calculated using estimated hours × hourly rates", + "manDaysCalculationDescription": "All project costs will be calculated using estimated man days × daily rates", + "calculationMethodTooltip": "This setting applies to all projects in your organization", + "calculationMethodUpdated": "Organization calculation method updated successfully", + "calculationMethodUpdateError": "Failed to update calculation method" } diff --git a/worklenz-frontend/public/locales/en/admin-center/settings.json b/worklenz-frontend/public/locales/en/admin-center/settings.json new file mode 100644 index 00000000..b0ccbc1d --- /dev/null +++ b/worklenz-frontend/public/locales/en/admin-center/settings.json @@ -0,0 +1,17 @@ +{ + "settings": "Settings", + "organizationWorkingDaysAndHours": "Organization Working Days & Hours", + "workingDays": "Working Days", + "workingHours": "Working Hours", + "hours": "hours", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday", + "saveButton": "Save", + "saved": "Settings saved successfully", + "errorSaving": "Error saving settings" +} \ No newline at end of file diff --git a/worklenz-frontend/public/locales/en/admin-center/sidebar.json b/worklenz-frontend/public/locales/en/admin-center/sidebar.json index 3b03d499..3ed41e1b 100644 --- a/worklenz-frontend/public/locales/en/admin-center/sidebar.json +++ b/worklenz-frontend/public/locales/en/admin-center/sidebar.json @@ -4,5 +4,6 @@ "teams": "Teams", "billing": "Billing", "projects": "Projects", + "settings": "Settings", "adminCenter": "Admin Center" } diff --git a/worklenz-frontend/public/locales/es/admin-center/overview.json b/worklenz-frontend/public/locales/es/admin-center/overview.json index f88dbdf6..c1fc6309 100644 --- a/worklenz-frontend/public/locales/es/admin-center/overview.json +++ b/worklenz-frontend/public/locales/es/admin-center/overview.json @@ -4,5 +4,29 @@ "owner": "Propietario de la Organización", "admins": "Administradores de la Organización", "contactNumber": "Agregar Número de Contacto", - "edit": "Editar" + "edit": "Editar", + "organizationWorkingDaysAndHours": "Días y Horas Laborales de la Organización", + "workingDays": "Días Laborales", + "workingHours": "Horas Laborales", + "monday": "Lunes", + "tuesday": "Martes", + "wednesday": "Miércoles", + "thursday": "Jueves", + "friday": "Viernes", + "saturday": "Sábado", + "sunday": "Domingo", + "hours": "horas", + "saveButton": "Guardar", + "saved": "Configuración guardada exitosamente", + "errorSaving": "Error al guardar la configuración", + "organizationCalculationMethod": "Método de Cálculo de la Organización", + "calculationMethod": "Método de Cálculo", + "hourlyRates": "Tarifas por Hora", + "manDays": "Días Hombre", + "saveChanges": "Guardar Cambios", + "hourlyCalculationDescription": "Todos los costos del proyecto se calcularán usando horas estimadas × tarifas por hora", + "manDaysCalculationDescription": "Todos los costos del proyecto se calcularán usando días hombre estimados × tarifas diarias", + "calculationMethodTooltip": "Esta configuración se aplica a todos los proyectos en su organización", + "calculationMethodUpdated": "Método de cálculo de la organización actualizado exitosamente", + "calculationMethodUpdateError": "Error al actualizar el método de cálculo" } diff --git a/worklenz-frontend/public/locales/es/admin-center/settings.json b/worklenz-frontend/public/locales/es/admin-center/settings.json new file mode 100644 index 00000000..9539e975 --- /dev/null +++ b/worklenz-frontend/public/locales/es/admin-center/settings.json @@ -0,0 +1,17 @@ +{ + "settings": "Configuración", + "organizationWorkingDaysAndHours": "Días y Horas Laborales de la Organización", + "workingDays": "Días Laborales", + "workingHours": "Horas Laborales", + "hours": "horas", + "monday": "Lunes", + "tuesday": "Martes", + "wednesday": "Miércoles", + "thursday": "Jueves", + "friday": "Viernes", + "saturday": "Sábado", + "sunday": "Domingo", + "saveButton": "Guardar", + "saved": "Configuración guardada exitosamente", + "errorSaving": "Error al guardar la configuración" +} \ No newline at end of file diff --git a/worklenz-frontend/public/locales/es/admin-center/sidebar.json b/worklenz-frontend/public/locales/es/admin-center/sidebar.json index 7626302c..a50edfed 100644 --- a/worklenz-frontend/public/locales/es/admin-center/sidebar.json +++ b/worklenz-frontend/public/locales/es/admin-center/sidebar.json @@ -4,5 +4,6 @@ "teams": "Equipos", "billing": "Facturación", "projects": "Proyectos", + "settings": "Configuración", "adminCenter": "Centro de Administración" } diff --git a/worklenz-frontend/public/locales/pt/admin-center/overview.json b/worklenz-frontend/public/locales/pt/admin-center/overview.json index 7cce8587..767a8afe 100644 --- a/worklenz-frontend/public/locales/pt/admin-center/overview.json +++ b/worklenz-frontend/public/locales/pt/admin-center/overview.json @@ -4,5 +4,29 @@ "owner": "Proprietário da Organização", "admins": "Administradores da Organização", "contactNumber": "Adicione o Número de Contato", - "edit": "Editar" + "edit": "Editar", + "organizationWorkingDaysAndHours": "Dias e Horas de Trabalho da Organização", + "workingDays": "Dias de Trabalho", + "workingHours": "Horas de Trabalho", + "monday": "Segunda-feira", + "tuesday": "Terça-feira", + "wednesday": "Quarta-feira", + "thursday": "Quinta-feira", + "friday": "Sexta-feira", + "saturday": "Sábado", + "sunday": "Domingo", + "hours": "horas", + "saveButton": "Salvar", + "saved": "Configurações salvas com sucesso", + "errorSaving": "Erro ao salvar configurações", + "organizationCalculationMethod": "Método de Cálculo da Organização", + "calculationMethod": "Método de Cálculo", + "hourlyRates": "Taxas por Hora", + "manDays": "Dias Homem", + "saveChanges": "Salvar Alterações", + "hourlyCalculationDescription": "Todos os custos do projeto serão calculados usando horas estimadas × taxas por hora", + "manDaysCalculationDescription": "Todos os custos do projeto serão calculados usando dias homem estimados × taxas diárias", + "calculationMethodTooltip": "Esta configuração se aplica a todos os projetos em sua organização", + "calculationMethodUpdated": "Método de cálculo da organização atualizado com sucesso", + "calculationMethodUpdateError": "Erro ao atualizar o método de cálculo" } diff --git a/worklenz-frontend/public/locales/pt/admin-center/settings.json b/worklenz-frontend/public/locales/pt/admin-center/settings.json new file mode 100644 index 00000000..44c8e091 --- /dev/null +++ b/worklenz-frontend/public/locales/pt/admin-center/settings.json @@ -0,0 +1,17 @@ +{ + "settings": "Configurações", + "organizationWorkingDaysAndHours": "Dias e Horas de Trabalho da Organização", + "workingDays": "Dias de Trabalho", + "workingHours": "Horas de Trabalho", + "hours": "horas", + "monday": "Segunda-feira", + "tuesday": "Terça-feira", + "wednesday": "Quarta-feira", + "thursday": "Quinta-feira", + "friday": "Sexta-feira", + "saturday": "Sábado", + "sunday": "Domingo", + "saveButton": "Salvar", + "saved": "Configurações salvas com sucesso", + "errorSaving": "Erro ao salvar configurações" +} \ No newline at end of file diff --git a/worklenz-frontend/public/locales/pt/admin-center/sidebar.json b/worklenz-frontend/public/locales/pt/admin-center/sidebar.json index 253b77e4..f02c5ca8 100644 --- a/worklenz-frontend/public/locales/pt/admin-center/sidebar.json +++ b/worklenz-frontend/public/locales/pt/admin-center/sidebar.json @@ -4,5 +4,6 @@ "teams": "Equipes", "billing": "Faturamento", "projects": "Projetos", + "settings": "Configurações", "adminCenter": "Central Administrativa" } diff --git a/worklenz-frontend/public/locales/zh/admin-center/overview.json b/worklenz-frontend/public/locales/zh/admin-center/overview.json index 9c70093f..e272fbd8 100644 --- a/worklenz-frontend/public/locales/zh/admin-center/overview.json +++ b/worklenz-frontend/public/locales/zh/admin-center/overview.json @@ -4,5 +4,29 @@ "owner": "组织所有者", "admins": "组织管理员", "contactNumber": "添加联系电话", - "edit": "编辑" + "edit": "编辑", + "organizationWorkingDaysAndHours": "组织工作日和工作时间", + "workingDays": "工作日", + "workingHours": "工作时间", + "monday": "星期一", + "tuesday": "星期二", + "wednesday": "星期三", + "thursday": "星期四", + "friday": "星期五", + "saturday": "星期六", + "sunday": "星期日", + "hours": "小时", + "saveButton": "保存", + "saved": "设置保存成功", + "errorSaving": "保存设置时出错", + "organizationCalculationMethod": "组织计算方法", + "calculationMethod": "计算方法", + "hourlyRates": "小时费率", + "manDays": "人天", + "saveChanges": "保存更改", + "hourlyCalculationDescription": "所有项目成本将使用估算小时数 × 小时费率计算", + "manDaysCalculationDescription": "所有项目成本将使用估算人天数 × 日费率计算", + "calculationMethodTooltip": "此设置适用于您组织中的所有项目", + "calculationMethodUpdated": "组织计算方法更新成功", + "calculationMethodUpdateError": "更新计算方法失败" } \ No newline at end of file diff --git a/worklenz-frontend/public/locales/zh/admin-center/settings.json b/worklenz-frontend/public/locales/zh/admin-center/settings.json new file mode 100644 index 00000000..6010d084 --- /dev/null +++ b/worklenz-frontend/public/locales/zh/admin-center/settings.json @@ -0,0 +1,17 @@ +{ + "settings": "设置", + "organizationWorkingDaysAndHours": "组织工作日和工作时间", + "workingDays": "工作日", + "workingHours": "工作时间", + "hours": "小时", + "monday": "星期一", + "tuesday": "星期二", + "wednesday": "星期三", + "thursday": "星期四", + "friday": "星期五", + "saturday": "星期六", + "sunday": "星期日", + "saveButton": "保存", + "saved": "设置保存成功", + "errorSaving": "保存设置时出错" +} \ No newline at end of file diff --git a/worklenz-frontend/public/locales/zh/admin-center/sidebar.json b/worklenz-frontend/public/locales/zh/admin-center/sidebar.json index ab8808c3..fa36ac43 100644 --- a/worklenz-frontend/public/locales/zh/admin-center/sidebar.json +++ b/worklenz-frontend/public/locales/zh/admin-center/sidebar.json @@ -4,5 +4,6 @@ "teams": "团队", "billing": "账单", "projects": "项目", + "settings": "设置", "adminCenter": "管理中心" } \ No newline at end of file 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 60269917..65942b01 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 @@ -280,4 +280,16 @@ export const adminCenterApiService = { ); return response.data; }, + + async updateOrganizationCalculationMethod( + calculationMethod: 'hourly' | 'man_days' + ): Promise> { + const response = await apiClient.put>( + `${rootUrl}/organization/calculation-method`, + { + calculation_method: calculationMethod, + } + ); + return response.data; + }, }; diff --git a/worklenz-frontend/src/components/admin-center/overview/organization-calculation-method/organization-calculation-method.tsx b/worklenz-frontend/src/components/admin-center/overview/organization-calculation-method/organization-calculation-method.tsx new file mode 100644 index 00000000..e238a9ef --- /dev/null +++ b/worklenz-frontend/src/components/admin-center/overview/organization-calculation-method/organization-calculation-method.tsx @@ -0,0 +1,120 @@ +import React, { useState, useEffect } from 'react'; +import { Card, Select, Space, Typography, Tooltip, message, Button } from '@/shared/antd-imports'; +import { useTranslation } from 'react-i18next'; +import { InfoCircleOutlined, CalculatorOutlined, SaveOutlined } from '@ant-design/icons'; +import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service'; +import { IOrganization } from '@/types/admin-center/admin-center.types'; + +const { Option } = Select; +const { Text, Title } = Typography; + +interface OrganizationCalculationMethodProps { + organization: IOrganization | null; + refetch: () => void; +} + +const OrganizationCalculationMethod: React.FC = ({ + organization, + refetch +}) => { + const { t } = useTranslation('admin-center/overview'); + const [updating, setUpdating] = useState(false); + const [currentMethod, setCurrentMethod] = useState<'hourly' | 'man_days'>( + organization?.calculation_method || 'hourly' + ); + const [hasChanges, setHasChanges] = useState(false); + + useEffect(() => { + if (organization) { + setCurrentMethod(organization.calculation_method || 'hourly'); + setHasChanges(false); + } + }, [organization]); + + const handleMethodChange = (newMethod: 'hourly' | 'man_days') => { + setCurrentMethod(newMethod); + setHasChanges(newMethod !== organization?.calculation_method); + }; + + const handleSave = async () => { + setUpdating(true); + try { + await adminCenterApiService.updateOrganizationCalculationMethod(currentMethod); + + message.success(t('calculationMethodUpdated')); + + setHasChanges(false); + refetch(); + } catch (error) { + console.error('Failed to update organization calculation method:', error); + message.error(t('calculationMethodUpdateError')); + } finally { + setUpdating(false); + } + }; + + return ( + + + + + + {t('organizationCalculationMethod')} + + + + + + + + + {t('calculationMethod')}: + + + {hasChanges && ( + + )} + + + {currentMethod === 'hourly' && ( + + {t('hourlyCalculationDescription')} + + )} + + {currentMethod === 'man_days' && ( + + {t('manDaysCalculationDescription')} ({organization?.hours_per_day}h/day) + + )} + + + + ); +}; + +export default OrganizationCalculationMethod; \ No newline at end of file diff --git a/worklenz-frontend/src/pages/admin-center/admin-center-constants.ts b/worklenz-frontend/src/pages/admin-center/admin-center-constants.ts index 7494ff78..9a320966 100644 --- a/worklenz-frontend/src/pages/admin-center/admin-center-constants.ts +++ b/worklenz-frontend/src/pages/admin-center/admin-center-constants.ts @@ -4,6 +4,7 @@ import { ProfileOutlined, TeamOutlined, UserOutlined, + SettingOutlined, } from '@/shared/antd-imports'; import React, { ReactNode, lazy } from 'react'; const Overview = lazy(() => import('./overview/overview')); @@ -11,6 +12,7 @@ const Users = lazy(() => import('./users/users')); const Teams = lazy(() => import('./teams/teams')); const Billing = lazy(() => import('./billing/billing')); const Projects = lazy(() => import('./projects/projects')); +const Settings = lazy(() => import('./settings/settings')); // type of a menu item in admin center sidebar type AdminCenterMenuItems = { @@ -57,4 +59,11 @@ export const adminCenterItems: AdminCenterMenuItems[] = [ icon: React.createElement(CreditCardOutlined), element: React.createElement(Billing), }, + { + key: 'settings', + name: 'settings', + endpoint: 'settings', + icon: React.createElement(SettingOutlined), + element: React.createElement(Settings), + }, ]; diff --git a/worklenz-frontend/src/pages/admin-center/overview/overview.tsx b/worklenz-frontend/src/pages/admin-center/overview/overview.tsx index dd46d605..0fb0e051 100644 --- a/worklenz-frontend/src/pages/admin-center/overview/overview.tsx +++ b/worklenz-frontend/src/pages/admin-center/overview/overview.tsx @@ -1,7 +1,10 @@ -import { EditOutlined, MailOutlined, PhoneOutlined } from '@/shared/antd-imports'; -import { PageHeader } from '@ant-design/pro-components'; -import { Button, Card, Input, Space, Tooltip, Typography } from '@/shared/antd-imports'; import React, { useEffect, useState } from 'react'; +import { + Card, + Space, + Typography, +} from '@/shared/antd-imports'; +import { PageHeader } from '@ant-design/pro-components'; import OrganizationAdminsTable from '@/components/admin-center/overview/organization-admins-table/organization-admins-table'; import { useAppSelector } from '@/hooks/useAppSelector'; import { RootState } from '@/app/store'; @@ -11,9 +14,6 @@ import OrganizationOwner from '@/components/admin-center/overview/organization-o import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service'; import { IOrganization, IOrganizationAdmin } from '@/types/admin-center/admin-center.types'; import logger from '@/utils/errorLogger'; -import { tr } from 'date-fns/locale'; - -const { Text } = Typography; const Overview: React.FC = () => { const [organization, setOrganization] = useState(null); diff --git a/worklenz-frontend/src/pages/admin-center/settings/index.ts b/worklenz-frontend/src/pages/admin-center/settings/index.ts new file mode 100644 index 00000000..d65aa48a --- /dev/null +++ b/worklenz-frontend/src/pages/admin-center/settings/index.ts @@ -0,0 +1 @@ +export { default } from './settings'; \ No newline at end of file diff --git a/worklenz-frontend/src/pages/admin-center/settings/settings.tsx b/worklenz-frontend/src/pages/admin-center/settings/settings.tsx new file mode 100644 index 00000000..8c13a919 --- /dev/null +++ b/worklenz-frontend/src/pages/admin-center/settings/settings.tsx @@ -0,0 +1,156 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Card, + Input, + Space, + Typography, + Checkbox, + Col, + Form, + Row, + message, +} from '@/shared/antd-imports'; +import { PageHeader } from '@ant-design/pro-components'; +import { useTranslation } from 'react-i18next'; +import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service'; +import { IOrganization } from '@/types/admin-center/admin-center.types'; +import logger from '@/utils/errorLogger'; +import { scheduleAPIService } from '@/api/schedule/schedule.api.service'; +import { Settings } from '@/types/schedule/schedule-v2.types'; +import OrganizationCalculationMethod from '@/components/admin-center/overview/organization-calculation-method/organization-calculation-method'; + +const SettingsPage: React.FC = () => { + const [organization, setOrganization] = useState(null); + const [workingDays, setWorkingDays] = useState([]); + const [workingHours, setWorkingHours] = useState(8); + const [saving, setSaving] = useState(false); + const [form] = Form.useForm(); + + const { t } = useTranslation('admin-center/settings'); + + const getOrganizationDetails = async () => { + try { + const res = await adminCenterApiService.getOrganizationDetails(); + if (res.done) { + setOrganization(res.body); + } + } catch (error) { + logger.error('Error getting organization details', error); + } + }; + + const getOrgWorkingSettings = async () => { + try { + const res = await scheduleAPIService.fetchScheduleSettings(); + if (res && res.done) { + setWorkingDays( + res.body.workingDays || ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'] + ); + setWorkingHours(res.body.workingHours || 8); + form.setFieldsValue({ + workingDays: res.body.workingDays || [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + ], + workingHours: res.body.workingHours || 8, + }); + } + } catch (error) { + logger.error('Error getting organization working settings', error); + } + }; + + const handleSave = async (values: any) => { + setSaving(true); + try { + const res = await scheduleAPIService.updateScheduleSettings({ + workingDays: values.workingDays, + workingHours: values.workingHours, + }); + if (res && res.done) { + message.success(t('saved')); + setWorkingDays(values.workingDays); + setWorkingHours(values.workingHours); + getOrgWorkingSettings(); + } + } catch (error) { + logger.error('Error updating organization working days/hours', error); + message.error(t('errorSaving')); + } finally { + setSaving(false); + } + }; + + useEffect(() => { + getOrganizationDetails(); + getOrgWorkingSettings(); + }, []); + + return ( +
+ {t('settings')}} style={{ padding: '16px 0' }} /> + + + + + {t('organizationWorkingDaysAndHours') || 'Organization Working Days & Hours'} + +
+ + + + + {t('monday')} + + + {t('tuesday')} + + + {t('wednesday')} + + + {t('thursday')} + + + {t('friday')} + + + {t('saturday')} + + + {t('sunday')} + + + + + + + + + + +
+
+ + +
+
+ ); +}; + +export default SettingsPage; \ No newline at end of file diff --git a/worklenz-frontend/src/types/admin-center/admin-center.types.ts b/worklenz-frontend/src/types/admin-center/admin-center.types.ts index e350d9e3..2d4b4861 100644 --- a/worklenz-frontend/src/types/admin-center/admin-center.types.ts +++ b/worklenz-frontend/src/types/admin-center/admin-center.types.ts @@ -1,4 +1,4 @@ -import { ISUBSCRIPTION_TYPE } from '@/shared/constants'; +import { ISUBSCRIPTION_TYPE } from "@/shared/constants"; export interface IOrganization { name?: string; @@ -6,6 +6,8 @@ export interface IOrganization { email?: string; contact_number?: string; contact_number_secondary?: string; + calculation_method?: 'hourly' | 'man_days'; + hours_per_day?: number; } export interface IOrganizationAdmin { @@ -79,7 +81,6 @@ export interface IBillingAccountInfo { unit_price?: number; unit_price_per_month?: number; usedPercentage?: number; - used_percent?: number; usedStorage?: number; is_custom?: boolean; is_ltd_user?: boolean; @@ -230,4 +231,4 @@ export interface IFreePlanSettings { export interface IOrganizationProjectsGetResponse { total?: number; data?: IOrganizationProject[]; -} +} \ No newline at end of file