Merge branch 'main' of https://github.com/Worklenz/worklenz into feature/task-activities-by-user

This commit is contained in:
chamikaJ
2025-07-14 12:46:18 +05:30
1166 changed files with 82289 additions and 15230 deletions

View File

@@ -112,11 +112,11 @@ export const adminCenterApiService = {
async updateTeam(
team_id: string,
team_members: IOrganizationUser[]
body: { name: string; teamMembers: IOrganizationUser[] }
): Promise<IServerResponse<IOrganization>> {
const response = await apiClient.put<IServerResponse<IOrganization>>(
`${rootUrl}/organization/team/${team_id}`,
team_members
body
);
return response.data;
},
@@ -152,7 +152,6 @@ export const adminCenterApiService = {
return response.data;
},
// Billing - Configuration
async getCountries(): Promise<IServerResponse<IBillingConfigurationCountry[]>> {
const response = await apiClient.get<IServerResponse<IBillingConfigurationCountry[]>>(
@@ -168,7 +167,9 @@ export const adminCenterApiService = {
return response.data;
},
async updateBillingConfiguration(body: IBillingConfiguration): Promise<IServerResponse<IBillingConfiguration>> {
async updateBillingConfiguration(
body: IBillingConfiguration
): Promise<IServerResponse<IBillingConfiguration>> {
const response = await apiClient.put<IServerResponse<IBillingConfiguration>>(
`${rootUrl}/billing/configuration`,
body
@@ -178,42 +179,58 @@ export const adminCenterApiService = {
// Billing - Current Bill
async getCharges(): Promise<IServerResponse<IBillingChargesResponse>> {
const response = await apiClient.get<IServerResponse<IBillingChargesResponse>>(`${rootUrl}/billing/charges`);
const response = await apiClient.get<IServerResponse<IBillingChargesResponse>>(
`${rootUrl}/billing/charges`
);
return response.data;
},
async getTransactions(): Promise<IServerResponse<IBillingTransaction[]>> {
const response = await apiClient.get<IServerResponse<IBillingTransaction[]>>(`${rootUrl}/billing/transactions`);
const response = await apiClient.get<IServerResponse<IBillingTransaction[]>>(
`${rootUrl}/billing/transactions`
);
return response.data;
},
async getBillingAccountInfo(): Promise<IServerResponse<IBillingAccountInfo>> {
const response = await apiClient.get<IServerResponse<IBillingAccountInfo>>(`${rootUrl}/billing/info`);
const response = await apiClient.get<IServerResponse<IBillingAccountInfo>>(
`${rootUrl}/billing/info`
);
return response.data;
},
async getFreePlanSettings(): Promise<IServerResponse<IFreePlanSettings>> {
const response = await apiClient.get<IServerResponse<IFreePlanSettings>>(`${rootUrl}/billing/free-plan`);
const response = await apiClient.get<IServerResponse<IFreePlanSettings>>(
`${rootUrl}/billing/free-plan`
);
return response.data;
},
async upgradePlan(plan: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
const response = await apiClient.get<IServerResponse<IUpgradeSubscriptionPlanResponse>>(`${rootUrl}/billing/upgrade-plan${toQueryString({plan})}`);
const response = await apiClient.get<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
`${rootUrl}/billing/upgrade-plan${toQueryString({ plan })}`
);
return response.data;
},
async changePlan(plan: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
const response = await apiClient.get<IServerResponse<IUpgradeSubscriptionPlanResponse>>(`${rootUrl}/billing/change-plan${toQueryString({plan})}`);
const response = await apiClient.get<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
`${rootUrl}/billing/change-plan${toQueryString({ plan })}`
);
return response.data;
},
async getPlans(): Promise<IServerResponse<IPricingPlans>> {
const response = await apiClient.get<IServerResponse<IPricingPlans>>(`${rootUrl}/billing/plans`);
const response = await apiClient.get<IServerResponse<IPricingPlans>>(
`${rootUrl}/billing/plans`
);
return response.data;
},
async getStorageInfo(): Promise<IServerResponse<IStorageInfo>> {
const response = await apiClient.get<IServerResponse<IStorageInfo>>(`${rootUrl}/billing/storage`);
const response = await apiClient.get<IServerResponse<IStorageInfo>>(
`${rootUrl}/billing/storage`
);
return response.data;
},
@@ -225,7 +242,7 @@ export const adminCenterApiService = {
async resumeSubscription(): Promise<IServerResponse<any>> {
const response = await apiClient.get<IServerResponse<any>>(`${rootUrl}/billing/resume-plan`);
return response.data;
},
},
async cancelSubscription(): Promise<IServerResponse<any>> {
const response = await apiClient.get<IServerResponse<any>>(`${rootUrl}/billing/cancel-plan`);
@@ -233,26 +250,34 @@ export const adminCenterApiService = {
},
async addMoreSeats(totalSeats: number): Promise<IServerResponse<any>> {
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/billing/purchase-more-seats`, {seatCount: totalSeats});
const response = await apiClient.post<IServerResponse<any>>(
`${rootUrl}/billing/purchase-more-seats`,
{ seatCount: totalSeats }
);
return response.data;
},
async redeemCode(code: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
const response = await apiClient.post<IServerResponse<IUpgradeSubscriptionPlanResponse>>(`${rootUrl}/billing/redeem`, {
code,
});
const response = await apiClient.post<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
`${rootUrl}/billing/redeem`,
{
code,
}
);
return response.data;
},
async getAccountStorage(): Promise<IServerResponse<IBillingAccountStorage>> {
const response = await apiClient.get<IServerResponse<IBillingAccountStorage>>(`${rootUrl}/billing/account-storage`);
const response = await apiClient.get<IServerResponse<IBillingAccountStorage>>(
`${rootUrl}/billing/account-storage`
);
return response.data;
},
async switchToFreePlan(teamId: string): Promise<IServerResponse<any>> {
const response = await apiClient.get<IServerResponse<any>>(`${rootUrl}/billing/switch-to-free-plan/${teamId}`);
const response = await apiClient.get<IServerResponse<any>>(
`${rootUrl}/billing/switch-to-free-plan/${teamId}`
);
return response.data;
},
};

View File

@@ -6,7 +6,10 @@ import { IUpgradeSubscriptionPlanResponse } from '@/types/admin-center/admin-cen
const rootUrl = `${API_BASE_URL}/billing`;
export const billingApiService = {
async upgradeToPaidPlan(plan: string, seatCount: number): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
async upgradeToPaidPlan(
plan: string,
seatCount: number
): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
const q = toQueryString({ plan, seatCount });
const response = await apiClient.get<IServerResponse<any>>(
`${rootUrl}/upgrade-to-paid-plan${q}`
@@ -14,7 +17,9 @@ export const billingApiService = {
return response.data;
},
async purchaseMoreSeats(seatCount: number): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
async purchaseMoreSeats(
seatCount: number
): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
const response = await apiClient.post<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
`${rootUrl}/purchase-more-seats`,
{ seatCount }
@@ -27,9 +32,5 @@ export const billingApiService = {
`${rootUrl}/contact-us${toQueryString({ contactNo })}`
);
return response.data;
}
},
};

View File

@@ -4,45 +4,79 @@ import alertService from '@/services/alerts/alertService';
import logger from '@/utils/errorLogger';
import config from '@/config/env';
export const getCsrfToken = (): string | null => {
const match = document.cookie.split('; ').find(cookie => cookie.startsWith('XSRF-TOKEN='));
// Store CSRF token in memory (since csrf-sync uses session-based tokens)
let csrfToken: string | null = null;
if (!match) {
return null;
}
return decodeURIComponent(match.split('=')[1]);
export const getCsrfToken = (): string | null => {
return csrfToken;
};
// Function to refresh CSRF token if needed
// Function to refresh CSRF token from server
export const refreshCsrfToken = async (): Promise<string | null> => {
try {
// Make a GET request to the server to get a fresh CSRF token
await axios.get(`${config.apiUrl}/csrf-token`, { withCredentials: true });
return getCsrfToken();
} catch (error) {
console.error('Failed to refresh CSRF token:', error);
const tokenStart = performance.now();
console.log('[CSRF] Starting CSRF token refresh...');
// Make a GET request to the server to get a fresh CSRF token with timeout
const response = await axios.get(`${config.apiUrl}/csrf-token`, {
withCredentials: true,
timeout: 10000, // 10 second timeout for CSRF token requests
});
const tokenEnd = performance.now();
console.log(`[CSRF] CSRF token refresh completed in ${(tokenEnd - tokenStart).toFixed(2)}ms`);
if (response.data && response.data.token) {
csrfToken = response.data.token;
console.log('[CSRF] CSRF token successfully refreshed');
return csrfToken;
} else {
console.warn('[CSRF] No token in response:', response.data);
}
return null;
} catch (error) {
console.error('[CSRF] Failed to refresh CSRF token:', error);
return null;
}
};
// Initialize CSRF token on app load
export const initializeCsrfToken = async (): Promise<void> => {
if (!csrfToken) {
await refreshCsrfToken();
}
};
const apiClient = axios.create({
baseURL: config.apiUrl,
withCredentials: true,
timeout: 30000, // 30 second timeout to prevent hanging requests
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
// Request interceptor
// Request interceptor with performance optimization
apiClient.interceptors.request.use(
config => {
const token = getCsrfToken();
if (token) {
config.headers['X-CSRF-Token'] = token;
} else {
console.warn('No CSRF token found');
async config => {
const requestStart = performance.now();
// Ensure we have a CSRF token before making requests
if (!csrfToken) {
const tokenStart = performance.now();
await refreshCsrfToken();
const tokenEnd = performance.now();
}
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
} else {
console.warn('No CSRF token available after refresh attempt');
}
const requestEnd = performance.now();
return config;
},
error => Promise.reject(error)
@@ -80,21 +114,24 @@ apiClient.interceptors.response.use(
const errorResponse = error.response;
// Handle CSRF token errors
if (errorResponse?.status === 403 &&
(typeof errorResponse.data === 'object' &&
errorResponse.data !== null &&
'message' in errorResponse.data &&
errorResponse.data.message === 'Invalid CSRF token' ||
(error as any).code === 'EBADCSRFTOKEN')) {
if (
errorResponse?.status === 403 &&
((typeof errorResponse.data === 'object' &&
errorResponse.data !== null &&
'message' in errorResponse.data &&
(errorResponse.data.message === 'invalid csrf token' ||
errorResponse.data.message === 'Invalid CSRF token')) ||
(error as any).code === 'EBADCSRFTOKEN')
) {
alertService.error('Security Error', 'Invalid security token. Refreshing your session...');
// Try to refresh the CSRF token and retry the request
const newToken = await refreshCsrfToken();
if (newToken && error.config) {
// Update the token in the failed request
error.config.headers['X-CSRF-Token'] = newToken;
// Retry the original request with the new token
return axios(error.config);
return apiClient(error.config);
} else {
// If token refresh failed, redirect to login
window.location.href = '/auth/login';

View File

@@ -1,25 +1,37 @@
import { IServerResponse } from "@/types/common.types";
import { IProjectAttachmentsViewModel } from "@/types/tasks/task-attachment-view-model";
import apiClient from "../api-client";
import { API_BASE_URL } from "@/shared/constants";
import { toQueryString } from "@/utils/toQueryString";
import { IServerResponse } from '@/types/common.types';
import { IProjectAttachmentsViewModel } from '@/types/tasks/task-attachment-view-model';
import apiClient from '../api-client';
import { API_BASE_URL } from '@/shared/constants';
import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/attachments`;
export const attachmentsApiService = {
getTaskAttachments: async (taskId: string): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
const response = await apiClient.get<IServerResponse<IProjectAttachmentsViewModel>>(`${rootUrl}/tasks/${taskId}`);
getTaskAttachments: async (
taskId: string
): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
const response = await apiClient.get<IServerResponse<IProjectAttachmentsViewModel>>(
`${rootUrl}/tasks/${taskId}`
);
return response.data;
},
getProjectAttachments: async (projectId: string, index: number, size: number): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
getProjectAttachments: async (
projectId: string,
index: number,
size: number
): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
const q = toQueryString({ index, size });
const response = await apiClient.get<IServerResponse<IProjectAttachmentsViewModel>>(`${rootUrl}/project/${projectId}${q}`);
const response = await apiClient.get<IServerResponse<IProjectAttachmentsViewModel>>(
`${rootUrl}/project/${projectId}${q}`
);
return response.data;
},
downloadAttachment: async (id: string, filename: string): Promise<IServerResponse<string>> => {
const response = await apiClient.get<IServerResponse<string>>(`${rootUrl}/download?id=${id}&file=${filename}`);
const response = await apiClient.get<IServerResponse<string>>(
`${rootUrl}/download?id=${id}&file=${filename}`
);
return response.data;
},
@@ -27,7 +39,4 @@ export const attachmentsApiService = {
const response = await apiClient.delete<IServerResponse<string>>(`${rootUrl}/tasks/${id}`);
return response.data;
},
};

View File

@@ -5,7 +5,7 @@ import { toQueryString } from '@/utils/toQueryString';
import { IHomeTasksModel, IHomeTasksConfig } from '@/types/home/home-page.types';
import { IMyTask } from '@/types/home/my-tasks.types';
import { IProject } from '@/types/project/project.types';
import { getCsrfToken } from '../api-client';
import { getCsrfToken, refreshCsrfToken } from '../api-client';
import config from '@/config/env';
const rootUrl = '/home';
@@ -14,9 +14,18 @@ const api = createApi({
reducerPath: 'homePageApi',
baseQuery: fetchBaseQuery({
baseUrl: `${config.apiUrl}${API_BASE_URL}`,
prepareHeaders: headers => {
headers.set('X-CSRF-Token', getCsrfToken() || '');
prepareHeaders: async headers => {
// Get CSRF token, refresh if needed
let token = getCsrfToken();
if (!token) {
token = await refreshCsrfToken();
}
if (token) {
headers.set('X-CSRF-Token', token);
}
headers.set('Content-Type', 'application/json');
return headers;
},
credentials: 'include',
}),

View File

@@ -10,7 +10,7 @@ export const projectMembersApiService = {
createProjectMember: async (
body: IProjectMemberViewModel
): Promise<IServerResponse<IProjectMemberViewModel>> => {
const q = toQueryString({current_project_id: body.project_id});
const q = toQueryString({ current_project_id: body.project_id });
const response = await apiClient.post<IServerResponse<IProjectMemberViewModel>>(
`${rootUrl}${q}`,

View File

@@ -34,7 +34,9 @@ export const projectTemplatesApiService = {
return response.data;
},
createCustomTemplate: async (body: { template_id: string }): Promise<IServerResponse<IProjectTemplate>> => {
createCustomTemplate: async (body: {
template_id: string;
}): Promise<IServerResponse<IProjectTemplate>> => {
const response = await apiClient.post(`${rootUrl}/custom-template`, body);
return response.data;
},
@@ -44,15 +46,17 @@ export const projectTemplatesApiService = {
return response.data;
},
createFromWorklenzTemplate: async (body: { template_id: string }): Promise<IServerResponse<IProjectTemplate>> => {
createFromWorklenzTemplate: async (body: {
template_id: string;
}): Promise<IServerResponse<IProjectTemplate>> => {
const response = await apiClient.post(`${rootUrl}/import-template`, body);
return response.data;
},
createFromCustomTemplate: async (body: { template_id: string }): Promise<IServerResponse<IProjectTemplate>> => {
const response = await apiClient.post(`${rootUrl}/import-custom-template`, body);
return response.data;
},
createFromCustomTemplate: async (body: {
template_id: string;
}): Promise<IServerResponse<IProjectTemplate>> => {
const response = await apiClient.post(`${rootUrl}/import-custom-template`, body);
return response.data;
},
};

View File

@@ -7,9 +7,15 @@ import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { ITeamMemberOverviewGetResponse } from '@/types/project/project-insights.types';
import { IProjectMembersViewModel } from '@/types/projectMember.types';
import { IProjectManager } from '@/types/project/projectManager.types';
import { IGroupedProjectsViewModel } from '@/types/project/groupedProjectsViewModel.types';
const rootUrl = `${API_BASE_URL}/projects`;
interface UpdateProjectPayload {
id: string;
[key: string]: any;
}
export const projectsApiService = {
getProjects: async (
index: number,
@@ -27,6 +33,23 @@ export const projectsApiService = {
return response.data;
},
getGroupedProjects: async (
index: number,
size: number,
field: string | null,
order: string | null,
search: string | null,
groupBy: string,
filter: number | null = null,
statuses: string | null = null,
categories: string | null = null
): Promise<IServerResponse<IGroupedProjectsViewModel>> => {
const s = encodeURIComponent(search || '');
const url = `${rootUrl}/grouped${toQueryString({ index, size, field, order, search: s, groupBy, filter, statuses, categories })}`;
const response = await apiClient.get<IServerResponse<IGroupedProjectsViewModel>>(`${url}`);
return response.data;
},
getProject: async (id: string): Promise<IServerResponse<IProjectViewModel>> => {
const url = `${rootUrl}/${id}`;
const response = await apiClient.get<IServerResponse<IProjectViewModel>>(`${url}`);
@@ -79,12 +102,12 @@ export const projectsApiService = {
},
updateProject: async (
id: string,
project: IProjectViewModel
payload: UpdateProjectPayload
): Promise<IServerResponse<IProjectViewModel>> => {
const { id, ...data } = payload;
const q = toQueryString({ current_project_id: id });
const url = `${rootUrl}/${id}${q}`;
const response = await apiClient.put<IServerResponse<IProjectViewModel>>(`${url}`, project);
const url = `${API_BASE_URL}/projects/${id}${q}`;
const response = await apiClient.patch<IServerResponse<IProjectViewModel>>(url, data);
return response.data;
},
@@ -106,7 +129,10 @@ export const projectsApiService = {
return response.data;
},
updateDefaultTab: async (body: { project_id: string; default_view: string }): Promise<IServerResponse<any>> => {
updateDefaultTab: async (body: {
project_id: string;
default_view: string;
}): Promise<IServerResponse<any>> => {
const url = `${rootUrl}/update-pinned-view`;
const response = await apiClient.put<IServerResponse<IProjectViewModel>>(`${url}`, body);
return response.data;
@@ -118,4 +144,3 @@ export const projectsApiService = {
return response.data;
},
};

View File

@@ -5,7 +5,7 @@ import { IProjectCategory } from '@/types/project/projectCategory.types';
import { IProjectsViewModel } from '@/types/project/projectsViewModel.types';
import { IServerResponse } from '@/types/common.types';
import { IProjectMembersViewModel } from '@/types/projectMember.types';
import { getCsrfToken } from '../api-client';
import { getCsrfToken, refreshCsrfToken } from '../api-client';
import config from '@/config/env';
const rootUrl = '/projects';
@@ -14,9 +14,18 @@ export const projectsApi = createApi({
reducerPath: 'projectsApi',
baseQuery: fetchBaseQuery({
baseUrl: `${config.apiUrl}${API_BASE_URL}`,
prepareHeaders: headers => {
headers.set('X-CSRF-Token', getCsrfToken() || '');
prepareHeaders: async headers => {
// Get CSRF token, refresh if needed
let token = getCsrfToken();
if (!token) {
token = await refreshCsrfToken();
}
if (token) {
headers.set('X-CSRF-Token', token);
}
headers.set('Content-Type', 'application/json');
return headers;
},
credentials: 'include',
}),

View File

@@ -1,5 +1,10 @@
import { IServerResponse } from '@/types/common.types';
import { IGetProjectsRequestBody, IRPTMembersViewModel, IRPTOverviewProjectMember, IRPTProjectsViewModel } from '@/types/reporting/reporting.types';
import {
IGetProjectsRequestBody,
IRPTMembersViewModel,
IRPTOverviewProjectMember,
IRPTProjectsViewModel,
} from '@/types/reporting/reporting.types';
import apiClient from '../api-client';
import { API_BASE_URL } from '@/shared/constants';
import { toQueryString } from '@/utils/toQueryString';
@@ -7,9 +12,7 @@ import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/reporting/members`;
export const reportingMembersApiService = {
getMembers: async (
body: any
): Promise<IServerResponse<IRPTMembersViewModel>> => {
getMembers: async (body: any): Promise<IServerResponse<IRPTMembersViewModel>> => {
const q = toQueryString(body);
const url = `${rootUrl}${q}`;
const response = await apiClient.get<IServerResponse<IRPTMembersViewModel>>(url);

View File

@@ -1,5 +1,10 @@
import { IServerResponse } from '@/types/common.types';
import { IGetProjectsRequestBody, IRPTOverviewProjectInfo, IRPTOverviewProjectMember, IRPTProjectsViewModel } from '@/types/reporting/reporting.types';
import {
IGetProjectsRequestBody,
IRPTOverviewProjectInfo,
IRPTOverviewProjectMember,
IRPTProjectsViewModel,
} from '@/types/reporting/reporting.types';
import apiClient from '../api-client';
import { API_BASE_URL } from '@/shared/constants';
import { toQueryString } from '@/utils/toQueryString';
@@ -33,8 +38,11 @@ export const reportingProjectsApiService = {
return response.data;
},
getTasks: async (projectId: string, groupBy: string): Promise<IServerResponse<ITaskListGroup[]>> => {
const q = toQueryString({group: groupBy})
getTasks: async (
projectId: string,
groupBy: string
): Promise<IServerResponse<ITaskListGroup[]>> => {
const q = toQueryString({ group: groupBy });
const url = `${API_BASE_URL}/reporting/overview/project/tasks/${projectId}${q}`;
const response = await apiClient.get<IServerResponse<ITaskListGroup[]>>(url);

View File

@@ -3,12 +3,20 @@ import { toQueryString } from '@/utils/toQueryString';
import apiClient from '../api-client';
import { IServerResponse } from '@/types/common.types';
import { IAllocationViewModel } from '@/types/reporting/reporting-allocation.types';
import { IProjectLogsBreakdown, IRPTTimeMember, IRPTTimeProject, ITimeLogBreakdownReq } from '@/types/reporting/reporting.types';
import {
IProjectLogsBreakdown,
IRPTTimeMember,
IRPTTimeProject,
ITimeLogBreakdownReq,
} from '@/types/reporting/reporting.types';
const rootUrl = `${API_BASE_URL}/reporting`;
export const reportingTimesheetApiService = {
getTimeSheetData: async (body = {}, archived = false): Promise<IServerResponse<IAllocationViewModel>> => {
getTimeSheetData: async (
body = {},
archived = false
): Promise<IServerResponse<IAllocationViewModel>> => {
const q = toQueryString({ archived });
const response = await apiClient.post(`${rootUrl}/allocation/${q}`, body);
return response.data;
@@ -19,24 +27,35 @@ export const reportingTimesheetApiService = {
return response.data;
},
getProjectTimeSheets: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeProject[]>> => {
getProjectTimeSheets: async (
body = {},
archived = false
): Promise<IServerResponse<IRPTTimeProject[]>> => {
const q = toQueryString({ archived });
const response = await apiClient.post(`${rootUrl}/time-reports/projects/${q}`, body);
return response.data;
},
getMemberTimeSheets: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeMember[]>> => {
getMemberTimeSheets: async (
body = {},
archived = false
): Promise<IServerResponse<IRPTTimeMember[]>> => {
const q = toQueryString({ archived });
const response = await apiClient.post(`${rootUrl}/time-reports/members/${q}`, body);
return response.data;
},
getProjectTimeLogs: async (body: ITimeLogBreakdownReq): Promise<IServerResponse<IProjectLogsBreakdown[]>> => {
getProjectTimeLogs: async (
body: ITimeLogBreakdownReq
): Promise<IServerResponse<IProjectLogsBreakdown[]>> => {
const response = await apiClient.post(`${rootUrl}/project-timelogs`, body);
return response.data;
},
getProjectEstimatedVsActual: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeProject[]>> => {
getProjectEstimatedVsActual: async (
body = {},
archived = false
): Promise<IServerResponse<IRPTTimeProject[]>> => {
const q = toQueryString({ archived });
const response = await apiClient.post(`${rootUrl}/time-reports/estimated-vs-actual${q}`, body);
return response.data;

View File

@@ -2,7 +2,13 @@ import { API_BASE_URL } from '@/shared/constants';
import apiClient from '../api-client';
import { IServerResponse } from '@/types/common.types';
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
import { DateList, Member, Project, ScheduleData, Settings } from '@/types/schedule/schedule-v2.types';
import {
DateList,
Member,
Project,
ScheduleData,
Settings,
} from '@/types/schedule/schedule-v2.types';
const rootUrl = `${API_BASE_URL}/schedule-gannt-v2`;
@@ -45,16 +51,18 @@ export const scheduleAPIService = {
},
fetchMemberProjects: async ({ id }: { id: string }): Promise<IServerResponse<Project>> => {
const response = await apiClient.get<IServerResponse<Project>>(`${rootUrl}/members/projects/${id}`);
const response = await apiClient.get<IServerResponse<Project>>(
`${rootUrl}/members/projects/${id}`
);
return response.data;
},
submitScheduleData: async ({
schedule
schedule,
}: {
schedule: ScheduleData
schedule: ScheduleData;
}): Promise<IServerResponse<any>> => {
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/schedule`, schedule);
return response.data;
}
},
};

View File

@@ -52,8 +52,11 @@ export const profileSettingsApiService = {
return response.data;
},
updateTeamName: async (id: string, body: ITeam): Promise<IServerResponse<ITeam>> => {
const response = await apiClient.put<IServerResponse<ITeam>>(`${rootUrl}/team-name/${id}`, body);
updateTeamName: async (id: string, body: ITeam): Promise<IServerResponse<ITeam>> => {
const response = await apiClient.put<IServerResponse<ITeam>>(
`${rootUrl}/team-name/${id}`,
body
);
return response.data;
},

View File

@@ -21,12 +21,18 @@ export const taskTemplatesApiService = {
const response = await apiClient.get<IServerResponse<ITaskTemplateGetResponse>>(`${url}`);
return response.data;
},
createTemplate: async (body: { name: string, tasks: IProjectTask[] }): Promise<IServerResponse<ITask>> => {
createTemplate: async (body: {
name: string;
tasks: IProjectTask[];
}): Promise<IServerResponse<ITask>> => {
const url = `${rootUrl}`;
const response = await apiClient.post<IServerResponse<ITask>>(`${url}`, body);
return response.data;
},
updateTemplate: async (id: string, body: { name: string, tasks: IProjectTask[] }): Promise<IServerResponse<ITask>> => {
updateTemplate: async (
id: string,
body: { name: string; tasks: IProjectTask[] }
): Promise<IServerResponse<ITask>> => {
const url = `${rootUrl}/${id}`;
const response = await apiClient.put<IServerResponse<ITask>>(`${url}`, body);
return response.data;

View File

@@ -1,12 +1,12 @@
import apiClient from '@/api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/task-phases`;
interface UpdateSortOrderBody {
export interface UpdateSortOrderBody {
from_index: number;
to_index: number;
phases: ITaskPhase[];
@@ -14,9 +14,10 @@ interface UpdateSortOrderBody {
}
export const phasesApiService = {
addPhaseOption: async (projectId: string) => {
addPhaseOption: async (projectId: string, name?: string) => {
const q = toQueryString({ id: projectId, current_project_id: projectId });
const response = await apiClient.post<IServerResponse<ITaskPhase>>(`${rootUrl}${q}`);
const body = name ? { name } : {};
const response = await apiClient.post<IServerResponse<ITaskPhase>>(`${rootUrl}${q}`, body);
return response.data;
},
@@ -43,7 +44,7 @@ export const phasesApiService = {
return response.data;
},
updateNameOfPhase: async (phaseId: string, body: ITaskPhase, projectId: string,) => {
updateNameOfPhase: async (phaseId: string, body: ITaskPhase, projectId: string) => {
const q = toQueryString({ id: projectId, current_project_id: projectId });
const response = await apiClient.put<IServerResponse<ITaskPhase>>(
`${rootUrl}/${phaseId}${q}`,

View File

@@ -60,6 +60,20 @@ export const statusApiService = {
return response.data;
},
updateStatusCategory: async (
statusId: string,
categoryId: string,
currentProjectId: string
): Promise<IServerResponse<ITaskStatus>> => {
const q = toQueryString({ current_project_id: currentProjectId });
const response = await apiClient.put<IServerResponse<ITaskStatus>>(
`${rootUrl}/category/${statusId}${q}`,
{ category_id: categoryId }
);
return response.data;
},
updateStatusOrder: async (
body: ITaskStatusCreateRequest,
currentProjectId: string
@@ -69,8 +83,16 @@ export const statusApiService = {
return response.data;
},
deleteStatus: async (statusId: string, projectId: string, replacingStatusId: string): Promise<IServerResponse<void>> => {
const q = toQueryString({ project: projectId, current_project_id: projectId, replace: replacingStatusId || null });
deleteStatus: async (
statusId: string,
projectId: string,
replacingStatusId: string
): Promise<IServerResponse<void>> => {
const q = toQueryString({
project: projectId,
current_project_id: projectId,
replace: replacingStatusId || null,
});
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/${statusId}${q}`);
return response.data;
},

View File

@@ -1,7 +1,7 @@
import { API_BASE_URL } from "@/shared/constants";
import apiClient from "../api-client";
import { IServerResponse } from "@/types/common.types";
import { ISubTask } from "@/types/tasks/subTask.types";
import { API_BASE_URL } from '@/shared/constants';
import apiClient from '../api-client';
import { IServerResponse } from '@/types/common.types';
import { ISubTask } from '@/types/tasks/subTask.types';
const root = `${API_BASE_URL}/sub-tasks`;
@@ -10,7 +10,4 @@ export const subTasksApiService = {
const response = await apiClient.get(`${root}/${parentTaskId}`);
return response.data;
},
};

View File

@@ -1,5 +1,9 @@
import { IServerResponse } from '@/types/common.types';
import { IProjectAttachmentsViewModel, ITaskAttachment, ITaskAttachmentViewModel } from '@/types/tasks/task-attachment-view-model';
import {
IProjectAttachmentsViewModel,
ITaskAttachment,
ITaskAttachmentViewModel,
} from '@/types/tasks/task-attachment-view-model';
import apiClient from '../api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IAvatarAttachment } from '@/types/avatarAttachment.types';
@@ -8,7 +12,6 @@ import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/attachments`;
const taskAttachmentsApiService = {
createTaskAttachment: async (
body: ITaskAttachment
): Promise<IServerResponse<ITaskAttachmentViewModel>> => {
@@ -16,18 +19,26 @@ const taskAttachmentsApiService = {
return response.data;
},
createAvatarAttachment: async (body: IAvatarAttachment): Promise<IServerResponse<{ url: string; }>> => {
createAvatarAttachment: async (
body: IAvatarAttachment
): Promise<IServerResponse<{ url: string }>> => {
const response = await apiClient.post(`${rootUrl}/avatar`, body);
return response.data;
},
getTaskAttachments: async (taskId: string): Promise<IServerResponse<ITaskAttachmentViewModel[]>> => {
getTaskAttachments: async (
taskId: string
): Promise<IServerResponse<ITaskAttachmentViewModel[]>> => {
const response = await apiClient.get(`${rootUrl}/tasks/${taskId}`);
return response.data;
},
getProjectAttachments: async (projectId: string, index: number, size: number ): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
const q = toQueryString({ index, size });
getProjectAttachments: async (
projectId: string,
index: number,
size: number
): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
const q = toQueryString({ index, size });
const response = await apiClient.get(`${rootUrl}/project/${projectId}${q}`);
return response.data;
},

View File

@@ -2,10 +2,16 @@ import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { toQueryString } from '@/utils/toQueryString';
import { ITaskComment, ITaskCommentsCreateRequest, ITaskCommentViewModel } from '@/types/tasks/task-comments.types';
import {
ITaskComment,
ITaskCommentsCreateRequest,
ITaskCommentViewModel,
} from '@/types/tasks/task-comments.types';
const taskCommentsApiService = {
create: async (data: ITaskCommentsCreateRequest): Promise<IServerResponse<ITaskCommentsCreateRequest>> => {
create: async (
data: ITaskCommentsCreateRequest
): Promise<IServerResponse<ITaskCommentsCreateRequest>> => {
const response = await apiClient.post(`${API_BASE_URL}/task-comments`, data);
return response.data;
},
@@ -21,12 +27,16 @@ const taskCommentsApiService = {
},
deleteAttachment: async (id: string, taskId: string): Promise<IServerResponse<ITaskComment>> => {
const response = await apiClient.delete(`${API_BASE_URL}/task-comments/attachment/${id}/${taskId}`);
const response = await apiClient.delete(
`${API_BASE_URL}/task-comments/attachment/${id}/${taskId}`
);
return response.data;
},
download: async (id: string, filename: string): Promise<IServerResponse<any>> => {
const response = await apiClient.get(`${API_BASE_URL}/task-comments/download?id=${id}&file=${filename}`);
const response = await apiClient.get(
`${API_BASE_URL}/task-comments/download?id=${id}&file=${filename}`
);
return response.data;
},
@@ -35,8 +45,13 @@ const taskCommentsApiService = {
return response.data;
},
updateReaction: async (id: string, body: {reaction_type: string, task_id: string}): Promise<IServerResponse<ITaskComment>> => {
const response = await apiClient.put(`${API_BASE_URL}/task-comments/reaction/${id}${toQueryString(body)}`);
updateReaction: async (
id: string,
body: { reaction_type: string; task_id: string }
): Promise<IServerResponse<ITaskComment>> => {
const response = await apiClient.put(
`${API_BASE_URL}/task-comments/reaction/${id}${toQueryString(body)}`
);
return response.data;
},
};

View File

@@ -1,7 +1,7 @@
import { API_BASE_URL } from "@/shared/constants";
import apiClient from "../api-client";
import { ITaskDependency } from "@/types/tasks/task-dependency.types";
import { IServerResponse } from "@/types/common.types";
import { API_BASE_URL } from '@/shared/constants';
import apiClient from '../api-client';
import { ITaskDependency } from '@/types/tasks/task-dependency.types';
import { IServerResponse } from '@/types/common.types';
const rootUrl = `${API_BASE_URL}/task-dependencies`;
@@ -10,7 +10,9 @@ export const taskDependenciesApiService = {
const response = await apiClient.get(`${rootUrl}/${taskId}`);
return response.data;
},
createTaskDependency: async (body: ITaskDependency): Promise<IServerResponse<ITaskDependency>> => {
createTaskDependency: async (
body: ITaskDependency
): Promise<IServerResponse<ITaskDependency>> => {
const response = await apiClient.post(`${rootUrl}`, body);
return response.data;
},
@@ -18,4 +20,4 @@ export const taskDependenciesApiService = {
const response = await apiClient.delete(`${rootUrl}/${dependencyId}`);
return response.data;
},
};
};

View File

@@ -0,0 +1,21 @@
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { ITaskRecurringSchedule } from '@/types/tasks/task-recurring-schedule';
import apiClient from '../api-client';
const rootUrl = `${API_BASE_URL}/task-recurring`;
export const taskRecurringApiService = {
getTaskRecurringData: async (
schedule_id: string
): Promise<IServerResponse<ITaskRecurringSchedule>> => {
const response = await apiClient.get(`${rootUrl}/${schedule_id}`);
return response.data;
},
updateTaskRecurringData: async (
schedule_id: string,
body: any
): Promise<IServerResponse<ITaskRecurringSchedule>> => {
return apiClient.put(`${rootUrl}/${schedule_id}`, body);
},
};

View File

@@ -1,17 +1,27 @@
import { API_BASE_URL } from "@/shared/constants";
import apiClient from "../api-client";
import { IServerResponse } from "@/types/common.types";
import { ITaskLogViewModel } from "@/types/tasks/task-log-view.types";
import { API_BASE_URL } from '@/shared/constants';
import apiClient from '../api-client';
import { IServerResponse } from '@/types/common.types';
import { ITaskLogViewModel } from '@/types/tasks/task-log-view.types';
const rootUrl = `${API_BASE_URL}/task-time-log`;
export interface IRunningTimer {
task_id: string;
start_time: string;
task_name: string;
project_id: string;
project_name: string;
parent_task_id?: string;
parent_task_name?: string;
}
export const taskTimeLogsApiService = {
getByTask: async (id: string) : Promise<IServerResponse<ITaskLogViewModel[]>> => {
getByTask: async (id: string): Promise<IServerResponse<ITaskLogViewModel[]>> => {
const response = await apiClient.get(`${rootUrl}/task/${id}`);
return response.data;
},
delete: async (id: string, taskId: string) : Promise<IServerResponse<void>> => {
delete: async (id: string, taskId: string): Promise<IServerResponse<void>> => {
const response = await apiClient.delete(`${rootUrl}/${id}?task=${taskId}`);
return response.data;
},
@@ -26,6 +36,11 @@ export const taskTimeLogsApiService = {
return response.data;
},
getRunningTimers: async (): Promise<IServerResponse<IRunningTimer[]>> => {
const response = await apiClient.get(`${rootUrl}/running-timers`);
return response.data;
},
exportToExcel(taskId: string) {
window.location.href = `${import.meta.env.VITE_API_URL}${API_BASE_URL}/task-time-log/export/${taskId}`;
},

View File

@@ -1,23 +1,23 @@
import { ITaskListColumn } from "@/types/tasks/taskList.types";
import apiClient from "../api-client";
import { IServerResponse } from "@/types/common.types";
import { ITaskListColumn } from '@/types/tasks/taskList.types';
import apiClient from '../api-client';
import { IServerResponse } from '@/types/common.types';
export const tasksCustomColumnsService = {
getCustomColumns: async (projectId: string): Promise<IServerResponse<ITaskListColumn[]>> => {
const response = await apiClient.get(`/api/v1/custom-columns/project/${projectId}/columns`);
return response.data;
},
updateTaskCustomColumnValue: async (
taskId: string,
columnKey: string,
taskId: string,
columnKey: string,
value: string | number | boolean,
projectId: string
): Promise<IServerResponse<any>> => {
const response = await apiClient.put(`/api/v1/tasks/${taskId}/custom-column`, {
column_key: columnKey,
value: value,
project_id: projectId
project_id: projectId,
});
return response.data;
},
@@ -35,7 +35,7 @@ export const tasksCustomColumnsService = {
): Promise<IServerResponse<any>> => {
const response = await apiClient.post('/api/v1/custom-columns', {
project_id: projectId,
...columnData
...columnData,
});
return response.data;
},
@@ -63,7 +63,10 @@ export const tasksCustomColumnsService = {
projectId: string,
item: ITaskListColumn
): Promise<IServerResponse<ITaskListColumn>> => {
const response = await apiClient.put(`/api/v1/custom-columns/project/${projectId}/columns`, item);
const response = await apiClient.put(
`/api/v1/custom-columns/project/${projectId}/columns`,
item
);
return response.data;
}
},
};

View File

@@ -28,6 +28,24 @@ export interface ITaskListConfigV2 {
parent_task?: string;
group?: string;
isSubtasksInclude: boolean;
include_empty?: string; // Include empty groups in response
customColumns?: boolean; // Include custom column values in response
}
export interface ITaskListV3Response {
groups: Array<{
id: string;
title: string;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
collapsed: boolean;
tasks: any[];
taskIds: string[];
color: string;
}>;
allTasks: any[];
grouping: string;
totalTasks: number;
}
export const tasksApiService = {
@@ -114,9 +132,70 @@ export const tasksApiService = {
return response.data;
},
getTaskDependencyStatus: async (taskId: string, statusId: string): Promise<IServerResponse<{ can_continue: boolean }>> => {
const q = toQueryString({taskId, statusId});
getTaskDependencyStatus: async (
taskId: string,
statusId: string
): Promise<IServerResponse<{ can_continue: boolean }>> => {
const q = toQueryString({ taskId, statusId });
const response = await apiClient.get(`${rootUrl}/dependency-status${q}`);
return response.data;
},
getTaskListV3: async (
config: ITaskListConfigV2
): Promise<IServerResponse<ITaskListV3Response>> => {
const q = toQueryString({ ...config, include_empty: 'true' });
const response = await apiClient.get(`${rootUrl}/list/v3/${config.id}${q}`);
return response.data;
},
refreshTaskProgress: async (projectId: string): Promise<IServerResponse<{ message: string }>> => {
const response = await apiClient.post(`${rootUrl}/refresh-progress/${projectId}`);
return response.data;
},
getTaskProgressStatus: async (
projectId: string
): Promise<
IServerResponse<{
projectId: string;
totalTasks: number;
completedTasks: number;
avgProgress: number;
lastUpdated: string;
completionPercentage: number;
}>
> => {
const response = await apiClient.get(`${rootUrl}/progress-status/${projectId}`);
return response.data;
},
// API method to reorder tasks
reorderTasks: async (params: {
taskIds: string[];
newOrder: number[];
projectId: string;
}): Promise<IServerResponse<{ done: boolean }>> => {
const response = await apiClient.post(`${rootUrl}/reorder`, {
task_ids: params.taskIds,
new_order: params.newOrder,
project_id: params.projectId,
});
return response.data;
},
// API method to update task group (status, priority, phase)
updateTaskGroup: async (params: {
taskId: string;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
projectId: string;
}): Promise<IServerResponse<{ done: boolean }>> => {
const response = await apiClient.put(`${rootUrl}/${params.taskId}/group`, {
group_type: params.groupType,
group_value: params.groupValue,
project_id: params.projectId,
});
return response.data;
},
};

View File

@@ -44,7 +44,9 @@ export const teamMembersApiService = {
return response.data;
},
getAll: async (projectId: string | null = null): Promise<IServerResponse<ITeamMemberViewModel[]>> => {
getAll: async (
projectId: string | null = null
): Promise<IServerResponse<ITeamMemberViewModel[]>> => {
const params = new URLSearchParams(projectId ? { project: projectId } : {});
const response = await apiClient.get<IServerResponse<ITeamMemberViewModel[]>>(
`${rootUrl}/all${params.toString() ? '?' + params.toString() : ''}`

View File

@@ -14,11 +14,8 @@ const rootUrl = `${API_BASE_URL}/teams`;
export const teamsApiService = {
getTeams: async (): Promise<IServerResponse<ITeamGetResponse[]>> => {
const response = await apiClient.get<IServerResponse<ITeamGetResponse[]>>(
`${rootUrl}`
);
const response = await apiClient.get<IServerResponse<ITeamGetResponse[]>>(`${rootUrl}`);
return response.data;
},
setActiveTeam: async (teamId: string): Promise<IServerResponse<ITeamActivateResponse>> => {
@@ -29,23 +26,18 @@ export const teamsApiService = {
return response.data;
},
createTeam: async (team: IOrganizationTeam): Promise<IServerResponse<ITeam>> => {
const response = await apiClient.post<IServerResponse<ITeam>>(`${rootUrl}`, team);
return response.data;
},
getInvitations: async (): Promise<IServerResponse<ITeamInvites[]>> => {
const response = await apiClient.get<IServerResponse<ITeamInvites[]>>(
`${rootUrl}/invites`
);
const response = await apiClient.get<IServerResponse<ITeamInvites[]>>(`${rootUrl}/invites`);
return response.data;
},
acceptInvitation: async (body: IAcceptTeamInvite): Promise<IServerResponse<ITeamInvites>> => {
const response = await apiClient.put<IServerResponse<ITeamInvites>>(`${rootUrl}`, body);
return response.data;
}
},
};