init
This commit is contained in:
@@ -5,8 +5,19 @@ import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {getColor} from "../shared/utils";
|
||||
import {calculateMonthDays, getColor, 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 {
|
||||
addModifier,
|
||||
cancelSubscription,
|
||||
changePlan,
|
||||
generatePayLinkRequest,
|
||||
pauseOrResumeSubscription,
|
||||
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";
|
||||
@@ -262,6 +273,384 @@ export default class AdminCenterController extends WorklenzControllerBase {
|
||||
return res.status(200).send(new ServerResponse(true, [], "Team updated successfully"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getBillingInfo(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT get_billing_info($1) AS billing_info;`;
|
||||
const result = await db.query(q, [req.user?.owner_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
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);
|
||||
|
||||
data.billing_info.expire_date_string = dateString;
|
||||
|
||||
if (daysDifference < 0) {
|
||||
data.billing_info.expire_date_string = `Your trial plan expired ${dateString} ago`;
|
||||
} else if (daysDifference === 0 && daysDifference < 7) {
|
||||
data.billing_info.expire_date_string = `Your trial plan expires today`;
|
||||
} else {
|
||||
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;
|
||||
|
||||
const teamMemberData = await getTeamMemberCount(req.user?.owner_id ?? "");
|
||||
const subscriptionData = await checkTeamSubscriptionStatus(req.user?.team_id ?? "");
|
||||
|
||||
data.billing_info.total_used = teamMemberData.user_count;
|
||||
data.billing_info.total_seats = subscriptionData.quantity;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.billing_info));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getBillingTransactions(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT subscription_payment_id,
|
||||
event_time::date,
|
||||
(next_bill_date::DATE - INTERVAL '1 day')::DATE AS next_bill_date,
|
||||
currency,
|
||||
receipt_url,
|
||||
payment_method,
|
||||
status,
|
||||
payment_status
|
||||
FROM licensing_payment_details
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC;`;
|
||||
const result = await db.query(q, [req.user?.owner_id]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getBillingCharges(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT (SELECT name FROM licensing_pricing_plans lpp WHERE id = lus.plan_id),
|
||||
unit_price::numeric,
|
||||
currency,
|
||||
status,
|
||||
quantity,
|
||||
unit_price::numeric * quantity AS amount,
|
||||
(SELECT event_time
|
||||
FROM licensing_payment_details lpd
|
||||
WHERE lpd.user_id = lus.user_id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1)::DATE AS start_date,
|
||||
(next_bill_date::DATE - INTERVAL '1 day')::DATE AS end_date
|
||||
FROM licensing_user_subscriptions lus
|
||||
WHERE user_id = $1;`;
|
||||
const result = await db.query(q, [req.user?.owner_id]);
|
||||
|
||||
const countQ = `SELECT subscription_id
|
||||
FROM licensing_user_subscription_modifiers
|
||||
WHERE subscription_id = (SELECT subscription_id
|
||||
FROM licensing_user_subscriptions
|
||||
WHERE user_id = $1
|
||||
AND status != 'deleted'
|
||||
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}));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getBillingModifiers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT created_at
|
||||
FROM licensing_user_subscription_modifiers
|
||||
WHERE subscription_id = (SELECT subscription_id
|
||||
FROM licensing_user_subscriptions
|
||||
WHERE user_id = $1
|
||||
AND status != 'deleted'
|
||||
LIMIT 1)::INT;`;
|
||||
const result = await db.query(q, [req.user?.owner_id]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getBillingConfiguration(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT name,
|
||||
email,
|
||||
organization_name AS company_name,
|
||||
contact_number AS phone,
|
||||
address_line_1,
|
||||
address_line_2,
|
||||
city,
|
||||
state,
|
||||
postal_code,
|
||||
country
|
||||
FROM organizations
|
||||
LEFT JOIN users u ON organizations.user_id = u.id
|
||||
WHERE u.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 updateBillingConfiguration(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
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,
|
||||
address_line_1 = $3,
|
||||
address_line_2 = $4,
|
||||
city = $5,
|
||||
state = $6,
|
||||
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 [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data, "Configuration Updated"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async upgradePlan(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
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);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, axiosResponse.body));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getPlans(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT
|
||||
ls.default_monthly_plan AS monthly_plan_id,
|
||||
lp_monthly.name AS monthly_plan_name,
|
||||
ls.default_annual_plan AS annual_plan_id,
|
||||
lp_monthly.recurring_price AS monthly_price,
|
||||
lp_annual.name AS annual_plan_name,
|
||||
lp_annual.recurring_price AS annual_price,
|
||||
ls.team_member_limit,
|
||||
ls.projects_limit,
|
||||
ls.free_tier_storage
|
||||
FROM
|
||||
licensing_settings ls
|
||||
JOIN
|
||||
licensing_pricing_plans lp_monthly ON ls.default_monthly_plan = lp_monthly.id
|
||||
JOIN
|
||||
licensing_pricing_plans lp_annual ON ls.default_annual_plan = lp_annual.id;`;
|
||||
const result = await db.query(q, []);
|
||||
const [data] = result.rows;
|
||||
|
||||
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.free_tier_storage = `${data.free_tier_storage}MB`;
|
||||
data.current_user_count = obj.user_count;
|
||||
data.annual_price = (data.annual_price / 12).toFixed(2);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async purchaseStorage(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT subscription_id
|
||||
FROM licensing_user_subscriptions lus
|
||||
WHERE user_id = $1;`;
|
||||
const result = await db.query(q, [req.user?.owner_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
await addModifier(data.subscription_id);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async changePlan(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {plan} = req.query;
|
||||
|
||||
const q = `SELECT subscription_id
|
||||
FROM licensing_user_subscriptions lus
|
||||
WHERE user_id = $1;`;
|
||||
const result = await db.query(q, [req.user?.owner_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
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<IWorkLenzResponse> {
|
||||
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
|
||||
WHERE user_id = $1;`;
|
||||
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);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, axiosResponse.body));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async pauseSubscription(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
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
|
||||
WHERE user_id = $1;`;
|
||||
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);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, axiosResponse.body));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async resumeSubscription(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
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
|
||||
WHERE user_id = $1;`;
|
||||
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);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, axiosResponse.body));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getBillingStorageInfo(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT trial_in_progress,
|
||||
trial_expire_date,
|
||||
ud.storage,
|
||||
(SELECT name AS plan_name FROM licensing_pricing_plans WHERE id = lus.plan_id),
|
||||
(SELECT default_trial_storage FROM licensing_settings),
|
||||
(SELECT storage_addon_size FROM licensing_settings),
|
||||
(SELECT storage_addon_price FROM licensing_settings)
|
||||
FROM organizations ud
|
||||
LEFT JOIN users u ON ud.user_id = u.id
|
||||
LEFT JOIN licensing_user_subscriptions lus ON u.id = lus.user_id
|
||||
WHERE ud.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 getAccountStorage(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamsQ = `SELECT id
|
||||
FROM teams
|
||||
WHERE user_id = $1;`;
|
||||
const teamsResponse = await db.query(teamsQ, [req.user?.owner_id]);
|
||||
|
||||
const storageQ = `SELECT storage
|
||||
FROM organizations
|
||||
WHERE user_id = $1;`;
|
||||
const result = await db.query(storageQ, [req.user?.owner_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const storage: any = {};
|
||||
storage.used = 0;
|
||||
storage.total = data.storage;
|
||||
|
||||
for (const team of teamsResponse.rows) {
|
||||
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;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, storage));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getCountries(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, code
|
||||
FROM countries
|
||||
ORDER BY name;`;
|
||||
const result = await db.query(q, []);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async switchToFreePlan(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id: teamId } = req.params;
|
||||
|
||||
const limits = await getFreePlanSettings();
|
||||
const ownerId = await getOwnerIdByTeam(teamId);
|
||||
|
||||
if (limits && ownerId) {
|
||||
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.`));
|
||||
}
|
||||
}
|
||||
|
||||
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.`));
|
||||
}
|
||||
|
||||
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.`));
|
||||
}
|
||||
|
||||
const update_q = `UPDATE organizations
|
||||
SET license_type_id = (SELECT id FROM sys_license_types WHERE key = 'FREE'),
|
||||
trial_in_progress = FALSE,
|
||||
subscription_status = 'free',
|
||||
storage = (SELECT free_tier_storage FROM licensing_settings)
|
||||
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(false, [], "Failed to switch to the Free Plan. Please try again later."));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async redeem(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
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;`;
|
||||
const result = await db.query(q, [code]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (!result.rows.length)
|
||||
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!"));
|
||||
|
||||
const updateQ = `UPDATE licensing_coupon_codes
|
||||
SET is_redeemed = TRUE, redeemed_at = CURRENT_TIMESTAMP,
|
||||
redeemed_by = $1
|
||||
WHERE id = $2;`;
|
||||
await db.query(updateQ, [req.user?.owner_id, data.id]);
|
||||
|
||||
const updateQ2 = `UPDATE organizations
|
||||
SET subscription_status = 'life_time_deal',
|
||||
trial_in_progress = FALSE,
|
||||
storage = (SELECT sum(storage_limit) FROM licensing_coupon_codes WHERE redeemed_by = $1),
|
||||
license_type_id = (SELECT id FROM sys_license_types WHERE key = 'LIFE_TIME_DEAL')
|
||||
WHERE user_id = $1;`;
|
||||
await db.query(updateQ2, [req.user?.owner_id]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, [], "Code redeemed successfully!"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
@@ -284,6 +673,11 @@ export default class AdminCenterController extends WorklenzControllerBase {
|
||||
|
||||
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."));
|
||||
}
|
||||
|
||||
const q = `SELECT remove_team_member($1, $2, $3) AS member;`;
|
||||
const result = await db.query(q, [id, req.user?.id, teamId]);
|
||||
@@ -291,6 +685,22 @@ export default class AdminCenterController extends WorklenzControllerBase {
|
||||
|
||||
const message = `You have been removed from <b>${req.user?.team_name}</b> by <b>${req.user?.name}</b>`;
|
||||
|
||||
// if (subscriptionData.status === "trialing") break;
|
||||
if (!subscriptionData.is_credit && !subscriptionData.is_custom) {
|
||||
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);
|
||||
|
||||
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."));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
NotificationsService.sendNotification({
|
||||
receiver_socket_id: data.socket_id,
|
||||
message,
|
||||
@@ -305,5 +715,49 @@ export default class AdminCenterController extends WorklenzControllerBase {
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getFreePlanLimits(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const limits = await getFreePlanSettings();
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, limits || {}));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOrganizationProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { searchQuery, size, offset } = this.toPaginationOptions(req.query, ["p.name"]);
|
||||
|
||||
const countQ = `SELECT COUNT(*) AS total
|
||||
FROM projects p
|
||||
JOIN teams t ON p.team_id = t.id
|
||||
JOIN organizations o ON t.organization_id = o.id
|
||||
WHERE o.user_id = $1;`;
|
||||
const countResult = await db.query(countQ, [req.user?.owner_id]);
|
||||
|
||||
// Query to get the project data
|
||||
const dataQ = `SELECT p.id,
|
||||
p.name,
|
||||
t.name AS team_name,
|
||||
p.created_at,
|
||||
pm.member_count
|
||||
FROM projects p
|
||||
JOIN teams t ON p.team_id = t.id
|
||||
JOIN organizations o ON t.organization_id = o.id
|
||||
LEFT JOIN (
|
||||
SELECT project_id, COUNT(*) AS member_count
|
||||
FROM project_members
|
||||
GROUP BY project_id
|
||||
) pm ON p.id = pm.project_id
|
||||
WHERE o.user_id = $1 ${searchQuery}
|
||||
ORDER BY p.name
|
||||
OFFSET $2 LIMIT $3;`;
|
||||
|
||||
const result = await db.query(dataQ, [req.user?.owner_id, offset, size]);
|
||||
|
||||
const response = {
|
||||
total: countResult.rows[0]?.total ?? 0,
|
||||
data: result.rows ?? []
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, response));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { humanFileSize, log_error, smallId } from "../shared/utils";
|
||||
import { humanFileSize, smallId } from "../shared/utils";
|
||||
import { getStorageUrl } from "../shared/constants";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import {
|
||||
createPresignedUrlWithClient,
|
||||
@@ -12,16 +13,10 @@ import {
|
||||
getRootDir,
|
||||
uploadBase64,
|
||||
uploadBuffer
|
||||
} from "../shared/s3";
|
||||
} from "../shared/storage";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
const {S3_URL} = process.env;
|
||||
|
||||
if (!S3_URL) {
|
||||
log_error("Invalid S3_URL. Please check .env file.");
|
||||
}
|
||||
|
||||
export default class AttachmentController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
@@ -42,7 +37,7 @@ export default class AttachmentController extends WorklenzControllerBase {
|
||||
req.user?.id,
|
||||
size,
|
||||
type,
|
||||
`${S3_URL}/${getRootDir()}`
|
||||
`${getStorageUrl()}/${getRootDir()}`
|
||||
]);
|
||||
const [data] = result.rows;
|
||||
|
||||
@@ -86,7 +81,7 @@ export default class AttachmentController extends WorklenzControllerBase {
|
||||
FROM task_attachments
|
||||
WHERE task_id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, `${S3_URL}/${getRootDir()}`]);
|
||||
const result = await db.query(q, [req.params.id, `${getStorageUrl()}/${getRootDir()}`]);
|
||||
|
||||
for (const item of result.rows)
|
||||
item.size = humanFileSize(item.size);
|
||||
@@ -121,7 +116,7 @@ export default class AttachmentController extends WorklenzControllerBase {
|
||||
LEFT JOIN tasks t ON task_attachments.task_id = t.id
|
||||
WHERE task_attachments.project_id = $1) rec;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, `${S3_URL}/${getRootDir()}`, size, offset]);
|
||||
const result = await db.query(q, [req.params.id, `${getStorageUrl()}/${getRootDir()}`, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const item of data?.attachments.data || [])
|
||||
@@ -135,26 +130,29 @@ export default class AttachmentController extends WorklenzControllerBase {
|
||||
const q = `DELETE
|
||||
FROM task_attachments
|
||||
WHERE id = $1
|
||||
RETURNING CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS key;`;
|
||||
const result = await db.query(q, [req.params.id, getRootDir()]);
|
||||
RETURNING team_id, project_id, id, type;`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data?.key)
|
||||
void deleteObject(data.key);
|
||||
if (data) {
|
||||
const key = getKey(data.team_id, data.project_id, data.id, data.type);
|
||||
void deleteObject(key);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async download(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS key
|
||||
const q = `SELECT team_id, project_id, id, type
|
||||
FROM task_attachments
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.query.id, getRootDir()]);
|
||||
const result = await db.query(q, [req.query.id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data?.key) {
|
||||
const url = await createPresignedUrlWithClient(data.key, req.query.file as string);
|
||||
if (data) {
|
||||
const key = getKey(data.team_id, data.project_id, data.id, data.type);
|
||||
const url = await createPresignedUrlWithClient(key, req.query.file as string);
|
||||
return res.status(200).send(new ServerResponse(true, url));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {PasswordStrengthChecker} from "../shared/password-strength-check";
|
||||
import FileConstants from "../shared/file-constants";
|
||||
import axios from "axios";
|
||||
import {log_error} from "../shared/utils";
|
||||
import {DEFAULT_ERROR_MESSAGE} from "../shared/constants";
|
||||
|
||||
export default class AuthController extends WorklenzControllerBase {
|
||||
/** This just send ok response to the client when the request came here through the sign-up-validator */
|
||||
@@ -42,11 +45,20 @@ export default class AuthController extends WorklenzControllerBase {
|
||||
}
|
||||
|
||||
public static logout(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
req.logout(() => true);
|
||||
req.session.destroy(() => {
|
||||
res.redirect("/");
|
||||
req.logout((err) => {
|
||||
if (err) {
|
||||
console.error("Logout error:", err);
|
||||
return res.status(500).send(new AuthResponse(null, true, {}, "Logout failed", null));
|
||||
}
|
||||
|
||||
req.session.destroy((destroyErr) => {
|
||||
if (destroyErr) {
|
||||
console.error("Session destroy error:", destroyErr);
|
||||
}
|
||||
res.status(200).send(new AuthResponse(null, req.isAuthenticated(), {}, null, null));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static async destroyOtherSessions(userId: string, sessionId: string) {
|
||||
try {
|
||||
@@ -138,4 +150,25 @@ export default class AuthController extends WorklenzControllerBase {
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(false, null, "Invalid Request. Please try again."));
|
||||
}
|
||||
|
||||
@HandleExceptions({logWithError: "body"})
|
||||
public static async verifyCaptcha(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const {token} = req.body;
|
||||
const secretKey = process.env.GOOGLE_CAPTCHA_SECRET_KEY;
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${token}`
|
||||
);
|
||||
|
||||
const {success, score} = response.data;
|
||||
|
||||
if (success && score > 0.5) {
|
||||
return res.status(200).send(new ServerResponse(true, null, null));
|
||||
}
|
||||
return res.status(400).send(new ServerResponse(false, null, "Please try again later.").withTitle("Error"));
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
res.status(500).send(new ServerResponse(false, null, DEFAULT_ERROR_MESSAGE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
288
worklenz-backend/src/controllers/billing-controller.ts
Normal file
288
worklenz-backend/src/controllers/billing-controller.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { getTeamMemberCount } from "../shared/paddle-utils";
|
||||
import { generatePayLinkRequest, updateUsers } from "../shared/paddle-requests";
|
||||
|
||||
import CryptoJS from "crypto-js";
|
||||
import moment from "moment";
|
||||
import axios from "axios";
|
||||
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { log_error } from "../shared/utils";
|
||||
import { sendEmail } from "../shared/email";
|
||||
|
||||
export default class BillingController extends WorklenzControllerBase {
|
||||
public static async getInitialCharge(count: number) {
|
||||
if (!count) throw new Error("No selected plan detected.");
|
||||
|
||||
const baseRate = 4990;
|
||||
const firstTier = 15;
|
||||
const secondTierEnd = 200;
|
||||
|
||||
if (count <= firstTier) {
|
||||
return baseRate;
|
||||
} else if (count <= secondTierEnd) {
|
||||
return baseRate + (count - firstTier) * 300;
|
||||
}
|
||||
return baseRate + (secondTierEnd - firstTier) * 300 + (count - secondTierEnd) * 200;
|
||||
|
||||
}
|
||||
|
||||
public static async getBillingMonth() {
|
||||
const startDate = moment().format("YYYYMMDD");
|
||||
const endDate = moment().add(1, "month").subtract(1, "day").format("YYYYMMDD");
|
||||
|
||||
return `${startDate} - ${endDate}`;
|
||||
}
|
||||
|
||||
public static async chargeInitialPayment(signature: string, data: any) {
|
||||
const config = {
|
||||
method: "post",
|
||||
maxBodyLength: Infinity,
|
||||
url: process.env.DP_URL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Signature": signature,
|
||||
"x-api-key": process.env.DP_API_KEY
|
||||
},
|
||||
data
|
||||
};
|
||||
|
||||
axios.request(config)
|
||||
.then((response) => {
|
||||
console.log(JSON.stringify(response.data));
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
public static async saveLocalTransaction(signature: string, data: any) {
|
||||
try {
|
||||
const q = `INSERT INTO transactions (status, transaction_id, transaction_status, description, date_time, reference, amount, card_number)
|
||||
VALUES ($1, $2, $3);`;
|
||||
const result = await db.query(q, []);
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async upgradeToPaidPlan(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { plan, seatCount } = req.query;
|
||||
|
||||
const teamMemberData = await getTeamMemberCount(req.user?.owner_id ?? "");
|
||||
teamMemberData.user_count = seatCount as string;
|
||||
const axiosResponse = await generatePayLinkRequest(teamMemberData, plan as string, req.user?.owner_id, req.user?.id);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, axiosResponse.body));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async addMoreSeats(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { seatCount } = req.body;
|
||||
|
||||
const q = `SELECT subscription_id
|
||||
FROM licensing_user_subscriptions lus
|
||||
WHERE user_id = $1;`;
|
||||
const result = await db.query(q, [req.user?.owner_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const response = await updateUsers(data.subscription_id, seatCount);
|
||||
|
||||
if (!response.body.subscription_id) {
|
||||
return res.status(200).send(new ServerResponse(false, null, response.message || "Please check your subscription."));
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, null, "Your purchase has been successfully completed!").withTitle("Done"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getDirectPayObject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { seatCount } = req.query;
|
||||
if (!seatCount) return res.status(200).send(new ServerResponse(false, null));
|
||||
const email = req.user?.email;
|
||||
const name = req.user?.name;
|
||||
const amount = await this.getInitialCharge(parseInt(seatCount as string));
|
||||
const uniqueTimestamp = moment().format("YYYYMMDDHHmmss");
|
||||
const billingMonth = await this.getBillingMonth();
|
||||
|
||||
const { DP_MERCHANT_ID, DP_SECRET_KEY, DP_STAGE } = process.env;
|
||||
|
||||
const payload = {
|
||||
merchant_id: DP_MERCHANT_ID,
|
||||
amount: 10,
|
||||
type: "RECURRING",
|
||||
order_id: `WORKLENZ_${email}_${uniqueTimestamp}`,
|
||||
currency: "LKR",
|
||||
return_url: null,
|
||||
response_url: null,
|
||||
first_name: name,
|
||||
last_name: null,
|
||||
phone: null,
|
||||
email,
|
||||
description: `${name} (${email})`,
|
||||
page_type: "IN_APP",
|
||||
logo: "https://app.worklenz.com/assets/icons/icon-96x96.png",
|
||||
start_date: moment().format("YYYY-MM-DD"),
|
||||
do_initial_payment: 1,
|
||||
interval: 1,
|
||||
};
|
||||
|
||||
const encodePayload = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(JSON.stringify(payload)));
|
||||
const signature = CryptoJS.HmacSHA256(encodePayload, DP_SECRET_KEY as string);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { signature: signature.toString(CryptoJS.enc.Hex), dataString: encodePayload, stage: DP_STAGE }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async saveTransactionData(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { status, card, transaction, seatCount } = req.body;
|
||||
const { DP_MERCHANT_ID, DP_STAGE } = process.env;
|
||||
|
||||
const email = req.user?.email;
|
||||
|
||||
const amount = await this.getInitialCharge(parseInt(seatCount as string));
|
||||
const uniqueTimestamp = moment().format("YYYYMMDDHHmmss");
|
||||
const billingMonth = await this.getBillingMonth();
|
||||
|
||||
const values = [
|
||||
status,
|
||||
card?.id,
|
||||
card?.number,
|
||||
card?.brand,
|
||||
card?.type,
|
||||
card?.issuer,
|
||||
card?.expiry?.year,
|
||||
card?.expiry?.month,
|
||||
card?.walletId,
|
||||
transaction?.id,
|
||||
transaction?.status,
|
||||
transaction?.amount || 0,
|
||||
transaction?.currency || null,
|
||||
transaction?.channel || null,
|
||||
transaction?.dateTime || null,
|
||||
transaction?.message || null,
|
||||
transaction?.description || null,
|
||||
req.user?.id,
|
||||
req.user?.owner_id,
|
||||
];
|
||||
|
||||
const q = `INSERT INTO licensing_lkr_payments (
|
||||
status, card_id, card_number, card_brand, card_type, card_issuer,
|
||||
card_expiry_year, card_expiry_month, wallet_id,
|
||||
transaction_id, transaction_status, transaction_amount,
|
||||
transaction_currency, transaction_channel, transaction_datetime,
|
||||
transaction_message, transaction_description, user_id, owner_id
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19
|
||||
);`;
|
||||
await db.query(q, values);
|
||||
|
||||
if (transaction.status === "SUCCESS") {
|
||||
const payload = {
|
||||
"merchantId": DP_MERCHANT_ID,
|
||||
"reference": `WORKLENZ_${email}_${uniqueTimestamp}`,
|
||||
"type": "CARD_PAY",
|
||||
"cardId": card.id,
|
||||
"refCode": req.user?.id,
|
||||
amount,
|
||||
"currency": "LKR"
|
||||
};
|
||||
const dataString = Object.values(payload).join("");
|
||||
const { DP_STAGE } = process.env;
|
||||
|
||||
const pemFile = DP_STAGE === "PROD" ? "src/keys/PRIVATE_KEY_PROD.pem" : `src/keys/PRIVATE_KEY_DEV.pem`;
|
||||
|
||||
const privateKeyTest = fs.readFileSync(path.resolve(pemFile), "utf8");
|
||||
const sign = crypto.createSign("SHA256");
|
||||
sign.update(dataString);
|
||||
sign.end();
|
||||
|
||||
const signature = sign.sign(privateKeyTest);
|
||||
const byteArray = new Uint8Array(signature);
|
||||
let byteString = "";
|
||||
for (let i = 0; i < byteArray.byteLength; i++) {
|
||||
byteString += String.fromCharCode(byteArray[i]);
|
||||
}
|
||||
const base64Signature = btoa(byteString);
|
||||
|
||||
this.chargeInitialPayment(base64Signature, payload);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, null, "Your purchase has been successfully completed!").withTitle("Done"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getCardList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const payload = {
|
||||
"merchantId": "RT02300",
|
||||
"reference": "1234",
|
||||
"type": "LIST_CARD"
|
||||
};
|
||||
|
||||
const { DP_STAGE } = process.env;
|
||||
|
||||
const dataString = `RT023001234LIST_CARD`;
|
||||
const pemFile = DP_STAGE === "PROD" ? "src/keys/PRIVATE_KEY_PROD.pem" : `src/keys/PRIVATE_KEY_DEV.pem`;
|
||||
|
||||
const privateKeyTest = fs.readFileSync(path.resolve(pemFile), "utf8");
|
||||
const sign = crypto.createSign("SHA256");
|
||||
sign.update(dataString);
|
||||
sign.end();
|
||||
|
||||
const signature = sign.sign(privateKeyTest);
|
||||
const byteArray = new Uint8Array(signature);
|
||||
let byteString = "";
|
||||
for (let i = 0; i < byteArray.byteLength; i++) {
|
||||
byteString += String.fromCharCode(byteArray[i]);
|
||||
}
|
||||
const base64Signature = btoa(byteString);
|
||||
// const signature = CryptoJS.HmacSHA256(dataString, DP_SECRET_KEY as string).toString(CryptoJS.enc.Hex);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { signature: base64Signature, dataString }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async contactUs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { contactNo } = req.query;
|
||||
|
||||
if (!contactNo) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Contact number is required!"));
|
||||
}
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Worklenz Local Billing - Contact Information</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1 style="text-align: center; margin-bottom: 20px;">Worklenz Local Billing - Contact Information</h1>
|
||||
<p><strong>Name:</strong> ${req.user?.name}</p>
|
||||
<p><strong>Contact No:</strong> ${contactNo as string}</p>
|
||||
<p><strong>Email:</strong> ${req.user?.email}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
const to = [process.env.CONTACT_US_EMAIL || "chamika@ceydigital.com"];
|
||||
|
||||
sendEmail({
|
||||
to,
|
||||
subject: "Worklenz - Local billing contact.",
|
||||
html
|
||||
});
|
||||
return res.status(200).send(new ServerResponse(true, null, "Your contact information has been sent successfully."));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export default class ClientsController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `INSERT INTO clients (name, team_id) VALUES ($1, $2);`;
|
||||
const q = `INSERT INTO clients (name, team_id) VALUES ($1, $2) RETURNING id, name;`;
|
||||
const result = await db.query(q, [req.body.name, req.user?.team_id || null]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
|
||||
531
worklenz-backend/src/controllers/custom-columns-controller.ts
Normal file
531
worklenz-backend/src/controllers/custom-columns-controller.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class CustomcolumnsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async create(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const {
|
||||
project_id,
|
||||
name,
|
||||
key,
|
||||
field_type,
|
||||
width = 150,
|
||||
is_visible = true,
|
||||
configuration,
|
||||
} = req.body;
|
||||
|
||||
// Start a transaction since we're inserting into multiple tables
|
||||
const client = await db.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. Insert the main custom column
|
||||
const columnQuery = `
|
||||
INSERT INTO cc_custom_columns (
|
||||
project_id, name, key, field_type, width, is_visible, is_custom_column
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, true)
|
||||
RETURNING id;
|
||||
`;
|
||||
const columnResult = await client.query(columnQuery, [
|
||||
project_id,
|
||||
name,
|
||||
key,
|
||||
field_type,
|
||||
width,
|
||||
is_visible,
|
||||
]);
|
||||
const columnId = columnResult.rows[0].id;
|
||||
|
||||
// 2. Insert the column configuration
|
||||
const configQuery = `
|
||||
INSERT INTO cc_column_configurations (
|
||||
column_id, field_title, field_type, number_type,
|
||||
decimals, label, label_position, preview_value,
|
||||
expression, first_numeric_column_key, second_numeric_column_key
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING id;
|
||||
`;
|
||||
await client.query(configQuery, [
|
||||
columnId,
|
||||
configuration.field_title,
|
||||
configuration.field_type,
|
||||
configuration.number_type || null,
|
||||
configuration.decimals || null,
|
||||
configuration.label || null,
|
||||
configuration.label_position || null,
|
||||
configuration.preview_value || null,
|
||||
configuration.expression || null,
|
||||
configuration.first_numeric_column_key || null,
|
||||
configuration.second_numeric_column_key || null,
|
||||
]);
|
||||
|
||||
// 3. Insert selection options if present
|
||||
if (
|
||||
configuration.selections_list &&
|
||||
configuration.selections_list.length > 0
|
||||
) {
|
||||
const selectionQuery = `
|
||||
INSERT INTO cc_selection_options (
|
||||
column_id, selection_id, selection_name, selection_color, selection_order
|
||||
) VALUES ($1, $2, $3, $4, $5);
|
||||
`;
|
||||
for (const [
|
||||
index,
|
||||
selection,
|
||||
] of configuration.selections_list.entries()) {
|
||||
await client.query(selectionQuery, [
|
||||
columnId,
|
||||
selection.selection_id,
|
||||
selection.selection_name,
|
||||
selection.selection_color,
|
||||
index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Insert label options if present
|
||||
if (configuration.labels_list && configuration.labels_list.length > 0) {
|
||||
const labelQuery = `
|
||||
INSERT INTO cc_label_options (
|
||||
column_id, label_id, label_name, label_color, label_order
|
||||
) VALUES ($1, $2, $3, $4, $5);
|
||||
`;
|
||||
for (const [index, label] of configuration.labels_list.entries()) {
|
||||
await client.query(labelQuery, [
|
||||
columnId,
|
||||
label.label_id,
|
||||
label.label_name,
|
||||
label.label_color,
|
||||
index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
// Fetch the complete column data
|
||||
const getColumnQuery = `
|
||||
SELECT
|
||||
cc.*,
|
||||
cf.field_title,
|
||||
cf.number_type,
|
||||
cf.decimals,
|
||||
cf.label,
|
||||
cf.label_position,
|
||||
cf.preview_value,
|
||||
cf.expression,
|
||||
cf.first_numeric_column_key,
|
||||
cf.second_numeric_column_key,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'selection_id', so.selection_id,
|
||||
'selection_name', so.selection_name,
|
||||
'selection_color', so.selection_color
|
||||
)
|
||||
)
|
||||
FROM cc_selection_options so
|
||||
WHERE so.column_id = cc.id
|
||||
) as selections_list,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'label_id', lo.label_id,
|
||||
'label_name', lo.label_name,
|
||||
'label_color', lo.label_color
|
||||
)
|
||||
)
|
||||
FROM cc_label_options lo
|
||||
WHERE lo.column_id = cc.id
|
||||
) as labels_list
|
||||
FROM cc_custom_columns cc
|
||||
LEFT JOIN cc_column_configurations cf ON cf.column_id = cc.id
|
||||
WHERE cc.id = $1;
|
||||
`;
|
||||
const result = await client.query(getColumnQuery, [columnId]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const { project_id } = req.query;
|
||||
|
||||
const q = `
|
||||
SELECT
|
||||
cc.*,
|
||||
cf.field_title,
|
||||
cf.number_type,
|
||||
cf.decimals,
|
||||
cf.label,
|
||||
cf.label_position,
|
||||
cf.preview_value,
|
||||
cf.expression,
|
||||
cf.first_numeric_column_key,
|
||||
cf.second_numeric_column_key,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'selection_id', so.selection_id,
|
||||
'selection_name', so.selection_name,
|
||||
'selection_color', so.selection_color
|
||||
)
|
||||
)
|
||||
FROM cc_selection_options so
|
||||
WHERE so.column_id = cc.id
|
||||
) as selections_list,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'label_id', lo.label_id,
|
||||
'label_name', lo.label_name,
|
||||
'label_color', lo.label_color
|
||||
)
|
||||
)
|
||||
FROM cc_label_options lo
|
||||
WHERE lo.column_id = cc.id
|
||||
) as labels_list
|
||||
FROM cc_custom_columns cc
|
||||
LEFT JOIN cc_column_configurations cf ON cf.column_id = cc.id
|
||||
WHERE cc.project_id = $1
|
||||
ORDER BY cc.created_at DESC;
|
||||
`;
|
||||
const result = await db.query(q, [project_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
|
||||
const q = `
|
||||
SELECT
|
||||
cc.*,
|
||||
cf.field_title,
|
||||
cf.number_type,
|
||||
cf.decimals,
|
||||
cf.label,
|
||||
cf.label_position,
|
||||
cf.preview_value,
|
||||
cf.expression,
|
||||
cf.first_numeric_column_key,
|
||||
cf.second_numeric_column_key,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'selection_id', so.selection_id,
|
||||
'selection_name', so.selection_name,
|
||||
'selection_color', so.selection_color
|
||||
)
|
||||
)
|
||||
FROM cc_selection_options so
|
||||
WHERE so.column_id = cc.id
|
||||
) as selections_list,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'label_id', lo.label_id,
|
||||
'label_name', lo.label_name,
|
||||
'label_color', lo.label_color
|
||||
)
|
||||
)
|
||||
FROM cc_label_options lo
|
||||
WHERE lo.column_id = cc.id
|
||||
) as labels_list
|
||||
FROM cc_custom_columns cc
|
||||
LEFT JOIN cc_column_configurations cf ON cf.column_id = cc.id
|
||||
WHERE cc.id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const { name, field_type, width, is_visible, configuration } = req.body;
|
||||
|
||||
const client = await db.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. Update the main custom column
|
||||
const columnQuery = `
|
||||
UPDATE cc_custom_columns
|
||||
SET name = $1, field_type = $2, width = $3, is_visible = $4, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $5
|
||||
RETURNING id;
|
||||
`;
|
||||
await client.query(columnQuery, [
|
||||
name,
|
||||
field_type,
|
||||
width,
|
||||
is_visible,
|
||||
id,
|
||||
]);
|
||||
|
||||
// 2. Update the configuration
|
||||
const configQuery = `
|
||||
UPDATE cc_column_configurations
|
||||
SET
|
||||
field_title = $1,
|
||||
field_type = $2,
|
||||
number_type = $3,
|
||||
decimals = $4,
|
||||
label = $5,
|
||||
label_position = $6,
|
||||
preview_value = $7,
|
||||
expression = $8,
|
||||
first_numeric_column_key = $9,
|
||||
second_numeric_column_key = $10,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE column_id = $11;
|
||||
`;
|
||||
await client.query(configQuery, [
|
||||
configuration.field_title,
|
||||
configuration.field_type,
|
||||
configuration.number_type || null,
|
||||
configuration.decimals || null,
|
||||
configuration.label || null,
|
||||
configuration.label_position || null,
|
||||
configuration.preview_value || null,
|
||||
configuration.expression || null,
|
||||
configuration.first_numeric_column_key || null,
|
||||
configuration.second_numeric_column_key || null,
|
||||
id,
|
||||
]);
|
||||
|
||||
// 3. Update selections if present
|
||||
if (configuration.selections_list) {
|
||||
// Delete existing selections
|
||||
await client.query(
|
||||
"DELETE FROM cc_selection_options WHERE column_id = $1",
|
||||
[id]
|
||||
);
|
||||
|
||||
// Insert new selections
|
||||
if (configuration.selections_list.length > 0) {
|
||||
const selectionQuery = `
|
||||
INSERT INTO cc_selection_options (
|
||||
column_id, selection_id, selection_name, selection_color, selection_order
|
||||
) VALUES ($1, $2, $3, $4, $5);
|
||||
`;
|
||||
for (const [
|
||||
index,
|
||||
selection,
|
||||
] of configuration.selections_list.entries()) {
|
||||
await client.query(selectionQuery, [
|
||||
id,
|
||||
selection.selection_id,
|
||||
selection.selection_name,
|
||||
selection.selection_color,
|
||||
index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update labels if present
|
||||
if (configuration.labels_list) {
|
||||
// Delete existing labels
|
||||
await client.query("DELETE FROM cc_label_options WHERE column_id = $1", [
|
||||
id,
|
||||
]);
|
||||
|
||||
// Insert new labels
|
||||
if (configuration.labels_list.length > 0) {
|
||||
const labelQuery = `
|
||||
INSERT INTO cc_label_options (
|
||||
column_id, label_id, label_name, label_color, label_order
|
||||
) VALUES ($1, $2, $3, $4, $5);
|
||||
`;
|
||||
for (const [index, label] of configuration.labels_list.entries()) {
|
||||
await client.query(labelQuery, [
|
||||
id,
|
||||
label.label_id,
|
||||
label.label_name,
|
||||
label.label_color,
|
||||
index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
// Fetch the updated column data
|
||||
const getColumnQuery = `
|
||||
SELECT
|
||||
cc.*,
|
||||
cf.field_title,
|
||||
cf.number_type,
|
||||
cf.decimals,
|
||||
cf.label,
|
||||
cf.label_position,
|
||||
cf.preview_value,
|
||||
cf.expression,
|
||||
cf.first_numeric_column_key,
|
||||
cf.second_numeric_column_key,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'selection_id', so.selection_id,
|
||||
'selection_name', so.selection_name,
|
||||
'selection_color', so.selection_color
|
||||
)
|
||||
)
|
||||
FROM cc_selection_options so
|
||||
WHERE so.column_id = cc.id
|
||||
) as selections_list,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'label_id', lo.label_id,
|
||||
'label_name', lo.label_name,
|
||||
'label_color', lo.label_color
|
||||
)
|
||||
)
|
||||
FROM cc_label_options lo
|
||||
WHERE lo.column_id = cc.id
|
||||
) as labels_list
|
||||
FROM cc_custom_columns cc
|
||||
LEFT JOIN cc_column_configurations cf ON cf.column_id = cc.id
|
||||
WHERE cc.id = $1;
|
||||
`;
|
||||
const result = await client.query(getColumnQuery, [id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
|
||||
const q = `
|
||||
DELETE FROM cc_custom_columns
|
||||
WHERE id = $1
|
||||
RETURNING id;
|
||||
`;
|
||||
const result = await db.query(q, [id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectColumns(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { project_id } = req.params;
|
||||
|
||||
const q = `
|
||||
WITH column_data AS (
|
||||
SELECT
|
||||
cc.id,
|
||||
cc.key,
|
||||
cc.name,
|
||||
cc.field_type,
|
||||
cc.width,
|
||||
cc.is_visible,
|
||||
cf.field_title,
|
||||
cf.number_type,
|
||||
cf.decimals,
|
||||
cf.label,
|
||||
cf.label_position,
|
||||
cf.preview_value,
|
||||
cf.expression,
|
||||
cf.first_numeric_column_key,
|
||||
cf.second_numeric_column_key,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'selection_id', so.selection_id,
|
||||
'selection_name', so.selection_name,
|
||||
'selection_color', so.selection_color
|
||||
)
|
||||
)
|
||||
FROM cc_selection_options so
|
||||
WHERE so.column_id = cc.id
|
||||
) as selections_list,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'label_id', lo.label_id,
|
||||
'label_name', lo.label_name,
|
||||
'label_color', lo.label_color
|
||||
)
|
||||
)
|
||||
FROM cc_label_options lo
|
||||
WHERE lo.column_id = cc.id
|
||||
) as labels_list
|
||||
FROM cc_custom_columns cc
|
||||
LEFT JOIN cc_column_configurations cf ON cf.column_id = cc.id
|
||||
WHERE cc.project_id = $1
|
||||
)
|
||||
SELECT
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'key', cd.key,
|
||||
'id', cd.id,
|
||||
'name', cd.name,
|
||||
'width', cd.width,
|
||||
'pinned', cd.is_visible,
|
||||
'custom_column', true,
|
||||
'custom_column_obj', json_build_object(
|
||||
'fieldType', cd.field_type,
|
||||
'fieldTitle', cd.field_title,
|
||||
'numberType', cd.number_type,
|
||||
'decimals', cd.decimals,
|
||||
'label', cd.label,
|
||||
'labelPosition', cd.label_position,
|
||||
'previewValue', cd.preview_value,
|
||||
'expression', cd.expression,
|
||||
'firstNumericColumnKey', cd.first_numeric_column_key,
|
||||
'secondNumericColumnKey', cd.second_numeric_column_key,
|
||||
'selectionsList', COALESCE(cd.selections_list, '[]'::json),
|
||||
'labelsList', COALESCE(cd.labels_list, '[]'::json)
|
||||
)
|
||||
)
|
||||
) as columns
|
||||
FROM column_data cd;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [project_id]);
|
||||
const columns = result.rows[0]?.columns || [];
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, columns));
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export default class HomePageController extends WorklenzControllerBase {
|
||||
p.team_id,
|
||||
p.name AS project_name,
|
||||
p.color_code AS project_color,
|
||||
(SELECT id FROM task_statuses WHERE id = t.status_id) AS status,
|
||||
(SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
|
||||
|
||||
@@ -59,7 +59,7 @@ export default class IndexController extends WorklenzControllerBase {
|
||||
if (req.user && !req.user.is_member)
|
||||
return res.redirect("/teams");
|
||||
|
||||
return res.redirect("/auth");
|
||||
return res.redirect(301, "/auth");
|
||||
}
|
||||
|
||||
public static redirectToLogin(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
|
||||
@@ -195,7 +195,7 @@ export default class ProjectCommentsController extends WorklenzControllerBase {
|
||||
pc.created_at,
|
||||
pc.updated_at
|
||||
FROM project_comments pc
|
||||
WHERE pc.project_id = $1 ORDER BY pc.updated_at DESC
|
||||
WHERE pc.project_id = $1 ORDER BY pc.updated_at
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {getColor} from "../shared/utils";
|
||||
import TeamMembersController from "./team-members-controller";
|
||||
import {checkTeamSubscriptionStatus} from "../shared/paddle-utils";
|
||||
import {updateUsers} from "../shared/paddle-requests";
|
||||
import {statusExclude} from "../shared/constants";
|
||||
import {NotificationsService} from "../services/notifications/notifications.service";
|
||||
|
||||
export default class ProjectMembersController extends WorklenzControllerBase {
|
||||
@@ -69,6 +72,70 @@ export default class ProjectMembersController extends WorklenzControllerBase {
|
||||
|
||||
if (!req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
|
||||
// check the subscription status
|
||||
const subscriptionData = await checkTeamSubscriptionStatus(req.user?.team_id);
|
||||
|
||||
const userExists = await this.checkIfUserAlreadyExists(req.user?.owner_id as string, req.body.email);
|
||||
|
||||
// Return error if user already exists
|
||||
if (userExists) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "User already exists in the team."));
|
||||
}
|
||||
|
||||
// Handle self-hosted subscriptions differently
|
||||
if (subscriptionData.subscription_type === 'SELF_HOSTED') {
|
||||
// Adding as a team member
|
||||
const teamMemberReq: { team_id?: string; emails: string[], project_id?: string; } = {
|
||||
team_id: req.user?.team_id,
|
||||
emails: [req.body.email]
|
||||
};
|
||||
|
||||
if (req.body.project_id)
|
||||
teamMemberReq.project_id = req.body.project_id;
|
||||
|
||||
const [member] = await TeamMembersController.createOrInviteMembers(teamMemberReq, req.user);
|
||||
|
||||
if (!member)
|
||||
return res.status(200).send(new ServerResponse(true, null, "Failed to add the member to the project. Please try again."));
|
||||
|
||||
// Adding to the project
|
||||
const projectMemberReq = {
|
||||
team_member_id: member.team_member_id,
|
||||
team_id: req.user?.team_id,
|
||||
project_id: req.body.project_id,
|
||||
user_id: req.user?.id,
|
||||
access_level: req.body.access_level ? req.body.access_level : "MEMBER"
|
||||
};
|
||||
const data = await this.createOrInviteMembers(projectMemberReq);
|
||||
return res.status(200).send(new ServerResponse(true, data.member));
|
||||
}
|
||||
|
||||
if (statusExclude.includes(subscriptionData.subscription_status)) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Unable to add user! Please check your subscription status."));
|
||||
}
|
||||
|
||||
if (!userExists && subscriptionData.is_ltd && subscriptionData.current_count && (parseInt(subscriptionData.current_count) + 1 > parseInt(subscriptionData.ltd_users))) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Maximum number of life time users reached."));
|
||||
}
|
||||
|
||||
// if (subscriptionData.status === "trialing") break;
|
||||
if (!userExists && !subscriptionData.is_credit && !subscriptionData.is_custom && subscriptionData.subscription_status !== "trialing") {
|
||||
// if (subscriptionData.subscription_status === "active") {
|
||||
// const response = await updateUsers(subscriptionData.subscription_id, (subscriptionData.quantity + 1));
|
||||
// if (!response.body.subscription_id) return res.status(200).send(new ServerResponse(false, null, response.message || "Unable to add user! Please check your subscription."));
|
||||
// }
|
||||
const updatedCount = parseInt(subscriptionData.current_count) + 1;
|
||||
const requiredSeats = updatedCount - subscriptionData.quantity;
|
||||
if (updatedCount > subscriptionData.quantity) {
|
||||
const obj = {
|
||||
seats_enough: false,
|
||||
required_count: requiredSeats,
|
||||
current_seat_amount: subscriptionData.quantity
|
||||
};
|
||||
return res.status(200).send(new ServerResponse(false, obj, null));
|
||||
}
|
||||
}
|
||||
|
||||
// Adding as a team member
|
||||
const teamMemberReq: { team_id?: string; emails: string[], project_id?: string; } = {
|
||||
team_id: req.user?.team_id,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { templateData } from "./project-templates";
|
||||
import ProjectTemplatesControllerBase from "./project-templates-base";
|
||||
import { LOG_DESCRIPTIONS, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
|
||||
import { IO } from "../../shared/io";
|
||||
import { getCurrentProjectsCount, getFreePlanSettings } from "../../shared/paddle-utils";
|
||||
|
||||
export default class ProjectTemplatesController extends ProjectTemplatesControllerBase {
|
||||
|
||||
@@ -46,10 +47,10 @@ export default class ProjectTemplatesController extends ProjectTemplatesControll
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getDefaultProjectHealth() {
|
||||
const q = `SELECT id FROM sys_project_healths WHERE is_default IS TRUE`;
|
||||
const result = await db.query(q, []);
|
||||
const [data] = result.rows;
|
||||
return data.id;
|
||||
const q = `SELECT id FROM sys_project_healths WHERE is_default IS TRUE`;
|
||||
const result = await db.query(q, []);
|
||||
const [data] = result.rows;
|
||||
return data.id;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
@@ -92,6 +93,16 @@ export default class ProjectTemplatesController extends ProjectTemplatesControll
|
||||
|
||||
@HandleExceptions()
|
||||
public static async importTemplates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
if (req.user?.subscription_status === "free" && req.user?.owner_id) {
|
||||
const limits = await getFreePlanSettings();
|
||||
const projectsCount = await getCurrentProjectsCount(req.user.owner_id);
|
||||
const projectsLimit = parseInt(limits.projects_limit);
|
||||
|
||||
if (parseInt(projectsCount) >= projectsLimit) {
|
||||
return res.status(200).send(new ServerResponse(false, [], `Sorry, the free plan cannot have more than ${projectsLimit} projects.`));
|
||||
}
|
||||
}
|
||||
|
||||
const { template_id } = req.body;
|
||||
let project_id: string | null = null;
|
||||
|
||||
@@ -202,6 +213,16 @@ export default class ProjectTemplatesController extends ProjectTemplatesControll
|
||||
|
||||
@HandleExceptions()
|
||||
public static async importCustomTemplate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
if (req.user?.subscription_status === "free" && req.user?.owner_id) {
|
||||
const limits = await getFreePlanSettings();
|
||||
const projectsCount = await getCurrentProjectsCount(req.user.owner_id);
|
||||
const projectsLimit = parseInt(limits.projects_limit);
|
||||
|
||||
if (parseInt(projectsCount) >= projectsLimit) {
|
||||
return res.status(200).send(new ServerResponse(false, [], `Sorry, the free plan cannot have more than ${projectsLimit} projects.`));
|
||||
}
|
||||
}
|
||||
|
||||
const { template_id } = req.body;
|
||||
let project_id: string | null = null;
|
||||
|
||||
@@ -223,8 +244,8 @@ export default class ProjectTemplatesController extends ProjectTemplatesControll
|
||||
await this.deleteDefaultStatusForProject(project_id as string);
|
||||
await this.insertTeamLabels(data.labels, req.user?.team_id);
|
||||
await this.insertProjectPhases(data.phases, project_id as string);
|
||||
await this.insertProjectStatuses(data.status, project_id as string, data.team_id );
|
||||
await this.insertProjectTasksFromCustom(data.tasks, data.team_id, project_id as string, data.user_id, IO.getSocketById(req.user?.socket_id as string));
|
||||
await this.insertProjectStatuses(data.status, project_id as string, data.team_id);
|
||||
await this.insertProjectTasksFromCustom(data.tasks, data.team_id, project_id as string, data.user_id, IO.getSocketById(req.user?.socket_id as string));
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { project_id }));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { NotificationsService } from "../services/notifications/notifications.se
|
||||
import { IPassportSession } from "../interfaces/passport-session";
|
||||
import { SocketEvents } from "../socket.io/events";
|
||||
import { IO } from "../shared/io";
|
||||
import { getCurrentProjectsCount, getFreePlanSettings } from "../shared/paddle-utils";
|
||||
|
||||
export default class ProjectsController extends WorklenzControllerBase {
|
||||
|
||||
@@ -61,6 +62,16 @@ export default class ProjectsController extends WorklenzControllerBase {
|
||||
}
|
||||
})
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
if (req.user?.subscription_status === "free" && req.user?.owner_id) {
|
||||
const limits = await getFreePlanSettings();
|
||||
const projectsCount = await getCurrentProjectsCount(req.user.owner_id);
|
||||
const projectsLimit = parseInt(limits.projects_limit);
|
||||
|
||||
if (parseInt(projectsCount) >= projectsLimit) {
|
||||
return res.status(200).send(new ServerResponse(false, [], `Sorry, the free plan cannot have more than ${projectsLimit} projects.`));
|
||||
}
|
||||
}
|
||||
|
||||
const q = `SELECT create_project($1) AS project`;
|
||||
|
||||
req.body.team_id = req.user?.team_id || null;
|
||||
@@ -689,7 +700,8 @@ export default class ProjectsController extends WorklenzControllerBase {
|
||||
public static async toggleArchiveAll(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT toggle_archive_all_projects($1);`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data.toggle_archive_all_projects || []));
|
||||
}
|
||||
|
||||
public static async getProjectManager(projectId: string) {
|
||||
@@ -698,4 +710,47 @@ export default class ProjectsController extends WorklenzControllerBase {
|
||||
return result.rows || [];
|
||||
}
|
||||
|
||||
public static async updateExistPhaseColors() {
|
||||
const q = `SELECT id, name FROM project_phases`;
|
||||
const phases = await db.query(q);
|
||||
|
||||
phases.rows.forEach((phase) => {
|
||||
phase.color_code = getColor(phase.name);
|
||||
});
|
||||
|
||||
const body = {
|
||||
phases: phases.rows
|
||||
};
|
||||
|
||||
const q2 = `SELECT update_existing_phase_colors($1)`;
|
||||
await db.query(q2, [JSON.stringify(body)]);
|
||||
|
||||
}
|
||||
|
||||
public static async updateExistSortOrder() {
|
||||
const q = `SELECT id, project_id FROM project_phases ORDER BY name`;
|
||||
const phases = await db.query(q);
|
||||
|
||||
const sortNumbers: any = {};
|
||||
|
||||
phases.rows.forEach(phase => {
|
||||
const projectId = phase.project_id;
|
||||
|
||||
if (!sortNumbers[projectId]) {
|
||||
sortNumbers[projectId] = 0;
|
||||
}
|
||||
|
||||
phase.sort_number = sortNumbers[projectId]++;
|
||||
});
|
||||
|
||||
const body = {
|
||||
phases: phases.rows
|
||||
};
|
||||
|
||||
const q2 = `SELECT update_existing_phase_sort_order($1)`;
|
||||
await db.query(q2, [JSON.stringify(body)]);
|
||||
// return phases;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IChartObject } from "./overview/reporting-overview-base";
|
||||
import * as Highcharts from "highcharts";
|
||||
|
||||
export interface IDuration {
|
||||
label: string;
|
||||
@@ -34,7 +34,7 @@ export interface IOverviewStatistics {
|
||||
}
|
||||
|
||||
export interface IChartData {
|
||||
chart: IChartObject[];
|
||||
chart: Highcharts.PointOptionsObject[];
|
||||
}
|
||||
|
||||
export interface ITasksByStatus extends IChartData {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import db from "../../../config/db";
|
||||
import * as Highcharts from "highcharts";
|
||||
import { ITasksByDue, ITasksByPriority, ITasksByStatus } from "../interfaces";
|
||||
import ReportingControllerBase from "../reporting-controller-base";
|
||||
import {
|
||||
@@ -15,36 +16,33 @@ import {
|
||||
TASK_STATUS_TODO_COLOR
|
||||
} from "../../../shared/constants";
|
||||
import { formatDuration, int } from "../../../shared/utils";
|
||||
import PointOptionsObject from "../point-options-object";
|
||||
import moment from "moment";
|
||||
|
||||
export interface IChartObject {
|
||||
name: string,
|
||||
color: string,
|
||||
y: number
|
||||
}
|
||||
|
||||
export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
private static createChartObject(name: string, color: string, y: number) {
|
||||
return {
|
||||
name,
|
||||
color,
|
||||
y
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getTeamsCounts(teamId: string | null, archivedQuery = "") {
|
||||
|
||||
const q = `
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'teams', (SELECT COUNT(*) FROM teams WHERE in_organization(id, $1)),
|
||||
'projects',
|
||||
(SELECT COUNT(*) FROM projects WHERE in_organization(team_id, $1) ${archivedQuery}),
|
||||
'team_members', (SELECT COUNT(DISTINCT email)
|
||||
FROM team_member_info_view
|
||||
WHERE in_organization(team_id, $1))
|
||||
) AS counts;
|
||||
`;
|
||||
WITH team_count AS (
|
||||
SELECT COUNT(*) AS count
|
||||
FROM teams
|
||||
WHERE in_organization(id, $1)
|
||||
),
|
||||
project_count AS (
|
||||
SELECT COUNT(*) AS count
|
||||
FROM projects
|
||||
WHERE in_organization(team_id, $1) ${archivedQuery}
|
||||
),
|
||||
team_member_count AS (
|
||||
SELECT COUNT(DISTINCT email) AS count
|
||||
FROM team_member_info_view
|
||||
WHERE in_organization(team_id, $1)
|
||||
)
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'teams', (SELECT count FROM team_count),
|
||||
'projects', (SELECT count FROM project_count),
|
||||
'team_members', (SELECT count FROM team_member_count)
|
||||
) AS counts;`;
|
||||
|
||||
const res = await db.query(q, [teamId]);
|
||||
const [data] = res.rows;
|
||||
@@ -173,7 +171,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
const doing = int(data?.counts.doing);
|
||||
const done = int(data?.counts.done);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
const chart: Highcharts.PointOptionsObject[] = [];
|
||||
|
||||
return {
|
||||
all,
|
||||
@@ -209,7 +207,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
const medium = int(data?.counts.medium);
|
||||
const high = int(data?.counts.high);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
const chart: Highcharts.PointOptionsObject[] = [];
|
||||
|
||||
return {
|
||||
all: 0,
|
||||
@@ -237,7 +235,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
const res = await db.query(q, [projectId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
const chart: Highcharts.PointOptionsObject[] = [];
|
||||
|
||||
return {
|
||||
all: 0,
|
||||
@@ -251,26 +249,26 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
protected static createByStatusChartData(body: ITasksByStatus) {
|
||||
body.chart = [
|
||||
this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, body.todo),
|
||||
this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, body.doing),
|
||||
this.createChartObject("Done", TASK_STATUS_DONE_COLOR, body.done),
|
||||
new PointOptionsObject("Todo", TASK_STATUS_TODO_COLOR, body.todo),
|
||||
new PointOptionsObject("Doing", TASK_STATUS_DOING_COLOR, body.doing),
|
||||
new PointOptionsObject("Done", TASK_STATUS_DONE_COLOR, body.done),
|
||||
];
|
||||
}
|
||||
|
||||
protected static createByPriorityChartData(body: ITasksByPriority) {
|
||||
body.chart = [
|
||||
this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, body.low),
|
||||
this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, body.medium),
|
||||
this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, body.high),
|
||||
new PointOptionsObject("Low", TASK_PRIORITY_LOW_COLOR, body.low),
|
||||
new PointOptionsObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, body.medium),
|
||||
new PointOptionsObject("High", TASK_PRIORITY_HIGH_COLOR, body.high),
|
||||
];
|
||||
}
|
||||
|
||||
protected static createByDueDateChartData(body: ITasksByDue) {
|
||||
body.chart = [
|
||||
this.createChartObject("Completed", TASK_DUE_COMPLETED_COLOR, body.completed),
|
||||
this.createChartObject("Upcoming", TASK_DUE_UPCOMING_COLOR, body.upcoming),
|
||||
this.createChartObject("Overdue", TASK_DUE_OVERDUE_COLOR, body.overdue),
|
||||
this.createChartObject("No due date", TASK_DUE_NO_DUE_COLOR, body.no_due),
|
||||
new PointOptionsObject("Completed", TASK_DUE_COMPLETED_COLOR, body.completed),
|
||||
new PointOptionsObject("Upcoming", TASK_DUE_UPCOMING_COLOR, body.upcoming),
|
||||
new PointOptionsObject("Overdue", TASK_DUE_OVERDUE_COLOR, body.overdue),
|
||||
new PointOptionsObject("No due date", TASK_DUE_NO_DUE_COLOR, body.no_due),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -581,7 +579,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
`;
|
||||
const result = await db.query(q, [teamMemberId]);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
const chart: Highcharts.PointOptionsObject[] = [];
|
||||
|
||||
const total = result.rows.reduce((accumulator: number, current: {
|
||||
count: number
|
||||
@@ -589,7 +587,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
for (const project of result.rows) {
|
||||
project.count = int(project.count);
|
||||
chart.push(this.createChartObject(project.label, project.color, project.count));
|
||||
chart.push(new PointOptionsObject(project.label, project.color, project.count));
|
||||
}
|
||||
|
||||
return { chart, total, data: result.rows };
|
||||
@@ -635,7 +633,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
`;
|
||||
const result = await db.query(q, [teamMemberId]);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
const chart: Highcharts.PointOptionsObject[] = [];
|
||||
|
||||
const total = result.rows.reduce((accumulator: number, current: {
|
||||
count: number
|
||||
@@ -643,7 +641,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
for (const project of result.rows) {
|
||||
project.count = int(project.count);
|
||||
chart.push(this.createChartObject(project.label, project.color, project.count));
|
||||
chart.push(new PointOptionsObject(project.label, project.color, project.count));
|
||||
}
|
||||
|
||||
return { chart, total, data: result.rows };
|
||||
@@ -673,10 +671,10 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
const total = int(d.low) + int(d.medium) + int(d.high);
|
||||
|
||||
const chart = [
|
||||
this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
|
||||
this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
|
||||
this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
|
||||
const chart: Highcharts.PointOptionsObject[] = [
|
||||
new PointOptionsObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
|
||||
new PointOptionsObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
|
||||
new PointOptionsObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
|
||||
];
|
||||
|
||||
const data = [
|
||||
@@ -730,10 +728,10 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
const total = int(d.low) + int(d.medium) + int(d.high);
|
||||
|
||||
const chart = [
|
||||
this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
|
||||
this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
|
||||
this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
|
||||
const chart: Highcharts.PointOptionsObject[] = [
|
||||
new PointOptionsObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
|
||||
new PointOptionsObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
|
||||
new PointOptionsObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
|
||||
];
|
||||
|
||||
const data = [
|
||||
@@ -784,10 +782,10 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
const total = int(d.total);
|
||||
|
||||
const chart = [
|
||||
this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
|
||||
this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
|
||||
this.createChartObject("Done", TASK_STATUS_DONE_COLOR, d.done),
|
||||
const chart: Highcharts.PointOptionsObject[] = [
|
||||
new PointOptionsObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
|
||||
new PointOptionsObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
|
||||
new PointOptionsObject("Done", TASK_STATUS_DONE_COLOR, d.done),
|
||||
];
|
||||
|
||||
const data = [
|
||||
@@ -826,10 +824,10 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
const total = int(d.todo) + int(d.doing) + int(d.done);
|
||||
|
||||
const chart = [
|
||||
this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
|
||||
this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
|
||||
this.createChartObject("Done", TASK_STATUS_DONE_COLOR, d.done),
|
||||
const chart: Highcharts.PointOptionsObject[] = [
|
||||
new PointOptionsObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
|
||||
new PointOptionsObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
|
||||
new PointOptionsObject("Done", TASK_STATUS_DONE_COLOR, d.done),
|
||||
];
|
||||
|
||||
const data = [
|
||||
@@ -878,7 +876,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
const in_progress = int(data?.counts.in_progress);
|
||||
const completed = int(data?.counts.completed);
|
||||
|
||||
const chart : IChartObject[] = [];
|
||||
const chart: Highcharts.PointOptionsObject[] = [];
|
||||
|
||||
return {
|
||||
all,
|
||||
@@ -908,7 +906,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
`;
|
||||
const result = await db.query(q, [teamId]);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
const chart: Highcharts.PointOptionsObject[] = [];
|
||||
|
||||
const total = result.rows.reduce((accumulator: number, current: {
|
||||
count: number
|
||||
@@ -916,11 +914,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
for (const category of result.rows) {
|
||||
category.count = int(category.count);
|
||||
chart.push({
|
||||
name: category.label,
|
||||
color: category.color,
|
||||
y: category.count
|
||||
});
|
||||
chart.push(new PointOptionsObject(category.label, category.color, category.count));
|
||||
}
|
||||
|
||||
return { chart, total, data: result.rows };
|
||||
@@ -956,7 +950,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
const at_risk = int(data?.counts.at_risk);
|
||||
const good = int(data?.counts.good);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
const chart: Highcharts.PointOptionsObject[] = [];
|
||||
|
||||
return {
|
||||
not_set,
|
||||
@@ -971,22 +965,22 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
// Team Overview
|
||||
protected static createByProjectStatusChartData(body: any) {
|
||||
body.chart = [
|
||||
this.createChartObject("Cancelled", "#f37070", body.cancelled),
|
||||
this.createChartObject("Blocked", "#cbc8a1", body.blocked),
|
||||
this.createChartObject("On Hold", "#cbc8a1", body.on_hold),
|
||||
this.createChartObject("Proposed", "#cbc8a1", body.proposed),
|
||||
this.createChartObject("In Planning", "#cbc8a1", body.in_planning),
|
||||
this.createChartObject("In Progress", "#80ca79", body.in_progress),
|
||||
this.createChartObject("Completed", "#80ca79", body.completed)
|
||||
new PointOptionsObject("Cancelled", "#f37070", body.cancelled),
|
||||
new PointOptionsObject("Blocked", "#cbc8a1", body.blocked),
|
||||
new PointOptionsObject("On Hold", "#cbc8a1", body.on_hold),
|
||||
new PointOptionsObject("Proposed", "#cbc8a1", body.proposed),
|
||||
new PointOptionsObject("In Planning", "#cbc8a1", body.in_planning),
|
||||
new PointOptionsObject("In Progress", "#80ca79", body.in_progress),
|
||||
new PointOptionsObject("Completed", "#80ca79", body.completed),
|
||||
];
|
||||
}
|
||||
|
||||
protected static createByProjectHealthChartData(body: any) {
|
||||
body.chart = [
|
||||
this.createChartObject("Not Set", "#a9a9a9", body.not_set),
|
||||
this.createChartObject("Needs Attention", "#f37070", body.needs_attention),
|
||||
this.createChartObject("At Risk", "#fbc84c", body.at_risk),
|
||||
this.createChartObject("Good", "#75c997", body.good)
|
||||
new PointOptionsObject("Not Set", "#a9a9a9", body.not_set),
|
||||
new PointOptionsObject("Needs Attention", "#f37070", body.needs_attention),
|
||||
new PointOptionsObject("At Risk", "#fbc84c", body.at_risk),
|
||||
new PointOptionsObject("Good", "#75c997", body.good)
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as Highcharts from "highcharts";
|
||||
|
||||
export default class PointOptionsObject implements Highcharts.PointOptionsObject {
|
||||
name!: string;
|
||||
color!: string;
|
||||
y!: number;
|
||||
|
||||
constructor(name: string, color: string, y: number) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,14 @@ import { getColor, int, log_error } from "../../shared/utils";
|
||||
import ReportingControllerBase from "./reporting-controller-base";
|
||||
import { DATE_RANGES } from "../../shared/constants";
|
||||
import Excel from "exceljs";
|
||||
import ChartJsImage from "chartjs-to-image";
|
||||
|
||||
enum IToggleOptions {
|
||||
'WORKING_DAYS' = 'WORKING_DAYS', 'MAN_DAYS' = 'MAN_DAYS'
|
||||
}
|
||||
|
||||
export default class ReportingAllocationController extends ReportingControllerBase {
|
||||
private static async getTimeLoggedByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false, user_id = ""): Promise<any> {
|
||||
private static async getTimeLoggedByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false, user_id = "", billable: { billable: boolean; nonBillable: boolean }): Promise<any> {
|
||||
try {
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
const userIds = users.map(u => `'${u}'`).join(",");
|
||||
@@ -24,8 +25,10 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
? ""
|
||||
: `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND user_id = '${user_id}') `;
|
||||
|
||||
const projectTimeLogs = await this.getTotalTimeLogsByProject(archived, duration, projectIds, userIds, archivedClause);
|
||||
const userTimeLogs = await this.getTotalTimeLogsByUser(archived, duration, projectIds, userIds);
|
||||
const billableQuery = this.buildBillableQuery(billable);
|
||||
|
||||
const projectTimeLogs = await this.getTotalTimeLogsByProject(archived, duration, projectIds, userIds, archivedClause, billableQuery);
|
||||
const userTimeLogs = await this.getTotalTimeLogsByUser(archived, duration, projectIds, userIds, billableQuery);
|
||||
|
||||
const format = (seconds: number) => {
|
||||
if (seconds === 0) return "-";
|
||||
@@ -65,7 +68,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
return [];
|
||||
}
|
||||
|
||||
private static async getTotalTimeLogsByProject(archived: boolean, duration: string, projectIds: string, userIds: string, archivedClause = "") {
|
||||
private static async getTotalTimeLogsByProject(archived: boolean, duration: string, projectIds: string, userIds: string, archivedClause = "", billableQuery = '') {
|
||||
try {
|
||||
const q = `SELECT projects.name,
|
||||
projects.color_code,
|
||||
@@ -74,12 +77,12 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
sps.icon AS status_icon,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
|
||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END ${billableQuery}
|
||||
AND project_id = projects.id) AS all_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
|
||||
AND project_id = projects.id
|
||||
AND project_id = projects.id ${billableQuery}
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE project_id = projects.id
|
||||
@@ -91,10 +94,10 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
SELECT name,
|
||||
(SELECT COALESCE(SUM(time_spent), 0)
|
||||
FROM task_work_log
|
||||
LEFT JOIN tasks t ON task_work_log.task_id = t.id
|
||||
WHERE user_id = users.id
|
||||
AND CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
|
||||
AND t.project_id = projects.id
|
||||
LEFT JOIN tasks ON task_work_log.task_id = tasks.id
|
||||
WHERE user_id = users.id ${billableQuery}
|
||||
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
|
||||
AND tasks.project_id = projects.id
|
||||
${duration}) AS time_logged
|
||||
FROM users
|
||||
WHERE id IN (${userIds})
|
||||
@@ -113,15 +116,15 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
}
|
||||
}
|
||||
|
||||
private static async getTotalTimeLogsByUser(archived: boolean, duration: string, projectIds: string, userIds: string) {
|
||||
private static async getTotalTimeLogsByUser(archived: boolean, duration: string, projectIds: string, userIds: string, billableQuery = "") {
|
||||
try {
|
||||
const q = `(SELECT id,
|
||||
(SELECT COALESCE(SUM(time_spent), 0)
|
||||
FROM task_work_log
|
||||
LEFT JOIN tasks t ON task_work_log.task_id = t.id
|
||||
LEFT JOIN tasks ON task_work_log.task_id = tasks.id ${billableQuery}
|
||||
WHERE user_id = users.id
|
||||
AND CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
|
||||
AND t.project_id IN (${projectIds})
|
||||
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
|
||||
AND tasks.project_id IN (${projectIds})
|
||||
${duration}) AS time_logged
|
||||
FROM users
|
||||
WHERE id IN (${userIds})
|
||||
@@ -154,6 +157,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
@HandleExceptions()
|
||||
public static async getAllocation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teams = (req.body.teams || []) as string[]; // ids
|
||||
const billable = req.body.billable;
|
||||
|
||||
const teamIds = teams.map(id => `'${id}'`).join(",");
|
||||
const projectIds = (req.body.projects || []) as string[];
|
||||
@@ -164,7 +168,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
const users = await this.getUserIds(teamIds);
|
||||
const userIds = users.map((u: any) => u.id);
|
||||
|
||||
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, req.body.duration, req.body.date_range, (req.query.archived === "true"), req.user?.id);
|
||||
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, req.body.duration, req.body.date_range, (req.query.archived === "true"), req.user?.id, billable);
|
||||
|
||||
for (const [i, user] of users.entries()) {
|
||||
user.total_time = userTimeLogs[i].time_logged;
|
||||
@@ -184,6 +188,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
public static async export(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const teams = (req.query.teams as string)?.split(",");
|
||||
const teamIds = teams.map(t => `'${t}'`).join(",");
|
||||
const billable = req.body.billable ? req.body.billable : { billable: req.query.billable === "true", nonBillable: req.query.nonBillable === "true" };
|
||||
|
||||
const projectIds = (req.query.projects as string)?.split(",");
|
||||
|
||||
@@ -218,7 +223,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
const users = await this.getUserIds(teamIds);
|
||||
const userIds = users.map((u: any) => u.id);
|
||||
|
||||
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, duration as string, dateRange, (req.query.include_archived === "true"), req.user?.id);
|
||||
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, duration as string, dateRange, (req.query.include_archived === "true"), req.user?.id, billable);
|
||||
|
||||
for (const [i, user] of users.entries()) {
|
||||
user.total_time = userTimeLogs[i].time_logged;
|
||||
@@ -341,6 +346,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
|
||||
const billable = req.body.billable;
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
|
||||
|
||||
@@ -352,6 +359,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const billableQuery = this.buildBillableQuery(billable);
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.name,
|
||||
@@ -359,8 +368,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
SUM(total_minutes) AS estimated,
|
||||
color_code
|
||||
FROM projects p
|
||||
LEFT JOIN tasks t ON t.project_id = p.id
|
||||
LEFT JOIN task_work_log ON task_work_log.task_id = t.id
|
||||
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
|
||||
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
|
||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
|
||||
GROUP BY p.id, p.name
|
||||
ORDER BY logged_time DESC;`;
|
||||
@@ -372,7 +381,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
project.value = project.logged_time ? parseFloat(moment.duration(project.logged_time, "seconds").asHours().toFixed(2)) : 0;
|
||||
project.estimated_value = project.estimated ? parseFloat(moment.duration(project.estimated, "minutes").asHours().toFixed(2)) : 0;
|
||||
|
||||
if (project.value > 0 ) {
|
||||
if (project.value > 0) {
|
||||
data.push(project);
|
||||
}
|
||||
|
||||
@@ -392,6 +401,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
|
||||
const billable = req.body.billable;
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
|
||||
|
||||
@@ -402,12 +413,14 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const billableQuery = this.buildBillableQuery(billable);
|
||||
|
||||
const q = `
|
||||
SELECT tmiv.email, tmiv.name, SUM(time_spent) AS logged_time
|
||||
FROM team_member_info_view tmiv
|
||||
LEFT JOIN task_work_log ON task_work_log.user_id = tmiv.user_id
|
||||
LEFT JOIN tasks t ON t.id = task_work_log.task_id
|
||||
LEFT JOIN projects p ON p.id = t.project_id AND p.team_id = tmiv.team_id
|
||||
LEFT JOIN tasks ON tasks.id = task_work_log.task_id ${billableQuery}
|
||||
LEFT JOIN projects p ON p.id = tasks.project_id AND p.team_id = tmiv.team_id
|
||||
WHERE p.id IN (${projectIds})
|
||||
${durationClause} ${archivedClause}
|
||||
GROUP BY tmiv.email, tmiv.name
|
||||
@@ -422,7 +435,64 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async exportTest(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const archived = req.query.archived === "true";
|
||||
const teamId = this.getCurrentTeamId(req);
|
||||
const { duration, date_range } = req.query;
|
||||
|
||||
const durationClause = this.getDateRangeClause(duration as string || DATE_RANGES.LAST_WEEK, date_range as string[]);
|
||||
|
||||
const archivedClause = archived
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.name,
|
||||
(SELECT SUM(time_spent)) AS logged_time,
|
||||
SUM(total_minutes) AS estimated,
|
||||
color_code
|
||||
FROM projects p
|
||||
LEFT JOIN tasks t ON t.project_id = p.id
|
||||
LEFT JOIN task_work_log ON task_work_log.task_id = t.id
|
||||
WHERE in_organization(p.team_id, $1)
|
||||
${durationClause} ${archivedClause}
|
||||
GROUP BY p.id, p.name
|
||||
ORDER BY p.name ASC;`;
|
||||
const result = await db.query(q, [teamId]);
|
||||
|
||||
const labelsX = [];
|
||||
const dataX = [];
|
||||
|
||||
for (const project of result.rows) {
|
||||
project.value = project.logged_time ? parseFloat(moment.duration(project.logged_time, "seconds").asHours().toFixed(2)) : 0;
|
||||
project.estimated_value = project.estimated ? parseFloat(moment.duration(project.estimated, "minutes").asHours().toFixed(2)) : 0;
|
||||
labelsX.push(project.name);
|
||||
dataX.push(project.value || 0);
|
||||
}
|
||||
|
||||
const chart = new ChartJsImage();
|
||||
chart.setConfig({
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: labelsX,
|
||||
datasets: [
|
||||
{ label: "", data: dataX }
|
||||
]
|
||||
},
|
||||
});
|
||||
chart.setWidth(1920).setHeight(1080).setBackgroundColor("transparent");
|
||||
const url = chart.getUrl();
|
||||
chart.toFile("test.png");
|
||||
return res.status(200).send(new ServerResponse(true, url));
|
||||
}
|
||||
|
||||
private static getEstimated(project: any, type: string) {
|
||||
// if (project.estimated_man_days === 0 || project.estimated_working_days === 0) {
|
||||
// return (parseFloat(moment.duration(project.estimated, "minutes").asHours().toFixed(2)) / int(project.hours_per_day)).toFixed(2)
|
||||
// }
|
||||
|
||||
switch (type) {
|
||||
case IToggleOptions.MAN_DAYS:
|
||||
@@ -445,7 +515,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
const { type } = req.body;
|
||||
const { type, billable } = req.body;
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
|
||||
@@ -458,6 +528,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const billableQuery = this.buildBillableQuery(billable);
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.name,
|
||||
@@ -471,8 +543,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
WHERE project_id = p.id) AS estimated,
|
||||
color_code
|
||||
FROM projects p
|
||||
LEFT JOIN tasks t ON t.project_id = p.id
|
||||
LEFT JOIN task_work_log ON task_work_log.task_id = t.id
|
||||
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
|
||||
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
|
||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
|
||||
GROUP BY p.id, p.name
|
||||
ORDER BY logged_time DESC;`;
|
||||
@@ -491,7 +563,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
project.estimated_working_days = project.estimated_working_days ?? 0;
|
||||
project.hours_per_day = project.hours_per_day ?? 0;
|
||||
|
||||
if (project.value > 0 || project.estimated_value > 0 ) {
|
||||
if (project.value > 0 || project.estimated_value > 0) {
|
||||
data.push(project);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,23 @@ export default abstract class ReportingControllerBase extends WorklenzController
|
||||
return "";
|
||||
}
|
||||
|
||||
protected static buildBillableQuery(selectedStatuses: { billable: boolean; nonBillable: boolean }): string {
|
||||
const { billable, nonBillable } = selectedStatuses;
|
||||
|
||||
if (billable && nonBillable) {
|
||||
// Both are enabled, no need to filter
|
||||
return "";
|
||||
} else if (billable) {
|
||||
// Only billable is enabled
|
||||
return " AND tasks.billable IS TRUE";
|
||||
} else if (nonBillable) {
|
||||
// Only non-billable is enabled
|
||||
return " AND tasks.billable IS FALSE";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
protected static formatEndDate(endDate: string) {
|
||||
const end = moment(endDate).format("YYYY-MM-DD");
|
||||
const fEndDate = moment(end);
|
||||
@@ -173,6 +190,9 @@ export default abstract class ReportingControllerBase extends WorklenzController
|
||||
(SELECT color_code
|
||||
FROM sys_project_healths
|
||||
WHERE sys_project_healths.id = p.health_id) AS health_color,
|
||||
(SELECT name
|
||||
FROM sys_project_healths
|
||||
WHERE sys_project_healths.id = p.health_id) AS health_name,
|
||||
|
||||
pc.id AS category_id,
|
||||
pc.name AS category_name,
|
||||
|
||||
@@ -862,7 +862,7 @@ export default class ReportingMembersController extends ReportingControllerBase
|
||||
}
|
||||
|
||||
|
||||
private static async memberTimeLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived: boolean, userId: string) {
|
||||
private static async memberTimeLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived: boolean, userId: string, billableQuery = "") {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
@@ -884,7 +884,7 @@ export default class ReportingMembersController extends ReportingControllerBase
|
||||
FROM task_work_log twl
|
||||
WHERE twl.user_id = tmiv.user_id
|
||||
${durationClause}
|
||||
AND task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1) ${archivedClause} )
|
||||
AND task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1) ${archivedClause} ${billableQuery})
|
||||
ORDER BY twl.updated_at DESC) tl) AS time_logs
|
||||
${minMaxDateClause}
|
||||
FROM team_member_info_view tmiv
|
||||
@@ -1017,14 +1017,33 @@ export default class ReportingMembersController extends ReportingControllerBase
|
||||
|
||||
}
|
||||
|
||||
protected static buildBillableQuery(selectedStatuses: { billable: boolean; nonBillable: boolean }): string {
|
||||
const { billable, nonBillable } = selectedStatuses;
|
||||
|
||||
if (billable && nonBillable) {
|
||||
// Both are enabled, no need to filter
|
||||
return "";
|
||||
} else if (billable) {
|
||||
// Only billable is enabled
|
||||
return " AND tasks.billable IS TRUE";
|
||||
} else if (nonBillable) {
|
||||
// Only non-billable is enabled
|
||||
return " AND tasks.billable IS FALSE";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMemberTimelogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { team_member_id, team_id, duration, date_range, archived } = req.body;
|
||||
const { team_member_id, team_id, duration, date_range, archived, billable } = req.body;
|
||||
|
||||
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "twl");
|
||||
const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_work_log");
|
||||
|
||||
const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string);
|
||||
const billableQuery = this.buildBillableQuery(billable);
|
||||
|
||||
const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string, billableQuery);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, logGroups));
|
||||
}
|
||||
@@ -1049,6 +1068,7 @@ export default class ReportingMembersController extends ReportingControllerBase
|
||||
const completedDurationClasue = this.completedDurationFilter(duration as string, dateRange);
|
||||
const overdueClauseByDate = this.getActivityLogsOverdue(duration as string, dateRange);
|
||||
const taskSelectorClause = this.getTaskSelectorClause();
|
||||
const durationFilter = this.memberTasksDurationFilter(duration as string, dateRange);
|
||||
|
||||
const q = `
|
||||
SELECT name AS team_member_name,
|
||||
@@ -1059,6 +1079,12 @@ export default class ReportingMembersController extends ReportingControllerBase
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${assignClause} ${archivedClause}) assigned) AS assigned,
|
||||
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(assigned))), '[]')
|
||||
FROM (${taskSelectorClause}
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${durationFilter} ${assignClause} ${archivedClause}) assigned) AS total,
|
||||
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(completed))), '[]')
|
||||
FROM (${taskSelectorClause}
|
||||
FROM tasks t
|
||||
@@ -1095,6 +1121,11 @@ export default class ReportingMembersController extends ReportingControllerBase
|
||||
const body = {
|
||||
team_member_name: data.team_member_name,
|
||||
groups: [
|
||||
{
|
||||
name: "Total Tasks",
|
||||
color_code: "#7590c9",
|
||||
tasks: data.total ? data.total : 0
|
||||
},
|
||||
{
|
||||
name: "Tasks Assigned",
|
||||
color_code: "#7590c9",
|
||||
@@ -1114,7 +1145,7 @@ export default class ReportingMembersController extends ReportingControllerBase
|
||||
name: "Tasks Ongoing",
|
||||
color_code: "#7cb5ec",
|
||||
tasks: data.ongoing ? data.ongoing : 0
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
import db from "../../config/db";
|
||||
import { ParsedQs } from "qs";
|
||||
import HandleExceptions from "../../decorators/handle-exceptions";
|
||||
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../../models/server-response";
|
||||
import { TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED } from "../../shared/constants";
|
||||
import { getColor } from "../../shared/utils";
|
||||
import moment, { Moment } from "moment";
|
||||
import momentTime from "moment-timezone";
|
||||
import WorklenzControllerBase from "../worklenz-controller-base";
|
||||
|
||||
interface IDateUnions {
|
||||
date_union: {
|
||||
start_date: string | null;
|
||||
end_date: string | null;
|
||||
},
|
||||
logs_date_union: {
|
||||
start_date: string | null;
|
||||
end_date: string | null;
|
||||
},
|
||||
allocated_date_union: {
|
||||
start_date: string | null;
|
||||
end_date: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
interface IDatesPair {
|
||||
start_date: string | null,
|
||||
end_date: string | null
|
||||
}
|
||||
|
||||
export default class ScheduleControllerV2 extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getSettings(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
// get organization working days
|
||||
const getDataq = `SELECT organization_id, array_agg(initcap(day)) AS working_days
|
||||
FROM (
|
||||
SELECT organization_id,
|
||||
unnest(ARRAY['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']) AS day,
|
||||
unnest(ARRAY[monday, tuesday, wednesday, thursday, friday, saturday, sunday]) AS is_working
|
||||
FROM public.organization_working_days
|
||||
WHERE organization_id IN (
|
||||
SELECT id FROM organizations
|
||||
WHERE user_id = $1
|
||||
)
|
||||
) t
|
||||
WHERE t.is_working
|
||||
GROUP BY organization_id LIMIT 1;`;
|
||||
|
||||
const workingDaysResults = await db.query(getDataq, [req.user?.owner_id]);
|
||||
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 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 }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateSettings(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { workingDays, workingHours } = req.body;
|
||||
|
||||
// Days of the week
|
||||
const days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
||||
|
||||
// Generate the SET clause dynamically
|
||||
const setClause = days
|
||||
.map(day => `${day.toLowerCase()} = ${workingDays.includes(day)}`)
|
||||
.join(", ");
|
||||
|
||||
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
|
||||
);
|
||||
`;
|
||||
|
||||
await db.query(updateQuery, [req.user?.owner_id]);
|
||||
|
||||
const getDataHoursq = `UPDATE organizations SET working_hours = $1 WHERE user_id = $2;`;
|
||||
|
||||
await db.query(getDataHoursq, [workingHours, req.user?.owner_id]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {}));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getDates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const { date, type } = req.params;
|
||||
|
||||
if (type === "week") {
|
||||
const getDataq = `WITH input_date AS (
|
||||
SELECT
|
||||
$1::DATE AS given_date,
|
||||
(SELECT id FROM organizations WHERE user_id=$2 LIMIT 1) AS organization_id
|
||||
),
|
||||
week_range AS (
|
||||
SELECT
|
||||
(given_date - (EXTRACT(DOW FROM given_date)::INT + 6) % 7)::DATE AS start_date, -- Current week start date
|
||||
(given_date - (EXTRACT(DOW FROM given_date)::INT + 6) % 7 + 6)::DATE AS end_date, -- Current week end date
|
||||
(given_date - (EXTRACT(DOW FROM given_date)::INT + 6) % 7 + 7)::DATE AS next_week_start, -- Next week start date
|
||||
(given_date - (EXTRACT(DOW FROM given_date)::INT + 6) % 7 + 13)::DATE AS next_week_end, -- Next week end date
|
||||
TO_CHAR(given_date, 'Mon YYYY') AS month_year, -- Format the month as 'Jan 2025'
|
||||
EXTRACT(DAY FROM given_date) AS day_number, -- Extract the day from the date
|
||||
(given_date - (EXTRACT(DOW FROM given_date)::INT + 6) % 7)::DATE AS chart_start, -- First week start date
|
||||
(given_date - (EXTRACT(DOW FROM given_date)::INT + 6) % 7 + 13)::DATE AS chart_end, -- Second week end date
|
||||
CURRENT_DATE::DATE AS today,
|
||||
organization_id
|
||||
FROM input_date
|
||||
),
|
||||
org_working_days AS (
|
||||
SELECT
|
||||
organization_id,
|
||||
monday, tuesday, wednesday, thursday, friday, saturday, sunday
|
||||
FROM organization_working_days
|
||||
WHERE organization_id = (SELECT organization_id FROM week_range)
|
||||
),
|
||||
days AS (
|
||||
SELECT
|
||||
generate_series((SELECT start_date FROM week_range), (SELECT next_week_end FROM week_range), '1 day'::INTERVAL)::DATE AS date
|
||||
),
|
||||
formatted_days AS (
|
||||
SELECT
|
||||
d.date,
|
||||
TO_CHAR(d.date, 'Dy') AS day_name,
|
||||
EXTRACT(DAY FROM d.date) AS day,
|
||||
TO_CHAR(d.date, 'Mon YYYY') AS month, -- Format the month as 'Jan 2025'
|
||||
CASE
|
||||
WHEN EXTRACT(DOW FROM d.date) = 0 THEN (SELECT sunday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 1 THEN (SELECT monday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 2 THEN (SELECT tuesday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 3 THEN (SELECT wednesday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 4 THEN (SELECT thursday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 5 THEN (SELECT friday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 6 THEN (SELECT saturday FROM org_working_days)
|
||||
END AS is_weekend,
|
||||
CASE WHEN d.date = (SELECT today FROM week_range) THEN TRUE ELSE FALSE END AS is_today
|
||||
FROM days d
|
||||
),
|
||||
aggregated_days AS (
|
||||
SELECT
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'day', day,
|
||||
'month', month, -- Include formatted month
|
||||
'name', day_name,
|
||||
'isWeekend', NOT is_weekend,
|
||||
'isToday', is_today
|
||||
) ORDER BY date
|
||||
) AS days_json
|
||||
FROM formatted_days
|
||||
)
|
||||
SELECT jsonb_build_object(
|
||||
'date_data', jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'month', (SELECT month_year FROM week_range), -- Formatted month-year (e.g., Jan 2025)
|
||||
'day', (SELECT day_number FROM week_range), -- Dynamic day number
|
||||
'weeks', '[]', -- Empty weeks array for now
|
||||
'days', (SELECT days_json FROM aggregated_days) -- Aggregated days data
|
||||
)
|
||||
),
|
||||
'chart_start', (SELECT chart_start FROM week_range), -- First week start date
|
||||
'chart_end', (SELECT chart_end FROM week_range) -- Second week end date
|
||||
) AS result_json;`;
|
||||
|
||||
const results = await db.query(getDataq, [date, req.user?.owner_id]);
|
||||
const [data] = results.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data.result_json));
|
||||
} else if (type === "month") {
|
||||
|
||||
const getDataq = `WITH params AS (
|
||||
SELECT
|
||||
DATE_TRUNC('month', $1::DATE)::DATE AS start_date, -- First day of the month
|
||||
(DATE_TRUNC('month', $1::DATE) + INTERVAL '1 month' - INTERVAL '1 day')::DATE AS end_date, -- Last day of the month
|
||||
CURRENT_DATE::DATE AS today,
|
||||
(SELECT id FROM organizations WHERE user_id = $2 LIMIT 1) AS org_id
|
||||
),
|
||||
days AS (
|
||||
SELECT
|
||||
generate_series(
|
||||
(SELECT start_date FROM params),
|
||||
(SELECT end_date FROM params),
|
||||
'1 day'::INTERVAL
|
||||
)::DATE AS date
|
||||
),
|
||||
org_working_days AS (
|
||||
SELECT
|
||||
monday, tuesday, wednesday, thursday, friday, saturday, sunday
|
||||
FROM organization_working_days
|
||||
WHERE organization_id = (SELECT org_id FROM params)
|
||||
LIMIT 1
|
||||
),
|
||||
formatted_days AS (
|
||||
SELECT
|
||||
d.date,
|
||||
TO_CHAR(d.date, 'Dy') AS day_name,
|
||||
EXTRACT(DAY FROM d.date) AS day,
|
||||
-- Dynamically check if the day is a weekend based on the organization's settings
|
||||
CASE
|
||||
WHEN EXTRACT(DOW FROM d.date) = 0 THEN NOT (SELECT sunday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 1 THEN NOT (SELECT monday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 2 THEN NOT (SELECT tuesday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 3 THEN NOT (SELECT wednesday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 4 THEN NOT (SELECT thursday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 5 THEN NOT (SELECT friday FROM org_working_days)
|
||||
WHEN EXTRACT(DOW FROM d.date) = 6 THEN NOT (SELECT saturday FROM org_working_days)
|
||||
END AS is_weekend,
|
||||
CASE WHEN d.date = (SELECT today FROM params) THEN TRUE ELSE FALSE END AS is_today
|
||||
FROM days d
|
||||
),
|
||||
grouped_by_month AS (
|
||||
SELECT
|
||||
TO_CHAR(date, 'Mon YYYY') AS month_name,
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'day', day,
|
||||
'name', day_name,
|
||||
'isWeekend', is_weekend,
|
||||
'isToday', is_today
|
||||
) ORDER BY date
|
||||
) AS days
|
||||
FROM formatted_days
|
||||
GROUP BY month_name
|
||||
)
|
||||
SELECT jsonb_build_object(
|
||||
'date_data', jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'month', month_name,
|
||||
'weeks', '[]'::JSONB, -- Placeholder for weeks data
|
||||
'days', days
|
||||
) ORDER BY month_name
|
||||
),
|
||||
'chart_start', (SELECT start_date FROM params),
|
||||
'chart_end', (SELECT end_date FROM params)
|
||||
) AS result_json
|
||||
FROM grouped_by_month;`;
|
||||
|
||||
const results = await db.query(getDataq, [date, req.user?.owner_id]);
|
||||
const [data] = results.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data.result_json));
|
||||
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOrganizationMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const getDataq = `SELECT DISTINCT ON (users.email)
|
||||
team_members.id AS team_member_id,
|
||||
users.id AS id,
|
||||
users.name AS name,
|
||||
users.email AS email,
|
||||
'[]'::JSONB AS projects
|
||||
FROM team_members
|
||||
INNER JOIN users ON users.id = team_members.user_id
|
||||
WHERE team_members.team_id IN (
|
||||
SELECT id FROM teams
|
||||
WHERE organization_id IN (
|
||||
SELECT id FROM organizations
|
||||
WHERE user_id = $1
|
||||
LIMIT 1
|
||||
)
|
||||
)
|
||||
ORDER BY users.email ASC, users.name ASC;`;
|
||||
|
||||
const results = await db.query(getDataq, [req.user?.owner_id]);
|
||||
return res.status(200).send(new ServerResponse(true, results.rows));
|
||||
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOrganizationMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
const getDataq = `WITH project_dates AS (
|
||||
SELECT
|
||||
pm.project_id,
|
||||
MIN(pm.allocated_from) AS start_date,
|
||||
MAX(pm.allocated_to) AS end_date,
|
||||
MAX(pm.seconds_per_day) / 3600 AS hours_per_day, -- Convert max seconds per day to hours per day
|
||||
(
|
||||
-- Calculate total working days between start and end dates
|
||||
SELECT COUNT(*)
|
||||
FROM generate_series(MIN(pm.allocated_from), MAX(pm.allocated_to), '1 day'::interval) AS day
|
||||
JOIN public.organization_working_days owd ON owd.organization_id = t.organization_id
|
||||
WHERE
|
||||
(EXTRACT(ISODOW FROM day) = 1 AND owd.monday = true) OR
|
||||
(EXTRACT(ISODOW FROM day) = 2 AND owd.tuesday = true) OR
|
||||
(EXTRACT(ISODOW FROM day) = 3 AND owd.wednesday = true) OR
|
||||
(EXTRACT(ISODOW FROM day) = 4 AND owd.thursday = true) OR
|
||||
(EXTRACT(ISODOW FROM day) = 5 AND owd.friday = true) OR
|
||||
(EXTRACT(ISODOW FROM day) = 6 AND owd.saturday = true) OR
|
||||
(EXTRACT(ISODOW FROM day) = 7 AND owd.sunday = true)
|
||||
) * (MAX(pm.seconds_per_day) / 3600) AS total_hours -- Multiply by hours per day
|
||||
FROM public.project_member_allocations pm
|
||||
JOIN public.projects p ON pm.project_id = p.id
|
||||
JOIN public.teams t ON p.team_id = t.id
|
||||
GROUP BY pm.project_id, t.organization_id
|
||||
),
|
||||
projects_with_offsets AS (
|
||||
SELECT
|
||||
p.name AS project_name,
|
||||
p.id AS project_id,
|
||||
COALESCE(pd.hours_per_day, 0) AS hours_per_day, -- Default to 8 if not available in project_member_allocations
|
||||
COALESCE(pd.total_hours, 0) AS total_hours, -- Calculated total hours based on working days
|
||||
pd.start_date,
|
||||
pd.end_date,
|
||||
p.team_id,
|
||||
tm.user_id,
|
||||
-- Calculate indicator_offset dynamically: days difference from earliest project start date * 75px
|
||||
COALESCE(
|
||||
(DATE_PART('day', pd.start_date - MIN(pd.start_date) OVER ())) * 75,
|
||||
0
|
||||
) AS indicator_offset,
|
||||
-- Calculate indicator_width as the number of days * 75 pixels per day
|
||||
COALESCE((DATE_PART('day', pd.end_date - pd.start_date) + 1) * 75, 75) AS indicator_width, -- Fallback to 75 if no dates exist
|
||||
75 AS min_width -- 75px minimum width for a 1-day project
|
||||
FROM public.projects p
|
||||
LEFT JOIN project_dates pd ON p.id = pd.project_id
|
||||
JOIN public.team_members tm ON tm.team_id = p.team_id
|
||||
JOIN public.teams t ON p.team_id = t.id
|
||||
WHERE tm.user_id = $2
|
||||
AND tm.team_id = $1
|
||||
ORDER BY pd.start_date, pd.end_date -- Order by start and end date
|
||||
)
|
||||
SELECT jsonb_agg(jsonb_build_object(
|
||||
'name', project_name,
|
||||
'id', project_id,
|
||||
'hours_per_day', hours_per_day,
|
||||
'total_hours', total_hours,
|
||||
'date_union', jsonb_build_object(
|
||||
'start', start_date::DATE,
|
||||
'end', end_date::DATE
|
||||
),
|
||||
'indicator_offset', indicator_offset,
|
||||
'indicator_width', indicator_width,
|
||||
'tasks', '[]'::jsonb, -- Empty tasks array for now,
|
||||
'default_values', jsonb_build_object(
|
||||
'allocated_from', start_date::DATE,
|
||||
'allocated_to', end_date::DATE,
|
||||
'seconds_per_day', hours_per_day,
|
||||
'total_seconds', total_hours
|
||||
)
|
||||
)) AS projects
|
||||
FROM projects_with_offsets;`;
|
||||
|
||||
const results = await db.query(getDataq, [req.user?.team_id, id]);
|
||||
const [data] = results.rows;
|
||||
return res.status(200).send(new ServerResponse(true, { projects: data.projects, id }));
|
||||
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createSchedule(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const { allocated_from, allocated_to, project_id, team_member_id, seconds_per_day } = req.body;
|
||||
|
||||
const fromFormat = moment(allocated_from).format("YYYY-MM-DD");
|
||||
const toFormat = moment(allocated_to).format("YYYY-MM-DD");
|
||||
|
||||
const getDataq1 = `
|
||||
SELECT id
|
||||
FROM project_member_allocations
|
||||
WHERE project_id = $1
|
||||
AND team_member_id = $2
|
||||
AND (
|
||||
-- Case 1: The given range starts inside an existing range
|
||||
($3 BETWEEN allocated_from AND allocated_to)
|
||||
OR
|
||||
-- Case 2: The given range ends inside an existing range
|
||||
($4 BETWEEN allocated_from AND allocated_to)
|
||||
OR
|
||||
-- Case 3: The given range fully covers an existing range
|
||||
(allocated_from BETWEEN $3 AND $4 AND allocated_to BETWEEN $3 AND $4)
|
||||
OR
|
||||
-- Case 4: The existing range fully covers the given range
|
||||
(allocated_from <= $3 AND allocated_to >= $4)
|
||||
);`;
|
||||
|
||||
const results1 = await db.query(getDataq1, [project_id, team_member_id, fromFormat, toFormat]);
|
||||
|
||||
const [data] = results1.rows;
|
||||
if (data) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Allocation already exists!"));
|
||||
}
|
||||
|
||||
const getDataq = `INSERT INTO public.project_member_allocations(
|
||||
project_id, team_member_id, allocated_from, allocated_to, seconds_per_day)
|
||||
VALUES ($1, $2, $3, $4, $5);`;
|
||||
|
||||
const results = await db.query(getDataq, [project_id, team_member_id, allocated_from, allocated_to, Number(seconds_per_day) * 60 * 60]);
|
||||
return res.status(200).send(new ServerResponse(true, null, "Allocated successfully!"));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,83 @@ export default class ScheduleControllerV2 extends ScheduleTasksControllerBase {
|
||||
private static GLOBAL_START_DATE = moment().format("YYYY-MM-DD");
|
||||
private static GLOBAL_END_DATE = moment().format("YYYY-MM-DD");
|
||||
|
||||
// Migrate data
|
||||
@HandleExceptions()
|
||||
public static async migrate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const getDataq = `SELECT p.id,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT tmiv.team_member_id,
|
||||
tmiv.user_id,
|
||||
|
||||
LEAST(
|
||||
(SELECT MIN(LEAST(start_date, end_date)) AS start_date
|
||||
FROM tasks
|
||||
INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = p.id
|
||||
AND ta.team_member_id = tmiv.team_member_id),
|
||||
(SELECT MIN(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS ll_start_date
|
||||
FROM task_work_log twl
|
||||
INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
|
||||
WHERE t.project_id = p.id
|
||||
AND twl.user_id = tmiv.user_id)
|
||||
) AS lowest_date,
|
||||
|
||||
GREATEST(
|
||||
(SELECT MAX(GREATEST(start_date, end_date)) AS end_date
|
||||
FROM tasks
|
||||
INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = p.id
|
||||
AND ta.team_member_id = tmiv.team_member_id),
|
||||
(SELECT MAX(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS ll_end_date
|
||||
FROM task_work_log twl
|
||||
INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
|
||||
WHERE t.project_id = p.id
|
||||
AND twl.user_id = tmiv.user_id)
|
||||
) AS greatest_date
|
||||
|
||||
FROM project_members pm
|
||||
INNER JOIN team_member_info_view tmiv
|
||||
ON pm.team_member_id = tmiv.team_member_id
|
||||
WHERE project_id = p.id) rec) AS members
|
||||
|
||||
FROM projects p
|
||||
WHERE team_id IS NOT NULL
|
||||
AND p.id NOT IN (SELECT project_id FROM archived_projects)`;
|
||||
|
||||
const projectMembersResults = await db.query(getDataq);
|
||||
|
||||
const projectMemberData = projectMembersResults.rows;
|
||||
|
||||
const arrayToInsert = [];
|
||||
|
||||
for (const data of projectMemberData) {
|
||||
if (data.members.length) {
|
||||
for (const member of data.members) {
|
||||
|
||||
const body = {
|
||||
project_id: data.id,
|
||||
team_member_id: member.team_member_id,
|
||||
allocated_from: member.lowest_date ? member.lowest_date : null,
|
||||
allocated_to: member.greatest_date ? member.greatest_date : null
|
||||
};
|
||||
|
||||
if (body.allocated_from && body.allocated_to) arrayToInsert.push(body);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const insertArray = JSON.stringify(arrayToInsert);
|
||||
|
||||
const insertFunctionCall = `SELECT migrate_member_allocations($1)`;
|
||||
await db.query(insertFunctionCall, [insertArray]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, ""));
|
||||
}
|
||||
|
||||
|
||||
private static async getFirstLastDates(teamId: string, userId: string) {
|
||||
const q = `SELECT MIN(LEAST(allocated_from, allocated_to)) AS start_date,
|
||||
MAX(GREATEST(allocated_from, allocated_to)) AS end_date,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import {PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
|
||||
import {PriorityColorCodes, PriorityColorCodesDark, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
|
||||
import {getColor} from "../shared/utils";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
@@ -33,6 +33,7 @@ export default class SubTasksController extends WorklenzControllerBase {
|
||||
(ts.name) AS status_name,
|
||||
TRUE AS is_sub_task,
|
||||
(tsc.color_code) AS status_color,
|
||||
(tsc.color_code_dark) AS status_color_dark,
|
||||
(SELECT name FROM projects WHERE id = t.project_id) AS project_name,
|
||||
(SELECT value FROM task_priorities WHERE id = t.priority_id) AS priority_value,
|
||||
total_minutes,
|
||||
@@ -46,11 +47,12 @@ export default class SubTasksController extends WorklenzControllerBase {
|
||||
WHERE task_id = t.id
|
||||
ORDER BY name) r) AS labels,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT task_statuses.id, task_statuses.name, stsc.color_code
|
||||
FROM (SELECT task_statuses.id, task_statuses.name, stsc.color_code, stsc.color_code_dark
|
||||
FROM task_statuses
|
||||
INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id
|
||||
WHERE project_id = t.project_id
|
||||
ORDER BY task_statuses.name) rec) AS statuses
|
||||
ORDER BY task_statuses.name) rec) AS statuses,
|
||||
t.completed_at
|
||||
FROM tasks t
|
||||
INNER JOIN task_statuses ts ON ts.id = t.status_id
|
||||
INNER JOIN task_priorities tp ON tp.id = t.priority_id
|
||||
@@ -62,6 +64,7 @@ export default class SubTasksController extends WorklenzControllerBase {
|
||||
|
||||
for (const task of result.rows) {
|
||||
task.priority_color = PriorityColorCodes[task.priority_value] || null;
|
||||
task.priority_color_dark = PriorityColorCodesDark[task.priority_value] || null;
|
||||
|
||||
task.time_spent = {hours: Math.floor(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60};
|
||||
task.time_spent_string = `${task.time_spent.hours}h ${task.time_spent.minutes}m`;
|
||||
@@ -72,6 +75,7 @@ export default class SubTasksController extends WorklenzControllerBase {
|
||||
task.labels = this.createTagList(task.labels, 2);
|
||||
|
||||
task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
task.status_color_dark = task.status_color_dark + TASK_STATUS_COLOR_ALPHA;
|
||||
task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { NotificationsService } from "../services/notifications/notifications.service";
|
||||
import { log_error } from "../shared/utils";
|
||||
import { HTML_TAG_REGEXP } from "../shared/constants";
|
||||
import { humanFileSize, log_error, megabytesToBytes } from "../shared/utils";
|
||||
import { HTML_TAG_REGEXP, S3_URL } from "../shared/constants";
|
||||
import { getBaseUrl } from "../cron_jobs/helpers";
|
||||
import { ICommentEmailNotification } from "../interfaces/comment-email-notification";
|
||||
import { sendTaskComment } from "../shared/email-notifications";
|
||||
import { getRootDir, uploadBase64, getKey, getTaskAttachmentKey, createPresignedUrlWithClient } from "../shared/s3";
|
||||
import { getFreePlanSettings, getUsedStorage } from "../shared/paddle-utils";
|
||||
|
||||
interface ITaskAssignee {
|
||||
team_member_id: string;
|
||||
@@ -99,11 +101,134 @@ export default class TaskCommentsController extends WorklenzControllerBase {
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.user_id = req.user?.id;
|
||||
req.body.team_id = req.user?.team_id;
|
||||
const {mentions} = req.body;
|
||||
const { mentions, attachments, task_id } = req.body;
|
||||
const url = `${S3_URL}/${getRootDir()}`;
|
||||
|
||||
let commentContent = req.body.content;
|
||||
if (mentions.length > 0) {
|
||||
commentContent = await this.replaceContent(commentContent, mentions);
|
||||
commentContent = this.replaceContent(commentContent, mentions);
|
||||
}
|
||||
|
||||
req.body.content = commentContent;
|
||||
|
||||
const q = `SELECT create_task_comment($1) AS comment;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const response = data.comment;
|
||||
|
||||
const commentId = response.id;
|
||||
|
||||
if (attachments.length !== 0) {
|
||||
for (const attachment of attachments) {
|
||||
const q = `
|
||||
INSERT INTO task_comment_attachments (name, type, size, task_id, comment_id, team_id, project_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, type, task_id, comment_id, created_at,
|
||||
CONCAT($8::TEXT, '/', team_id, '/', project_id, '/', task_id, '/', comment_id, '/', id, '.', type) AS url;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [
|
||||
attachment.file_name,
|
||||
attachment.file_name.split(".").pop(),
|
||||
attachment.size,
|
||||
task_id,
|
||||
commentId,
|
||||
req.user?.team_id,
|
||||
attachment.project_id,
|
||||
url
|
||||
]);
|
||||
|
||||
const [data] = result.rows;
|
||||
|
||||
const s3Url = await uploadBase64(attachment.file, getTaskAttachmentKey(req.user?.team_id as string, attachment.project_id, task_id, commentId, data.id, data.type));
|
||||
|
||||
if (!data?.id || !s3Url)
|
||||
return res.status(200).send(new ServerResponse(false, null, "Attachment upload failed"));
|
||||
}
|
||||
}
|
||||
|
||||
const mentionMessage = `<b>${req.user?.name}</b> has mentioned you in a comment on <b>${response.task_name}</b> (${response.team_name})`;
|
||||
// const mentions = [...new Set(req.body.mentions || [])] as string[]; // remove duplicates
|
||||
|
||||
const assignees = await getAssignees(req.body.task_id);
|
||||
|
||||
const commentMessage = `<b>${req.user?.name}</b> added a comment on <b>${response.task_name}</b> (${response.team_name})`;
|
||||
for (const member of assignees || []) {
|
||||
if (member.user_id && member.user_id === req.user?.id) continue;
|
||||
|
||||
void NotificationsService.createNotification({
|
||||
userId: member.user_id,
|
||||
teamId: req.user?.team_id as string,
|
||||
socketId: member.socket_id,
|
||||
message: commentMessage,
|
||||
taskId: req.body.task_id,
|
||||
projectId: response.project_id
|
||||
});
|
||||
|
||||
if (member.email_notifications_enabled)
|
||||
await this.sendMail({
|
||||
message: commentMessage,
|
||||
receiverEmail: member.email,
|
||||
receiverName: member.name,
|
||||
content: req.body.content,
|
||||
commentId: response.id,
|
||||
projectId: response.project_id,
|
||||
taskId: req.body.task_id,
|
||||
teamName: response.team_name,
|
||||
projectName: response.project_name,
|
||||
taskName: response.task_name
|
||||
});
|
||||
}
|
||||
|
||||
const senderUserId = req.user?.id as string;
|
||||
|
||||
for (const mention of mentions) {
|
||||
if (mention) {
|
||||
const member = await this.getUserDataByTeamMemberId(senderUserId, mention.team_member_id, response.project_id);
|
||||
if (member) {
|
||||
|
||||
NotificationsService.sendNotification({
|
||||
team: member.team,
|
||||
receiver_socket_id: member.socket_id,
|
||||
message: mentionMessage,
|
||||
task_id: req.body.task_id,
|
||||
project_id: response.project_id,
|
||||
project: member.project,
|
||||
project_color: member.project_color,
|
||||
team_id: req.user?.team_id as string
|
||||
});
|
||||
|
||||
if (member.email_notifications_enabled)
|
||||
await this.sendMail({
|
||||
message: mentionMessage,
|
||||
receiverEmail: member.email,
|
||||
receiverName: member.user_name,
|
||||
content: req.body.content,
|
||||
commentId: response.id,
|
||||
projectId: response.project_id,
|
||||
taskId: req.body.task_id,
|
||||
teamName: response.team_name,
|
||||
projectName: response.project_name,
|
||||
taskName: response.task_name
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.comment));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.user_id = req.user?.id;
|
||||
req.body.team_id = req.user?.team_id;
|
||||
const { mentions, comment_id } = req.body;
|
||||
|
||||
let commentContent = req.body.content;
|
||||
if (mentions.length > 0) {
|
||||
commentContent = await this.replaceContent(commentContent, mentions);
|
||||
}
|
||||
|
||||
req.body.content = commentContent;
|
||||
@@ -210,46 +335,90 @@ export default class TaskCommentsController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getByTaskId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT task_comments.id,
|
||||
tc.text_content AS content,
|
||||
task_comments.user_id,
|
||||
task_comments.team_member_id,
|
||||
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) AS member_name,
|
||||
u.avatar_url,
|
||||
task_comments.created_at,
|
||||
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
|
||||
FROM (SELECT tmiv.name AS user_name,
|
||||
tmiv.email AS user_email
|
||||
FROM task_comment_mentions tcm
|
||||
LEFT JOIN team_member_info_view tmiv ON tcm.informed_by = tmiv.team_member_id
|
||||
WHERE tcm.comment_id = task_comments.id) rec) AS mentions
|
||||
FROM task_comments
|
||||
INNER JOIN task_comment_contents tc ON task_comments.id = tc.comment_id
|
||||
INNER JOIN team_members tm ON task_comments.team_member_id = tm.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE task_comments.task_id = $1
|
||||
ORDER BY task_comments.created_at DESC;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]); // task id
|
||||
const result = await TaskCommentsController.getTaskComments(req.params.id); // task id
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
private static async getTaskComments(taskId: string) {
|
||||
const url = `${S3_URL}/${getRootDir()}`;
|
||||
|
||||
const q = `SELECT task_comments.id,
|
||||
tc.text_content AS content,
|
||||
task_comments.user_id,
|
||||
task_comments.team_member_id,
|
||||
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) AS member_name,
|
||||
u.avatar_url,
|
||||
task_comments.created_at,
|
||||
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
|
||||
FROM (SELECT tmiv.name AS user_name,
|
||||
tmiv.email AS user_email
|
||||
FROM task_comment_mentions tcm
|
||||
LEFT JOIN team_member_info_view tmiv ON tcm.informed_by = tmiv.team_member_id
|
||||
WHERE tcm.comment_id = task_comments.id) rec) AS mentions,
|
||||
(SELECT JSON_BUILD_OBJECT(
|
||||
'likes',
|
||||
JSON_BUILD_OBJECT(
|
||||
'count', (SELECT COUNT(*)
|
||||
FROM task_comment_reactions tcr
|
||||
WHERE tcr.comment_id = task_comments.id
|
||||
AND reaction_type = 'like'),
|
||||
'liked_members', COALESCE(
|
||||
(SELECT JSON_AGG(tmiv.name)
|
||||
FROM task_comment_reactions tcr
|
||||
JOIN team_member_info_view tmiv ON tcr.team_member_id = tmiv.team_member_id
|
||||
WHERE tcr.comment_id = task_comments.id
|
||||
AND tcr.reaction_type = 'like'),
|
||||
'[]'::JSON
|
||||
),
|
||||
'liked_member_ids', COALESCE(
|
||||
(SELECT JSON_AGG(tmiv.team_member_id)
|
||||
FROM task_comment_reactions tcr
|
||||
JOIN team_member_info_view tmiv ON tcr.team_member_id = tmiv.team_member_id
|
||||
WHERE tcr.comment_id = task_comments.id
|
||||
AND tcr.reaction_type = 'like'),
|
||||
'[]'::JSON
|
||||
)
|
||||
)
|
||||
)) AS reactions,
|
||||
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
|
||||
FROM (SELECT id, created_at, name, size, type, (CONCAT('/', team_id, '/', project_id, '/', task_id, '/', comment_id, '/', id, '.', type)) AS url
|
||||
FROM task_comment_attachments tca
|
||||
WHERE tca.comment_id = task_comments.id) rec) AS attachments
|
||||
FROM task_comments
|
||||
LEFT JOIN task_comment_contents tc ON task_comments.id = tc.comment_id
|
||||
INNER JOIN team_members tm ON task_comments.team_member_id = tm.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE task_comments.task_id = $1
|
||||
ORDER BY task_comments.created_at;`;
|
||||
const result = await db.query(q, [taskId]); // task id
|
||||
|
||||
for (const comment of result.rows) {
|
||||
if (!comment.content) comment.content = "";
|
||||
comment.rawContent = await comment.content;
|
||||
comment.content = await comment.content.replace(/\n/g, "</br>");
|
||||
const {mentions} = comment;
|
||||
comment.edit = false;
|
||||
const { mentions } = comment;
|
||||
if (mentions.length > 0) {
|
||||
const placeHolders = comment.content.match(/{\d+}/g);
|
||||
if (placeHolders) {
|
||||
placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
|
||||
const index = parseInt(placeHolder.match(/\d+/)[0]);
|
||||
if (index >= 0 && index < comment.mentions.length) {
|
||||
comment.content = comment.content.replace(placeHolder, `<span class="mentions"> @${comment.mentions[index].user_name} </span>`);
|
||||
}
|
||||
const index = parseInt(placeHolder.match(/\d+/)[0]);
|
||||
if (index >= 0 && index < comment.mentions.length) {
|
||||
comment.rawContent = comment.rawContent.replace(placeHolder, `@${comment.mentions[index].user_name}`);
|
||||
comment.content = comment.content.replace(placeHolder, `<span class="mentions"> @${comment.mentions[index].user_name} </span>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const attachment of comment.attachments) {
|
||||
attachment.size = humanFileSize(attachment.size);
|
||||
attachment.url = url + attachment.url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
return result;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
@@ -262,4 +431,186 @@ export default class TaskCommentsController extends WorklenzControllerBase {
|
||||
const result = await db.query(q, [req.params.id, req.params.taskId, req.user?.id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteAttachmentById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE
|
||||
FROM task_comment_attachments
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
private static async checkIfAlreadyExists(commentId: string, teamMemberId: string | undefined, reaction_type: string) {
|
||||
if (!teamMemberId) return;
|
||||
try {
|
||||
const q = `SELECT EXISTS(SELECT 1 FROM task_comment_reactions WHERE comment_id = $1 AND team_member_id = $2 AND reaction_type = $3)`;
|
||||
const result = await db.query(q, [commentId, teamMemberId, reaction_type]);
|
||||
const [data] = result.rows;
|
||||
return data.exists;
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
private static async getTaskCommentData(commentId: string) {
|
||||
if (!commentId) return;
|
||||
try {
|
||||
const q = `SELECT tc.user_id,
|
||||
t.project_id,
|
||||
t.name AS task_name,
|
||||
(SELECT team_id FROM projects p WHERE p.id = t.project_id) AS team_id,
|
||||
(SELECT name FROM teams te WHERE id = (SELECT team_id FROM projects p WHERE p.id = t.project_id)) AS team_name,
|
||||
(SELECT u.socket_id FROM users u WHERE u.id = tc.user_id) AS socket_id,
|
||||
(SELECT name FROM team_member_info_view tmiv WHERE tmiv.team_member_id = tcr.team_member_id) AS reactor_name
|
||||
FROM task_comments tc
|
||||
LEFT JOIN tasks t ON t.id = tc.task_id
|
||||
LEFT JOIN task_comment_reactions tcr ON tc.id = tcr.comment_id
|
||||
WHERE tc.id = $1;`;
|
||||
const result = await db.query(q, [commentId]);
|
||||
const [data] = result.rows;
|
||||
return data;
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateReaction(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const { reaction_type, task_id } = req.query;
|
||||
|
||||
const exists = await this.checkIfAlreadyExists(id, req.user?.team_member_id, reaction_type as string);
|
||||
|
||||
if (exists) {
|
||||
const deleteQ = `DELETE FROM task_comment_reactions WHERE comment_id = $1 AND team_member_id = $2;`;
|
||||
await db.query(deleteQ, [id, req.user?.team_member_id]);
|
||||
} else {
|
||||
const q = `INSERT INTO task_comment_reactions (comment_id, user_id, team_member_id) VALUES ($1, $2, $3);`;
|
||||
await db.query(q, [id, req.user?.id, req.user?.team_member_id]);
|
||||
|
||||
const getTaskCommentData = await TaskCommentsController.getTaskCommentData(id);
|
||||
const commentMessage = `<b>${getTaskCommentData.reactor_name}</b> liked your comment on <b>${getTaskCommentData.task_name}</b> (${getTaskCommentData.team_name})`;
|
||||
|
||||
if (getTaskCommentData && getTaskCommentData.user_id !== req.user?.id) {
|
||||
void NotificationsService.createNotification({
|
||||
userId: getTaskCommentData.user_id,
|
||||
teamId: req.user?.team_id as string,
|
||||
socketId: getTaskCommentData.socket_id,
|
||||
message: commentMessage,
|
||||
taskId: req.body.task_id,
|
||||
projectId: getTaskCommentData.project_id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await TaskCommentsController.getTaskComments(task_id as string);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createAttachment(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.user_id = req.user?.id;
|
||||
req.body.team_id = req.user?.team_id;
|
||||
const { attachments, task_id } = req.body;
|
||||
|
||||
const q = `INSERT INTO task_comments (user_id, team_member_id, task_id)
|
||||
VALUES ($1, (SELECT id
|
||||
FROM team_members
|
||||
WHERE user_id = $1
|
||||
AND team_id = $2::UUID), $3)
|
||||
RETURNING id;`;
|
||||
const result = await db.query(q, [req.user?.id, req.user?.team_id, task_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const commentId = data.id;
|
||||
|
||||
const url = `${S3_URL}/${getRootDir()}`;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
if (req.user?.subscription_status === "free" && req.user?.owner_id) {
|
||||
const limits = await getFreePlanSettings();
|
||||
|
||||
const usedStorage = await getUsedStorage(req.user?.owner_id);
|
||||
if ((parseInt(usedStorage) + attachment.size) > 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 q = `
|
||||
INSERT INTO task_comment_attachments (name, type, size, task_id, comment_id, team_id, project_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, type, task_id, comment_id, created_at,
|
||||
CONCAT($8::TEXT, '/', team_id, '/', project_id, '/', task_id, '/', comment_id, '/', id, '.', type) AS url;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [
|
||||
attachment.file_name,
|
||||
attachment.size,
|
||||
attachment.file_name.split(".").pop(),
|
||||
task_id,
|
||||
commentId,
|
||||
req.user?.team_id,
|
||||
attachment.project_id,
|
||||
url
|
||||
]);
|
||||
|
||||
const [data] = result.rows;
|
||||
|
||||
const s3Url = await uploadBase64(attachment.file, getTaskAttachmentKey(req.user?.team_id as string, attachment.project_id, task_id, commentId, data.id, data.type));
|
||||
|
||||
if (!data?.id || !s3Url)
|
||||
return res.status(200).send(new ServerResponse(false, null, "Attachment upload failed"));
|
||||
}
|
||||
|
||||
const assignees = await getAssignees(task_id);
|
||||
|
||||
const commentMessage = `<b>${req.user?.name}</b> added a new attachment as a comment on <b>${commentId.task_name}</b> (${commentId.team_name})`;
|
||||
|
||||
for (const member of assignees || []) {
|
||||
if (member.user_id && member.user_id === req.user?.id) continue;
|
||||
|
||||
void NotificationsService.createNotification({
|
||||
userId: member.user_id,
|
||||
teamId: req.user?.team_id as string,
|
||||
socketId: member.socket_id,
|
||||
message: commentMessage,
|
||||
taskId: task_id,
|
||||
projectId: commentId.project_id
|
||||
});
|
||||
|
||||
if (member.email_notifications_enabled)
|
||||
await this.sendMail({
|
||||
message: commentMessage,
|
||||
receiverEmail: member.email,
|
||||
receiverName: member.name,
|
||||
content: req.body.content,
|
||||
commentId: commentId.id,
|
||||
projectId: commentId.project_id,
|
||||
taskId: task_id,
|
||||
teamName: commentId.team_name,
|
||||
projectName: commentId.project_name,
|
||||
taskName: commentId.task_name
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async download(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', task_id, '/', comment_id, '/', id, '.', type) AS key
|
||||
FROM task_comment_attachments
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.query.id, getRootDir()]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data?.key) {
|
||||
const url = await createPresignedUrlWithClient(data.key, req.query.file as string);
|
||||
return res.status(200).send(new ServerResponse(true, url));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class TaskdependenciesController extends WorklenzControllerBase {
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"DEPENDENCY_EXISTS": `Task dependency already exists.`
|
||||
}
|
||||
})
|
||||
public static async saveTaskDependency(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {task_id, related_task_id, dependency_type } = req.body;
|
||||
const q = `SELECT insert_task_dependency($1, $2, $3);`;
|
||||
const result = await db.query(q, [task_id, related_task_id, dependency_type]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTaskDependencies(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
|
||||
const q = `SELECT
|
||||
td.id,
|
||||
t2.name AS task_name,
|
||||
td.dependency_type,
|
||||
CONCAT(p.key, '-', t2.task_no) AS task_key
|
||||
FROM
|
||||
task_dependencies td
|
||||
LEFT JOIN
|
||||
tasks t ON td.task_id = t.id
|
||||
LEFT JOIN
|
||||
tasks t2 ON td.related_task_id = t2.id
|
||||
LEFT JOIN
|
||||
projects p ON t.project_id = p.id
|
||||
WHERE
|
||||
td.task_id = $1;`;
|
||||
const result = await db.query(q, [id]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
const q = `DELETE FROM task_dependencies WHERE id = $1;`;
|
||||
const result = await db.query(q, [id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,9 @@ export default class TaskListColumnsController extends WorklenzControllerBase {
|
||||
const q = `UPDATE project_task_list_cols
|
||||
SET pinned = $3
|
||||
WHERE project_id = $1
|
||||
AND key = $2;`;
|
||||
AND key = $2 RETURNING *;`;
|
||||
const result = await db.query(q, [req.params.id, req.body.key, !!req.body.pinned]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,17 @@ import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {PriorityColorCodes} from "../shared/constants";
|
||||
import {PriorityColorCodes, PriorityColorCodesDark} from "../shared/constants";
|
||||
|
||||
export default class TaskPrioritiesController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async get(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, value From task_priorities ORDER BY value;`;
|
||||
const result = await db.query(q, []);
|
||||
for (const item of result.rows)
|
||||
for (const item of result.rows) {
|
||||
item.color_code = PriorityColorCodes[item.value] || PriorityColorCodes["0"];
|
||||
item.color_code_dark = PriorityColorCodesDark[item.value] || PriorityColorCodesDark["0"];
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
|
||||
108
worklenz-backend/src/controllers/task-recurring-controller.ts
Normal file
108
worklenz-backend/src/controllers/task-recurring-controller.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import db from "../config/db";
|
||||
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import { calculateNextEndDate, log_error } from "../shared/utils";
|
||||
|
||||
export default class TaskRecurringController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const q = `SELECT id,
|
||||
schedule_type,
|
||||
days_of_week,
|
||||
date_of_month,
|
||||
day_of_month,
|
||||
week_of_month,
|
||||
interval_days,
|
||||
interval_weeks,
|
||||
interval_months,
|
||||
created_at
|
||||
FROM task_recurring_schedules WHERE id = $1;`;
|
||||
const result = await db.query(q, [id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
private static async insertTaskRecurringTemplate(taskId: string, scheduleId: string) {
|
||||
const q = `SELECT create_recurring_task_template($1, $2);`;
|
||||
await db.query(q, [taskId, scheduleId]);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createTaskSchedule(taskId: string) {
|
||||
const q = `INSERT INTO task_recurring_schedules (schedule_type) VALUES ('daily') RETURNING id, schedule_type;`;
|
||||
const result = await db.query(q, []);
|
||||
const [data] = result.rows;
|
||||
|
||||
const updateQ = `UPDATE tasks SET schedule_id = $1 WHERE id = $2;`;
|
||||
await db.query(updateQ, [data.id, taskId]);
|
||||
|
||||
await TaskRecurringController.insertTaskRecurringTemplate(taskId, data.id);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async removeTaskSchedule(scheduleId: string) {
|
||||
const deleteQ = `DELETE FROM task_recurring_schedules WHERE id = $1;`;
|
||||
await db.query(deleteQ, [scheduleId]);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateSchedule(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const { schedule_type, days_of_week, day_of_month, week_of_month, interval_days, interval_weeks, interval_months, date_of_month } = req.body;
|
||||
|
||||
const deleteQ = `UPDATE task_recurring_schedules
|
||||
SET schedule_type = $1,
|
||||
days_of_week = $2,
|
||||
date_of_month = $3,
|
||||
day_of_month = $4,
|
||||
week_of_month = $5,
|
||||
interval_days = $6,
|
||||
interval_weeks = $7,
|
||||
interval_months = $8
|
||||
WHERE id = $9;`;
|
||||
await db.query(deleteQ, [schedule_type, days_of_week, date_of_month, day_of_month, week_of_month, interval_days, interval_weeks, interval_months, id]);
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
}
|
||||
|
||||
// Function to create the next task in the recurring schedule
|
||||
private static async createNextRecurringTask(scheduleId: string, lastTask: any, taskTemplate: any) {
|
||||
try {
|
||||
const q = "SELECT * FROM task_recurring_schedules WHERE id = $1";
|
||||
const { rows: schedules } = await db.query(q, [scheduleId]);
|
||||
|
||||
if (schedules.length === 0) {
|
||||
log_error("No schedule found");
|
||||
return;
|
||||
}
|
||||
|
||||
const [schedule] = schedules;
|
||||
|
||||
// Define the next start date based on the schedule
|
||||
const nextStartDate = calculateNextEndDate(schedule, lastTask.start_date);
|
||||
|
||||
const result = await db.query(
|
||||
`INSERT INTO tasks (name, start_date, end_date, priority_id, project_id, reporter_id, description, total_minutes, status_id, schedule_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id;`,
|
||||
[
|
||||
taskTemplate.name, nextStartDate, null, taskTemplate.priority_id,
|
||||
lastTask.project_id, lastTask.reporter_id, taskTemplate.description,
|
||||
0, taskTemplate.status_id, scheduleId
|
||||
]
|
||||
);
|
||||
const [data] = result.rows;
|
||||
|
||||
log_error(`Next task created with id: ${data.id}`);
|
||||
|
||||
} catch (error) {
|
||||
log_error("Error creating next recurring task:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export default class TaskStatusesController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getCategories(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, color_code, description
|
||||
const q = `SELECT id, name, color_code, color_code_dark, description
|
||||
FROM sys_task_status_categories
|
||||
ORDER BY index;`;
|
||||
const result = await db.query(q, []);
|
||||
@@ -73,7 +73,7 @@ export default class TaskStatusesController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT task_statuses.id, task_statuses.name, stsc.color_code
|
||||
SELECT task_statuses.id, task_statuses.name, stsc.color_code, stsc.color_code_dark
|
||||
FROM task_statuses
|
||||
INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id
|
||||
WHERE task_statuses.id = $1
|
||||
@@ -113,7 +113,7 @@ export default class TaskStatusesController extends WorklenzControllerBase {
|
||||
category_id = COALESCE($4, (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE))
|
||||
WHERE id = $1
|
||||
AND project_id = $3
|
||||
RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id);
|
||||
RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id), (SELECT color_code_dark FROM sys_task_status_categories WHERE id = task_statuses.category_id);
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.body.name, req.body.project_id, req.body.category_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
@@ -234,4 +234,25 @@ export default class TaskWorklogController extends WorklenzControllerBase {
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getAllRunningTimers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT
|
||||
tt.task_id,
|
||||
tt.start_time,
|
||||
t1.name AS task_name,
|
||||
pr.id AS project_id,
|
||||
pr.name AS project_name,
|
||||
t1.parent_task_id,
|
||||
t2.name AS parent_task_name
|
||||
FROM task_timers tt
|
||||
LEFT JOIN public.tasks t1 ON tt.task_id = t1.id
|
||||
LEFT JOIN public.tasks t2 ON t1.parent_task_id = t2.id -- Optimized join for parent task name
|
||||
INNER JOIN projects pr ON t1.project_id = pr.id -- INNER JOIN ensures project-team match
|
||||
WHERE tt.user_id = $1
|
||||
AND pr.team_id = $2;`;
|
||||
const params = [req.user?.id, req.user?.team_id];
|
||||
const result = await db.query(q, params);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ export default class TasksControllerBase extends WorklenzControllerBase {
|
||||
if (task.timer_start_time)
|
||||
task.timer_start_time = moment(task.timer_start_time).valueOf();
|
||||
|
||||
const totalCompleted = +task.completed_sub_tasks + +task.parent_task_completed;
|
||||
const totalTasks = +task.sub_tasks_count + 1; // +1 for parent
|
||||
const totalCompleted = (+task.completed_sub_tasks + +task.parent_task_completed) || 0;
|
||||
const totalTasks = +task.sub_tasks_count || 0; // if needed add +1 for parent
|
||||
task.complete_ratio = TasksControllerBase.calculateTaskCompleteRatio(totalCompleted, totalTasks);
|
||||
task.completed_count = totalCompleted;
|
||||
task.total_tasks_count = totalTasks;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {ParsedQs} from "qs";
|
||||
import { ParsedQs } from "qs";
|
||||
|
||||
import db from "../config/db";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import {TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED} from "../shared/constants";
|
||||
import {getColor} from "../shared/utils";
|
||||
import TasksControllerBase, {GroupBy, ITaskGroup} from "./tasks-controller-base";
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import { TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED } from "../shared/constants";
|
||||
import { getColor, log_error } from "../shared/utils";
|
||||
import TasksControllerBase, { GroupBy, ITaskGroup } from "./tasks-controller-base";
|
||||
|
||||
export class TaskListGroup implements ITaskGroup {
|
||||
name: string;
|
||||
category_id: string | null;
|
||||
color_code: string;
|
||||
color_code_dark: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
todo_progress: number;
|
||||
@@ -26,6 +27,7 @@ export class TaskListGroup implements ITaskGroup {
|
||||
this.start_date = group.start_date || null;
|
||||
this.end_date = group.end_date || null;
|
||||
this.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA;
|
||||
this.color_code_dark = group.color_code_dark;
|
||||
this.todo_progress = 0;
|
||||
this.doing_progress = 0;
|
||||
this.done_progress = 0;
|
||||
@@ -104,7 +106,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
|
||||
private static getQuery(userId: string, options: ParsedQs) {
|
||||
const searchField = options.search ? "t.name" : "sort_order";
|
||||
const {searchQuery, sortField} = TasksControllerV2.toPaginationOptions(options, searchField);
|
||||
const { searchQuery, sortField } = TasksControllerV2.toPaginationOptions(options, searchField);
|
||||
|
||||
const isSubTasks = !!options.parent_task;
|
||||
|
||||
@@ -124,6 +126,33 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
const filterByAssignee = TasksControllerV2.getFilterByAssignee(options.filterBy as string);
|
||||
// Returns statuses of each task as a json array if filterBy === "member"
|
||||
const statusesQuery = TasksControllerV2.getStatusesQuery(options.filterBy as string);
|
||||
|
||||
// Custom columns data query
|
||||
const customColumnsQuery = options.customColumns
|
||||
? `, (SELECT COALESCE(
|
||||
jsonb_object_agg(
|
||||
custom_cols.key,
|
||||
custom_cols.value
|
||||
),
|
||||
'{}'::JSONB
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
cc.key,
|
||||
CASE
|
||||
WHEN ccv.text_value IS NOT NULL THEN to_jsonb(ccv.text_value)
|
||||
WHEN ccv.number_value IS NOT NULL THEN to_jsonb(ccv.number_value)
|
||||
WHEN ccv.boolean_value IS NOT NULL THEN to_jsonb(ccv.boolean_value)
|
||||
WHEN ccv.date_value IS NOT NULL THEN to_jsonb(ccv.date_value)
|
||||
WHEN ccv.json_value IS NOT NULL THEN ccv.json_value
|
||||
ELSE NULL::JSONB
|
||||
END AS value
|
||||
FROM cc_column_values ccv
|
||||
JOIN cc_custom_columns cc ON ccv.column_id = cc.id
|
||||
WHERE ccv.task_id = t.id
|
||||
) AS custom_cols
|
||||
WHERE custom_cols.value IS NOT NULL) AS custom_column_values`
|
||||
: "";
|
||||
|
||||
const archivedFilter = options.archived === "true" ? "archived IS TRUE" : "archived IS FALSE";
|
||||
|
||||
@@ -173,7 +202,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_color_code,
|
||||
|
||||
(EXISTS(SELECT 1 FROM task_subscribers WHERE task_id = t.id)) AS has_subscribers,
|
||||
|
||||
(EXISTS(SELECT 1 FROM task_dependencies td WHERE td.task_id = t.id)) AS has_dependencies,
|
||||
(SELECT start_time
|
||||
FROM task_timers
|
||||
WHERE task_id = t.id
|
||||
@@ -183,6 +212,10 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
|
||||
|
||||
(SELECT color_code_dark
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color_dark,
|
||||
|
||||
(SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
|
||||
FROM (SELECT is_done, is_doing, is_todo
|
||||
FROM sys_task_status_categories
|
||||
@@ -209,7 +242,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
(SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
|
||||
FROM task_labels
|
||||
WHERE task_id = t.id) r) AS labels,
|
||||
|
||||
(SELECT is_completed(status_id, project_id)) AS is_complete,
|
||||
(SELECT name FROM users WHERE id = t.reporter_id) AS reporter,
|
||||
(SELECT id FROM task_priorities WHERE id = t.priority_id) AS priority,
|
||||
(SELECT value FROM task_priorities WHERE id = t.priority_id) AS priority_value,
|
||||
@@ -219,7 +252,9 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
updated_at,
|
||||
completed_at,
|
||||
start_date,
|
||||
END_DATE ${statusesQuery}
|
||||
billable,
|
||||
schedule_id,
|
||||
END_DATE ${customColumnsQuery} ${statusesQuery}
|
||||
FROM tasks t
|
||||
WHERE ${filters} ${searchQuery}
|
||||
ORDER BY ${sortFields}
|
||||
@@ -235,6 +270,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
SELECT id,
|
||||
name,
|
||||
(SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id),
|
||||
(SELECT color_code_dark FROM sys_task_status_categories WHERE id = task_statuses.category_id),
|
||||
category_id
|
||||
FROM task_statuses
|
||||
WHERE project_id = $1
|
||||
@@ -243,7 +279,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
params = [projectId];
|
||||
break;
|
||||
case GroupBy.PRIORITY:
|
||||
q = `SELECT id, name, color_code
|
||||
q = `SELECT id, name, color_code, color_code_dark
|
||||
FROM task_priorities
|
||||
ORDER BY value DESC;`;
|
||||
break;
|
||||
@@ -261,7 +297,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
break;
|
||||
case GroupBy.PHASE:
|
||||
q = `
|
||||
SELECT id, name, color_code, start_date, end_date, sort_index
|
||||
SELECT id, name, color_code, color_code AS color_code_dark, start_date, end_date, sort_index
|
||||
FROM project_phases
|
||||
WHERE project_id = $1
|
||||
ORDER BY sort_index DESC;
|
||||
@@ -281,6 +317,9 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
public static async getList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const isSubTasks = !!req.query.parent_task;
|
||||
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
||||
|
||||
// Add customColumns flag to query params
|
||||
req.query.customColumns = "true";
|
||||
|
||||
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
|
||||
@@ -356,6 +395,10 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const isSubTasks = !!req.query.parent_task;
|
||||
|
||||
// Add customColumns flag to query params
|
||||
req.query.customColumns = "true";
|
||||
|
||||
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
|
||||
const result = await db.query(q, params);
|
||||
@@ -393,7 +436,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getNewKanbanTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
const { id } = req.params;
|
||||
const result = await db.query("SELECT get_single_task($1) AS task;", [id]);
|
||||
const [data] = result.rows;
|
||||
const task = TasksControllerV2.updateTaskViewModel(data.task);
|
||||
@@ -474,9 +517,211 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
|
||||
}
|
||||
|
||||
public static async getTasksByName(searchString: string, projectId: string, taskId: string) {
|
||||
const q = `SELECT id AS value ,
|
||||
name AS label,
|
||||
CONCAT((SELECT key FROM projects WHERE id = t.project_id), '-', task_no) AS task_key
|
||||
FROM tasks t
|
||||
WHERE t.name ILIKE '%${searchString}%'
|
||||
AND t.project_id = $1 AND t.id != $2
|
||||
LIMIT 15;`;
|
||||
const result = await db.query(q, [projectId, taskId]);
|
||||
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getSubscribers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const subscribers = await this.getTaskSubscribers(req.params.id);
|
||||
return res.status(200).send(new ServerResponse(true, subscribers));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async searchTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { projectId, taskId, searchQuery } = req.query;
|
||||
const tasks = await this.getTasksByName(searchQuery as string, projectId as string, taskId as string);
|
||||
return res.status(200).send(new ServerResponse(true, tasks));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTaskDependencyStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { statusId, taskId } = req.query;
|
||||
const canContinue = await TasksControllerV2.checkForCompletedDependencies(taskId as string, statusId as string);
|
||||
return res.status(200).send(new ServerResponse(true, { can_continue: canContinue }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async checkForCompletedDependencies(taskId: string, nextStatusId: string): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
-- Check if the status id is not in the "done" category
|
||||
SELECT 1
|
||||
FROM task_statuses ts
|
||||
WHERE ts.id = $2
|
||||
AND ts.project_id = (SELECT project_id FROM tasks WHERE id = $1)
|
||||
AND ts.category_id IN (
|
||||
SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE
|
||||
)
|
||||
) THEN TRUE -- If status is not in the "done" category, continue immediately (TRUE)
|
||||
|
||||
WHEN EXISTS (
|
||||
-- Check if any dependent tasks are not completed
|
||||
SELECT 1
|
||||
FROM task_dependencies td
|
||||
LEFT JOIN public.tasks t ON t.id = td.related_task_id
|
||||
WHERE td.task_id = $1
|
||||
AND t.status_id NOT IN (
|
||||
SELECT id
|
||||
FROM task_statuses ts
|
||||
WHERE t.project_id = ts.project_id
|
||||
AND ts.category_id IN (
|
||||
SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE
|
||||
)
|
||||
)
|
||||
) THEN FALSE -- If there are incomplete dependent tasks, do not continue (FALSE)
|
||||
|
||||
ELSE TRUE -- Continue if no other conditions block the process
|
||||
END AS can_continue;`;
|
||||
const result = await db.query(q, [taskId, nextStatusId]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return data.can_continue;
|
||||
}
|
||||
|
||||
public static async getTaskStatusColor(status_id: string) {
|
||||
try {
|
||||
const q = `SELECT color_code, color_code_dark
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = $1)`;
|
||||
const result = await db.query(q, [status_id]);
|
||||
const [data] = result.rows;
|
||||
return data;
|
||||
} catch (e) {
|
||||
log_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async assignLabelsToTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const { labels }: { labels: string[] } = req.body;
|
||||
|
||||
labels.forEach(async (label: string) => {
|
||||
const q = `SELECT add_or_remove_task_label($1, $2) AS labels;`;
|
||||
await db.query(q, [id, label]);
|
||||
});
|
||||
return res.status(200).send(new ServerResponse(true, null, "Labels assigned successfully"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a custom column value for a task
|
||||
* @param req The request object
|
||||
* @param res The response object
|
||||
*/
|
||||
@HandleExceptions()
|
||||
public static async updateCustomColumnValue(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const { taskId } = req.params;
|
||||
const { column_key, value, project_id } = req.body;
|
||||
|
||||
if (!taskId || !column_key || value === undefined || !project_id) {
|
||||
return res.status(400).send(new ServerResponse(false, "Missing required parameters"));
|
||||
}
|
||||
|
||||
// Get column information
|
||||
const columnQuery = `
|
||||
SELECT id, field_type
|
||||
FROM cc_custom_columns
|
||||
WHERE project_id = $1 AND key = $2
|
||||
`;
|
||||
const columnResult = await db.query(columnQuery, [project_id, column_key]);
|
||||
|
||||
if (columnResult.rowCount === 0) {
|
||||
return res.status(404).send(new ServerResponse(false, "Custom column not found"));
|
||||
}
|
||||
|
||||
const column = columnResult.rows[0];
|
||||
const columnId = column.id;
|
||||
const fieldType = column.field_type;
|
||||
|
||||
// Determine which value field to use based on the field_type
|
||||
let textValue = null;
|
||||
let numberValue = null;
|
||||
let dateValue = null;
|
||||
let booleanValue = null;
|
||||
let jsonValue = null;
|
||||
|
||||
switch (fieldType) {
|
||||
case "number":
|
||||
numberValue = parseFloat(String(value));
|
||||
break;
|
||||
case "date":
|
||||
dateValue = new Date(String(value));
|
||||
break;
|
||||
case "checkbox":
|
||||
booleanValue = Boolean(value);
|
||||
break;
|
||||
case "people":
|
||||
jsonValue = JSON.stringify(Array.isArray(value) ? value : [value]);
|
||||
break;
|
||||
default:
|
||||
textValue = String(value);
|
||||
}
|
||||
|
||||
// Check if a value already exists
|
||||
const existingValueQuery = `
|
||||
SELECT id
|
||||
FROM cc_column_values
|
||||
WHERE task_id = $1 AND column_id = $2
|
||||
`;
|
||||
const existingValueResult = await db.query(existingValueQuery, [taskId, columnId]);
|
||||
|
||||
if (existingValueResult.rowCount && existingValueResult.rowCount > 0) {
|
||||
// Update existing value
|
||||
const updateQuery = `
|
||||
UPDATE cc_column_values
|
||||
SET text_value = $1,
|
||||
number_value = $2,
|
||||
date_value = $3,
|
||||
boolean_value = $4,
|
||||
json_value = $5,
|
||||
updated_at = NOW()
|
||||
WHERE task_id = $6 AND column_id = $7
|
||||
`;
|
||||
await db.query(updateQuery, [
|
||||
textValue,
|
||||
numberValue,
|
||||
dateValue,
|
||||
booleanValue,
|
||||
jsonValue,
|
||||
taskId,
|
||||
columnId
|
||||
]);
|
||||
} else {
|
||||
// Insert new value
|
||||
const insertQuery = `
|
||||
INSERT INTO cc_column_values
|
||||
(task_id, column_id, text_value, number_value, date_value, boolean_value, json_value, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
||||
`;
|
||||
await db.query(insertQuery, [
|
||||
taskId,
|
||||
columnId,
|
||||
textValue,
|
||||
numberValue,
|
||||
dateValue,
|
||||
booleanValue,
|
||||
jsonValue
|
||||
]);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {
|
||||
task_id: taskId,
|
||||
column_key,
|
||||
value
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
import db from "../config/db";
|
||||
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import { TASK_STATUS_COLOR_ALPHA } from "../shared/constants";
|
||||
import { S3_URL, TASK_STATUS_COLOR_ALPHA } from "../shared/constants";
|
||||
import { getDates, getMinMaxOfTaskDates, getMonthRange, getWeekRange } from "../shared/tasks-controller-utils";
|
||||
import { getColor, getRandomColorCode, log_error, toMinutes } from "../shared/utils";
|
||||
import { getColor, getRandomColorCode, humanFileSize, log_error, toMinutes } from "../shared/utils";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { NotificationsService } from "../services/notifications/notifications.service";
|
||||
@@ -18,9 +18,9 @@ import TasksControllerV2 from "./tasks-controller-v2";
|
||||
import { IO } from "../shared/io";
|
||||
import { SocketEvents } from "../socket.io/events";
|
||||
import TasksControllerBase from "./tasks-controller-base";
|
||||
import { insertToActivityLogs, logStatusChange } from "../services/activity-logs/activity-logs.service";
|
||||
import { forEach } from "lodash";
|
||||
import { insertToActivityLogs } from "../services/activity-logs/activity-logs.service";
|
||||
import { IActivityLog } from "../services/activity-logs/interfaces";
|
||||
import { getKey, getRootDir, uploadBase64 } from "../shared/s3";
|
||||
|
||||
export default class TasksController extends TasksControllerBase {
|
||||
private static notifyProjectUpdates(socketId: string, projectId: string) {
|
||||
@@ -29,14 +29,54 @@ export default class TasksController extends TasksControllerBase {
|
||||
.emit(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString());
|
||||
}
|
||||
|
||||
public static async uploadAttachment(attachments: any, teamId: string, userId: string) {
|
||||
try {
|
||||
const promises = attachments.map(async (attachment: any) => {
|
||||
const { file, file_name, project_id, size } = attachment;
|
||||
const type = file_name.split(".").pop();
|
||||
|
||||
const q = `
|
||||
INSERT INTO task_attachments (name, task_id, team_id, project_id, uploaded_by, size, type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, size, type, created_at, CONCAT($8::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS url;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [
|
||||
file_name,
|
||||
null,
|
||||
teamId,
|
||||
project_id,
|
||||
userId,
|
||||
size,
|
||||
type,
|
||||
`${S3_URL}/${getRootDir()}`
|
||||
]);
|
||||
|
||||
const [data] = result.rows;
|
||||
await uploadBase64(file, getKey(teamId, project_id, data.id, data.type));
|
||||
return data.id;
|
||||
});
|
||||
|
||||
const attachmentIds = await Promise.all(promises);
|
||||
return attachmentIds;
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const userId = req.user?.id as string;
|
||||
const teamId = req.user?.team_id as string;
|
||||
|
||||
if (req.body.attachments_raw) {
|
||||
req.body.attachments = await this.uploadAttachment(req.body.attachments_raw, teamId, userId);
|
||||
}
|
||||
|
||||
const q = `SELECT create_task($1) AS task;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const userId = req.user?.id as string;
|
||||
|
||||
for (const member of data?.task.assignees || []) {
|
||||
NotificationsService.createTaskUpdate(
|
||||
"ASSIGN",
|
||||
@@ -468,7 +508,7 @@ export default class TasksController extends TasksControllerBase {
|
||||
|
||||
TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
return res.status(200).send(new ServerResponse(true, { failed_tasks: data.task }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
|
||||
@@ -13,7 +13,9 @@ import { SocketEvents } from "../socket.io/events";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { formatDuration, getColor } from "../shared/utils";
|
||||
import { TEAM_MEMBER_TREE_MAP_COLOR_ALPHA } from "../shared/constants";
|
||||
import { statusExclude, TEAM_MEMBER_TREE_MAP_COLOR_ALPHA } from "../shared/constants";
|
||||
import { checkTeamSubscriptionStatus } from "../shared/paddle-utils";
|
||||
import { updateUsers } from "../shared/paddle-requests";
|
||||
import { NotificationsService } from "../services/notifications/notifications.service";
|
||||
|
||||
export default class TeamMembersController extends WorklenzControllerBase {
|
||||
@@ -80,6 +82,98 @@ export default class TeamMembersController extends WorklenzControllerBase {
|
||||
return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the subscription status of the team.
|
||||
* @type {Object} subscriptionData - Object containing subscription information
|
||||
*/
|
||||
const subscriptionData = await checkTeamSubscriptionStatus(req.user?.team_id);
|
||||
|
||||
let incrementBy = 0;
|
||||
|
||||
// Handle self-hosted subscriptions differently
|
||||
if (subscriptionData.subscription_type === 'SELF_HOSTED') {
|
||||
// Check if users exist and add them if they don't
|
||||
await Promise.all(req.body.emails.map(async (email: string) => {
|
||||
const trimmedEmail = email.trim();
|
||||
const userExists = await this.checkIfUserAlreadyExists(req.user?.owner_id as string, trimmedEmail);
|
||||
if (!userExists) {
|
||||
incrementBy = incrementBy + 1;
|
||||
}
|
||||
}));
|
||||
|
||||
// Create or invite new members
|
||||
const newMembers = await this.createOrInviteMembers(req.body, req.user);
|
||||
return res.status(200).send(new ServerResponse(true, newMembers, `Your teammates will get an email that gives them access to your team.`).withTitle("Invitations sent"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through each email in the request body and checks if the user already exists.
|
||||
* If the user doesn't exist, increments the counter.
|
||||
* @param {string} email - Email address to check
|
||||
*/
|
||||
await Promise.all(req.body.emails.map(async (email: string) => {
|
||||
const trimmedEmail = email.trim();
|
||||
|
||||
const userExists = await this.checkIfUserAlreadyExists(req.user?.owner_id as string, trimmedEmail);
|
||||
const isUserActive = await this.checkIfUserActiveInOtherTeams(req.user?.owner_id as string, trimmedEmail);
|
||||
|
||||
if (!userExists || !isUserActive) {
|
||||
incrementBy = incrementBy + 1;
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* Checks various conditions to determine if the maximum number of lifetime users is exceeded.
|
||||
* Sends a response if the limit is reached.
|
||||
*/
|
||||
if (
|
||||
incrementBy > 0
|
||||
&& subscriptionData.is_ltd
|
||||
&& subscriptionData.current_count
|
||||
&& ((parseInt(subscriptionData.current_count) + req.body.emails.length) > parseInt(subscriptionData.ltd_users))) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Cannot exceed the maximum number of life time users."));
|
||||
}
|
||||
|
||||
if (
|
||||
subscriptionData.is_ltd
|
||||
&& subscriptionData.current_count
|
||||
&& ((parseInt(subscriptionData.current_count) + incrementBy) > parseInt(subscriptionData.ltd_users))) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Cannot exceed the maximum number of life time users."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks subscription details and updates the user count if applicable.
|
||||
* Sends a response if there is an issue with the subscription.
|
||||
*/
|
||||
// if (!subscriptionData.is_credit && !subscriptionData.is_custom && subscriptionData.subscription_status === "active") {
|
||||
// const response = await updateUsers(subscriptionData.subscription_id, (subscriptionData.quantity + incrementBy));
|
||||
|
||||
// if (!response.body.subscription_id) {
|
||||
// return res.status(200).send(new ServerResponse(false, null, response.message || "Please check your subscription."));
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!subscriptionData.is_credit && !subscriptionData.is_custom && subscriptionData.subscription_status === "active") {
|
||||
const updatedCount = parseInt(subscriptionData.current_count) + incrementBy;
|
||||
const requiredSeats = updatedCount - subscriptionData.quantity;
|
||||
if (updatedCount > subscriptionData.quantity) {
|
||||
const obj = {
|
||||
seats_enough: false,
|
||||
required_count: requiredSeats,
|
||||
current_seat_amount: subscriptionData.quantity
|
||||
};
|
||||
return res.status(200).send(new ServerResponse(false, obj, null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the subscription status is in the exclusion list.
|
||||
* Sends a response if the status is excluded.
|
||||
*/
|
||||
if (statusExclude.includes(subscriptionData.subscription_status)) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Unable to add user! Please check your subscription status."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or invites new members based on the request body and user information.
|
||||
* Sends a response with the result.
|
||||
@@ -93,12 +187,24 @@ export default class TeamMembersController extends WorklenzControllerBase {
|
||||
req.query.field = ["is_owner", "active", "u.name", "u.email"];
|
||||
req.query.order = "descend";
|
||||
|
||||
// Helper function to check for encoded components
|
||||
function containsEncodedComponents(x: string) {
|
||||
return decodeURI(x) !== decodeURIComponent(x);
|
||||
}
|
||||
|
||||
// Decode search parameter if it contains encoded components
|
||||
if (req.query.search && typeof req.query.search === 'string') {
|
||||
if (containsEncodedComponents(req.query.search)) {
|
||||
req.query.search = decodeURIComponent(req.query.search);
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
searchQuery,
|
||||
sortField,
|
||||
sortOrder,
|
||||
size,
|
||||
offset
|
||||
searchQuery,
|
||||
sortField,
|
||||
sortOrder,
|
||||
size,
|
||||
offset
|
||||
} = this.toPaginationOptions(req.query, ["u.name", "u.email"], true);
|
||||
|
||||
const paginate = req.query.all === "false" ? `LIMIT ${size} OFFSET ${offset}` : "";
|
||||
@@ -126,7 +232,7 @@ export default class TeamMembersController extends WorklenzControllerBase {
|
||||
ELSE FALSE END) AS is_owner,
|
||||
(SELECT email
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = team_members.id),
|
||||
WHERE team_member_info_view.team_member_id = team_members.id) AS email,
|
||||
EXISTS(SELECT email
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = team_members.id
|
||||
@@ -277,12 +383,33 @@ export default class TeamMembersController extends WorklenzControllerBase {
|
||||
|
||||
if (!id || !req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
|
||||
// check subscription status
|
||||
const subscriptionData = await checkTeamSubscriptionStatus(req.user?.team_id);
|
||||
if (statusExclude.includes(subscriptionData.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;`;
|
||||
const result = await db.query(q, [id, req.user?.id, req.user?.team_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const message = `You have been removed from <b>${req.user?.team_name}</b> by <b>${req.user?.name}</b>`;
|
||||
|
||||
// if (subscriptionData.status === "trialing") break;
|
||||
// if (!subscriptionData.is_credit && !subscriptionData.is_custom) {
|
||||
// if (subscriptionData.subscription_status === "active" && subscriptionData.quantity > 0) {
|
||||
// const obj = await getActiveTeamMemberCount(req.user?.owner_id ?? "");
|
||||
// // const activeObj = await getActiveTeamMemberCount(req.user?.owner_id ?? "");
|
||||
|
||||
// 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."));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
NotificationsService.sendNotification({
|
||||
receiver_socket_id: data.socket_id,
|
||||
message,
|
||||
@@ -871,20 +998,68 @@ export default class TeamMembersController extends WorklenzControllerBase {
|
||||
public static async toggleMemberActiveStatus(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
if (!req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
|
||||
const q1 = `SELECT active FROM team_members WHERE id = $1;`;
|
||||
const result1 = await db.query(q1, [req.params?.id]);
|
||||
const [status] = result1.rows;
|
||||
|
||||
if (status.active) {
|
||||
const updateQ1 = `UPDATE users
|
||||
SET active_team = (SELECT id FROM teams WHERE user_id = users.id ORDER BY created_at DESC LIMIT 1)
|
||||
WHERE id = (SELECT user_id FROM team_members WHERE id = $1 AND active IS TRUE LIMIT 1);`;
|
||||
await db.query(updateQ1, [req.params?.id]);
|
||||
// check subscription status
|
||||
const subscriptionData = await checkTeamSubscriptionStatus(req.user?.team_id);
|
||||
if (statusExclude.includes(subscriptionData.subscription_status)) {
|
||||
return res.status(200).send(new ServerResponse(false, "Please check your subscription status."));
|
||||
}
|
||||
|
||||
const q = `UPDATE team_members SET active = NOT active WHERE id = $1 RETURNING active;`;
|
||||
const result = await db.query(q, [req.params?.id]);
|
||||
const [data] = result.rows;
|
||||
let data: any;
|
||||
|
||||
if (req.query.active === "true") {
|
||||
const q1 = `SELECT active FROM team_members WHERE id = $1;`;
|
||||
const result1 = await db.query(q1, [req.params?.id]);
|
||||
const [status] = result1.rows;
|
||||
|
||||
if (status.active) {
|
||||
const updateQ1 = `UPDATE users
|
||||
SET active_team = (SELECT id FROM teams WHERE user_id = users.id ORDER BY created_at DESC LIMIT 1)
|
||||
WHERE id = (SELECT user_id FROM team_members WHERE id = $1 AND active IS TRUE LIMIT 1);`;
|
||||
await db.query(updateQ1, [req.params?.id]);
|
||||
}
|
||||
|
||||
const q = `UPDATE team_members SET active = NOT active WHERE id = $1 RETURNING active;`;
|
||||
const result = await db.query(q, [req.params?.id]);
|
||||
data = result.rows[0];
|
||||
|
||||
// const userExists = await this.checkIfUserActiveInOtherTeams(req.user?.owner_id as string, req.query?.email as string);
|
||||
|
||||
// if (subscriptionData.status === "trialing") break;
|
||||
// if (!userExists && !subscriptionData.is_credit && !subscriptionData.is_custom) {
|
||||
// if (subscriptionData.subscription_status === "active" && subscriptionData.quantity > 0) {
|
||||
// const operator = req.query.active === "true" ? - 1 : + 1;
|
||||
// const response = await updateUsers(subscriptionData.subscription_id, subscriptionData.quantity + operator);
|
||||
// if (!response.body.subscription_id) return res.status(200).send(new ServerResponse(false, response.message || "Please check your subscription."));
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
|
||||
const userExists = await this.checkIfUserActiveInOtherTeams(req.user?.owner_id as string, req.query?.email as string);
|
||||
|
||||
// if (subscriptionData.status === "trialing") break;
|
||||
// if (!userExists && !subscriptionData.is_credit && !subscriptionData.is_custom) {
|
||||
// if (subscriptionData.subscription_status === "active" && subscriptionData.quantity > 0) {
|
||||
// const operator = req.query.active === "true" ? - 1 : + 1;
|
||||
// const response = await updateUsers(subscriptionData.subscription_id, subscriptionData.quantity + operator);
|
||||
// if (!response.body.subscription_id) return res.status(200).send(new ServerResponse(false, response.message || "Please check your subscription."));
|
||||
// }
|
||||
// }
|
||||
|
||||
const q1 = `SELECT active FROM team_members WHERE id = $1;`;
|
||||
const result1 = await db.query(q1, [req.params?.id]);
|
||||
const [status] = result1.rows;
|
||||
|
||||
if (status.active) {
|
||||
const updateQ1 = `UPDATE users
|
||||
SET active_team = (SELECT id FROM teams WHERE user_id = users.id ORDER BY created_at DESC LIMIT 1)
|
||||
WHERE id = (SELECT user_id FROM team_members WHERE id = $1 AND active IS TRUE LIMIT 1);`;
|
||||
await db.query(updateQ1, [req.params?.id]);
|
||||
}
|
||||
|
||||
const q = `UPDATE team_members SET active = NOT active WHERE id = $1 RETURNING active;`;
|
||||
const result = await db.query(q, [req.params?.id]);
|
||||
data = result.rows[0];
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, [], `Team member ${data.active ? " activated" : " deactivated"} successfully.`));
|
||||
}
|
||||
@@ -899,6 +1074,21 @@ export default class TeamMembersController extends WorklenzControllerBase {
|
||||
|
||||
if (!req.body.team_id || !req.user?.id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
|
||||
// check the subscription status
|
||||
const subscriptionData = await checkTeamSubscriptionStatus(req.body.team_id);
|
||||
|
||||
if (statusExclude.includes(subscriptionData.subscription_status)) {
|
||||
return res.status(200).send(new ServerResponse(false, "Please check your subscription status."));
|
||||
}
|
||||
|
||||
// if (subscriptionData.status === "trialing") break;
|
||||
if (!subscriptionData.is_credit && !subscriptionData.is_custom) {
|
||||
if (subscriptionData.subscription_status === "active") {
|
||||
const response = await updateUsers(subscriptionData.subscription_id, subscriptionData.quantity + (req.body.emails.length || 1));
|
||||
if (!response.body.subscription_id) return res.status(200).send(new ServerResponse(false, response.message || "Please check your subscription."));
|
||||
}
|
||||
}
|
||||
|
||||
const newMembers = await this.createOrInviteMembers(req.body, req.user);
|
||||
return res.status(200).send(new ServerResponse(true, newMembers, `Your teammates will get an email that gives them access to your team.`).withTitle("Invitations sent"));
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ export default class TimezonesController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `UPDATE users SET timezone_id = $2 WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.user?.id, req.body.timezone]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows, "Timezone updated"));
|
||||
const q = `UPDATE users SET timezone_id = $2, language = $3 WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.user?.id, req.body.timezone, req.body.language]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows, "Updated successfully"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user