This commit is contained in:
chamikaJ
2025-04-17 18:28:54 +05:30
parent f583291d8a
commit 8825b0410a
2837 changed files with 241385 additions and 127578 deletions

View File

@@ -0,0 +1,48 @@
// Core dependencies
import React, { Suspense, useEffect } from 'react';
import { RouterProvider } from 'react-router-dom';
import i18next from 'i18next';
// Components
import ThemeWrapper from './features/theme/ThemeWrapper';
import PreferenceSelector from './components/PreferenceSelector';
// Routes
import router from './app/routes';
// Hooks & Utils
import { useAppSelector } from './hooks/useAppSelector';
import { initMixpanel } from './utils/mixpanelInit';
// Types & Constants
import { Language } from './features/i18n/localesSlice';
import logger from './utils/errorLogger';
import { SuspenseFallback } from './components/suspense-fallback/suspense-fallback';
const App: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const themeMode = useAppSelector(state => state.themeReducer.mode);
const language = useAppSelector(state => state.localesReducer.lng);
initMixpanel(import.meta.env.VITE_MIXPANEL_TOKEN as string);
useEffect(() => {
document.documentElement.setAttribute('data-theme', themeMode);
}, [themeMode]);
useEffect(() => {
i18next.changeLanguage(language || Language.EN, err => {
if (err) return logger.error('Error changing language', err);
});
}, [language]);
return (
<Suspense fallback={<SuspenseFallback />}>
<ThemeWrapper>
<RouterProvider router={router} future={{ v7_startTransition: true }} />
<PreferenceSelector />
</ThemeWrapper>
</Suspense>
);
};
export default App;

View File

@@ -0,0 +1,258 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '../api-client';
import { API_BASE_URL } from '@/shared/constants';
import {
IOrganization,
IOrganizationUser,
IOrganizationTeam,
IOrganizationUsersGetRequest,
IOrganizationTeamGetRequest,
IOrganizationProjectsGetResponse,
IBillingConfigurationCountry,
IBillingConfiguration,
IBillingAccountInfo,
IUpgradeSubscriptionPlanResponse,
IPricingPlans,
IBillingTransaction,
IBillingChargesResponse,
IStorageInfo,
IFreePlanSettings,
IBillingAccountStorage,
} from '@/types/admin-center/admin-center.types';
import { IClient } from '@/types/client.types';
import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/admin-center`;
export interface IOrganizationUserRequestParams {
page: number;
pageSize: number;
sort: string;
order: string;
searchTerm: string;
}
export interface IOrganizationTeamRequestParams {
index: number;
size: number;
field: string | null;
order: string | null;
search: string | null;
}
export const adminCenterApiService = {
async getOrganizationDetails(): Promise<IServerResponse<IOrganization>> {
const response = await apiClient.get<IServerResponse<IOrganization>>(`${rootUrl}/organization`);
return response.data;
},
async getOrganizationAdmins(): Promise<IServerResponse<IOrganizationUser[]>> {
const response = await apiClient.get<IServerResponse<IOrganizationUser[]>>(
`${rootUrl}/organization/admins`
);
return response.data;
},
async updateOrganizationName<T>(body: IClient): Promise<IServerResponse<IOrganization>> {
const response = await apiClient.put<IServerResponse<IOrganization>>(
`${rootUrl}/organization`,
body
);
return response.data;
},
async updateOwnerContactNumber<T>(body: {
contact_number: string;
}): Promise<IServerResponse<IOrganization>> {
const response = await apiClient.put<IServerResponse<IOrganization>>(
`${rootUrl}/organization/owner/contact-number`,
body
);
return response.data;
},
async getOrganizationUsers(
requestParams: IOrganizationUserRequestParams
): Promise<IServerResponse<IOrganizationUsersGetRequest>> {
const params = new URLSearchParams({
index: requestParams.page.toString(),
size: requestParams.pageSize.toString(),
...(requestParams.sort && { field: requestParams.sort }),
...(requestParams.order && { order: requestParams.order }),
...(requestParams.searchTerm && { search: requestParams.searchTerm }),
});
const response = await apiClient.get<IServerResponse<IOrganizationUsersGetRequest>>(
`${rootUrl}/organization/users?${params}`
);
return response.data;
},
async getOrganizationTeams(
requestParams: IOrganizationTeamRequestParams
): Promise<IServerResponse<IOrganizationTeamGetRequest>> {
const params = new URLSearchParams({
index: requestParams.index.toString(),
size: requestParams.size.toString(),
...(requestParams.field && { field: requestParams.field }),
...(requestParams.order && { order: requestParams.order }),
...(requestParams.search && { search: requestParams.search }),
});
const response = await apiClient.get<IServerResponse<IOrganizationTeamGetRequest>>(
`${rootUrl}/organization/teams?${params}`
);
return response.data;
},
async getOrganizationTeam(team_id: string): Promise<IServerResponse<IOrganizationTeam>> {
const response = await apiClient.get<IServerResponse<IOrganizationTeam>>(
`${rootUrl}/organization/team/${team_id}`
);
return response.data;
},
async updateTeam(
team_id: string,
team_members: IOrganizationUser[]
): Promise<IServerResponse<IOrganization>> {
const response = await apiClient.put<IServerResponse<IOrganization>>(
`${rootUrl}/organization/team/${team_id}`,
team_members
);
return response.data;
},
async deleteTeam(id: string): Promise<IServerResponse<any>> {
const response = await apiClient.delete<IServerResponse<any>>(
`${rootUrl}/organization/team/${id}`
);
return response.data;
},
async removeTeamMember(team_member_id: string, team_id: string): Promise<IServerResponse<any>> {
const response = await apiClient.put<IServerResponse<any>>(
`${rootUrl}/organization/team-member/${team_member_id}`,
{ teamId: team_id }
);
return response.data;
},
async getOrganizationProjects(
requestParams: IOrganizationTeamRequestParams
): Promise<IServerResponse<IOrganizationProjectsGetResponse>> {
const params = new URLSearchParams({
index: requestParams.index.toString(),
size: requestParams.size.toString(),
...(requestParams.field && { field: requestParams.field }),
...(requestParams.order && { order: requestParams.order }),
...(requestParams.search && { search: requestParams.search }),
});
const response = await apiClient.get<IServerResponse<IOrganizationProjectsGetResponse>>(
`${rootUrl}/organization/projects?${params}`
);
return response.data;
},
// Billing - Configuration
async getCountries(): Promise<IServerResponse<IBillingConfigurationCountry[]>> {
const response = await apiClient.get<IServerResponse<IBillingConfigurationCountry[]>>(
`${rootUrl}/billing/countries`
);
return response.data;
},
async getBillingConfiguration(): Promise<IServerResponse<IBillingConfiguration>> {
const response = await apiClient.get<IServerResponse<IBillingConfiguration>>(
`${rootUrl}/billing/configuration`
);
return response.data;
},
async updateBillingConfiguration(body: IBillingConfiguration): Promise<IServerResponse<IBillingConfiguration>> {
const response = await apiClient.put<IServerResponse<IBillingConfiguration>>(
`${rootUrl}/billing/configuration`,
body
);
return response.data;
},
// Billing - Current Bill
async getCharges(): Promise<IServerResponse<IBillingChargesResponse>> {
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`);
return response.data;
},
async getBillingAccountInfo(): Promise<IServerResponse<IBillingAccountInfo>> {
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`);
return response.data;
},
async upgradePlan(plan: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
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})}`);
return response.data;
},
async getPlans(): Promise<IServerResponse<IPricingPlans>> {
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`);
return response.data;
},
async pauseSubscription(): Promise<IServerResponse<any>> {
const response = await apiClient.get<IServerResponse<any>>(`${rootUrl}/billing/pause-plan`);
return response.data;
},
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`);
return response.data;
},
async addMoreSeats(totalSeats: number): Promise<IServerResponse<any>> {
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,
});
return response.data;
},
async getAccountStorage(): Promise<IServerResponse<IBillingAccountStorage>> {
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}`);
return response.data;
},
};

View File

@@ -0,0 +1,35 @@
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import apiClient from '../api-client';
import { toQueryString } from '@/utils/toQueryString';
import { IUpgradeSubscriptionPlanResponse } from '@/types/admin-center/admin-center.types';
const rootUrl = `${API_BASE_URL}/billing`;
export const billingApiService = {
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}`
);
return response.data;
},
async purchaseMoreSeats(seatCount: number): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
const response = await apiClient.post<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
`${rootUrl}/purchase-more-seats`,
{ seatCount }
);
return response.data;
},
async contactUs(contactNo: string): Promise<IServerResponse<any>> {
const response = await apiClient.get<IServerResponse<any>>(
`${rootUrl}/contact-us${toQueryString({ contactNo })}`
);
return response.data;
}
};

View File

@@ -0,0 +1,134 @@
import axios, { AxiosError } from 'axios';
import alertService from '@/services/alerts/alertService';
import logger from '@/utils/errorLogger';
export const getCsrfToken = (): string | null => {
const match = document.cookie.split('; ').find(cookie => cookie.startsWith('XSRF-TOKEN='));
if (!match) {
return null;
}
return decodeURIComponent(match.split('=')[1]);
};
// Function to refresh CSRF token if needed
export const refreshCsrfToken = async (): Promise<string | null> => {
try {
// Make a GET request to the server to get a fresh CSRF token
await axios.get(`${import.meta.env.VITE_API_URL}/csrf-token`, { withCredentials: true });
return getCsrfToken();
} catch (error) {
console.error('Failed to refresh CSRF token:', error);
return null;
}
};
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
// Request interceptor
apiClient.interceptors.request.use(
config => {
const token = getCsrfToken();
if (token) {
config.headers['X-CSRF-Token'] = token;
} else {
console.warn('No CSRF token found');
}
return config;
},
error => Promise.reject(error)
);
// Response interceptor with notification handling based on done flag
apiClient.interceptors.response.use(
response => {
// Handle 302 redirect
if (response.status === 302) {
const redirectUrl = response.headers.location;
if (redirectUrl) {
window.location.href = redirectUrl;
return response;
}
}
if (response.data) {
const { title, message, auth_error, done } = response.data;
if (message && message.charAt(0) !== '$') {
if (done) {
alertService.success(title || '', message);
} else {
alertService.error(title || '', message);
}
} else if (auth_error) {
alertService.error(title || 'Authentication Error', auth_error);
}
}
return response;
},
async (error: AxiosError) => {
const { message, code, name } = error || {};
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')) {
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);
} else {
// If token refresh failed, redirect to login
window.location.href = '/auth/login';
return Promise.reject(error);
}
}
// Add 401 unauthorized handling
if (error.response?.status === 401) {
alertService.error('Session Expired', 'Please log in again');
// Redirect to login page or trigger re-authentication
window.location.href = '/auth/login'; // Adjust this path as needed
return Promise.reject(error);
}
const errorMessage = message || 'An unexpected error occurred';
const errorTitle = 'Error';
if (error.code !== 'ERR_NETWORK') {
alertService.error(errorTitle, errorMessage);
}
// Development logging
if (import.meta.env.VITE_APP_ENV === 'development') {
logger.error('API Error:', {
code,
name,
message,
headers: error.config?.headers,
cookies: document.cookie,
});
}
return Promise.reject(error);
}
);
export default apiClient;

View File

@@ -0,0 +1,33 @@
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}`);
return response.data;
},
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}`);
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}`);
return response.data;
},
deleteAttachment: async (id: string): Promise<IServerResponse<string>> => {
const response = await apiClient.delete<IServerResponse<string>>(`${rootUrl}/tasks/${id}`);
return response.data;
},
};

View File

@@ -0,0 +1,59 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '../api-client';
import {
IUserLoginRequest,
IUserLoginResponse,
IAuthorizeResponse,
} from '@/types/auth/login.types';
import { AUTH_API_BASE_URL } from '@/shared/constants';
const rootUrl = `${AUTH_API_BASE_URL}`;
export const authApiService = {
async login(credentials: IUserLoginRequest): Promise<IAuthorizeResponse> {
const response = await apiClient.post<IAuthorizeResponse>(`${rootUrl}/login`, credentials);
return response.data;
},
async logout(): Promise<IServerResponse<void>> {
const response = await apiClient.get<IServerResponse<void>>(`${rootUrl}/logout`);
return response.data;
},
async verify(): Promise<IAuthorizeResponse> {
const response = await apiClient.get<IAuthorizeResponse>(`${rootUrl}/verify`);
return response.data;
},
async signUp(body: any): Promise<IServerResponse<void>> {
const response = await apiClient.post<IServerResponse<void>>(`${rootUrl}/signup`, body);
return response.data;
},
async signUpCheck(body: any): Promise<IServerResponse<void>> {
const response = await apiClient.post<IServerResponse<void>>(`${rootUrl}/signup/check`, body);
return response.data;
},
async resetPassword(email: string): Promise<IServerResponse<string>> {
const response = await apiClient.post<IServerResponse<string>>(`${rootUrl}/reset-password`, {
email,
});
return response.data;
},
async updatePassword(values: any): Promise<IServerResponse<string>> {
const response = await apiClient.post<IServerResponse<string>>(
`${rootUrl}/update-password`,
values
);
return response.data;
},
async verifyRecaptchaToken(token: string): Promise<IServerResponse<string>> {
const response = await apiClient.post<IServerResponse<string>>(`${rootUrl}/verify-captcha`, {
token,
});
return response.data;
},
};

View File

@@ -0,0 +1,49 @@
import { IClient, IClientsViewModel } from '@/types/client.types';
import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/clients`;
export const clientsApiService = {
// Get all clients
async getClients(
index: number,
size: number,
field: string | null,
order: string | null,
search?: string | null
): Promise<IServerResponse<IClientsViewModel>> {
const s = encodeURIComponent(search || '');
const queryString = toQueryString({ index, size, field, order, search: s });
const response = await apiClient.get<IServerResponse<IClientsViewModel>>(
`${rootUrl}${queryString}`
);
return response.data;
},
// Get single client by ID
async getClientById(id: string): Promise<IServerResponse<IClient>> {
const response = await apiClient.get<IServerResponse<IClient>>(`${rootUrl}/${id}`);
return response.data;
},
// Create new client
async createClient(body: IClient): Promise<IServerResponse<IClient>> {
const response = await apiClient.post<IServerResponse<IClient>>(rootUrl, body);
return response.data;
},
// Update existing client
async updateClient(id: string, body: IClient): Promise<IServerResponse<IClient>> {
const response = await apiClient.put<IServerResponse<IClient>>(`${rootUrl}/${id}`, body);
return response.data;
},
// Delete client
async deleteClient(id: string): Promise<IServerResponse<void>> {
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/${id}`);
return response.data;
},
};

View File

@@ -0,0 +1,72 @@
import { BaseQueryFn, createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
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';
const rootUrl = '/home';
const api = createApi({
reducerPath: 'homePageApi',
baseQuery: fetchBaseQuery({
baseUrl: `${import.meta.env.VITE_API_URL}${API_BASE_URL}`,
prepareHeaders: headers => {
headers.set('X-CSRF-Token', getCsrfToken() || '');
headers.set('Content-Type', 'application/json');
},
credentials: 'include',
}),
tagTypes: ['personalTasks', 'projects', 'teamProjects'],
endpoints: builder => ({
getPersonalTasks: builder.query<IServerResponse<IMyTask[]>, void>({
query: () => `${rootUrl}/personal-tasks`,
}),
createPersonalTask: builder.mutation<IServerResponse<any>, IMyTask>({
query: body => ({
url: `${rootUrl}/personal-task`,
method: 'POST',
body,
}),
}),
markPersonalTaskAsDone: builder.mutation<IServerResponse<any>, string>({
query: taskId => ({
url: `${rootUrl}/update-personal-task`,
method: 'PUT',
body: { id: taskId },
}),
}),
getMyTasks: builder.query<IServerResponse<IHomeTasksModel>, IHomeTasksConfig>({
query: config => {
const { tasks_group_by, current_tab, is_calendar_view, selected_date, time_zone } = config;
const url = `${rootUrl}/tasks${toQueryString({
group_by: tasks_group_by,
current_tab,
is_calendar_view,
selected_date: selected_date?.toISOString().split('T')[0],
time_zone,
})}`;
return url;
},
}),
getProjects: builder.query<IServerResponse<IProject[]>, { view: number }>({
query: ({ view }) => `${rootUrl}/projects?view=${view}`,
}),
getProjectsByTeam: builder.query<IServerResponse<IProject[]>, void>({
query: () => `${rootUrl}/team-projects`,
}),
}),
});
export const {
useCreatePersonalTaskMutation,
useGetMyTasksQuery,
useGetPersonalTasksQuery,
useGetProjectsQuery,
useGetProjectsByTeamQuery,
useMarkPersonalTaskAsDoneMutation,
} = api;
export default api;

View File

@@ -0,0 +1,34 @@
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 { IMyTask } from '@/types/home/my-tasks.types';
import { IWorklenzNotification } from '@/types/notifications/notifications.types';
const rootUrl = `${API_BASE_URL}/notifications`;
export const notificationsApiService = {
getNotifications: async (filter: string): Promise<IServerResponse<IWorklenzNotification[]>> => {
const q = toQueryString({ filter });
const response = await apiClient.get<IServerResponse<IWorklenzNotification[]>>(
`${rootUrl}${q}`
);
return response.data;
},
updateNotification: async (id: string): Promise<IServerResponse<any>> => {
const response = await apiClient.put<IServerResponse<any>>(`${rootUrl}/${id}`);
return response.data;
},
readAllNotifications: async (): Promise<IServerResponse<any>> => {
const response = await apiClient.put<IServerResponse<any>>(`${rootUrl}/read-all`);
return response.data;
},
getUnreadCount: async (): Promise<IServerResponse<number>> => {
const response = await apiClient.get<IServerResponse<number>>(`${rootUrl}/unread-count`);
return response.data;
},
};

View File

@@ -0,0 +1,52 @@
import { IProjectMemberViewModel } from '@/types/projectMember.types';
import apiClient from '../api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/project-members`;
export const projectMembersApiService = {
createProjectMember: async (
body: IProjectMemberViewModel
): Promise<IServerResponse<IProjectMemberViewModel>> => {
const q = toQueryString({current_project_id: body.project_id});
const response = await apiClient.post<IServerResponse<IProjectMemberViewModel>>(
`${rootUrl}${q}`,
body
);
return response.data;
},
createByEmail: async (body: {
project_id: string;
email: string;
}): Promise<IServerResponse<IProjectMemberViewModel>> => {
const response = await apiClient.post<IServerResponse<IProjectMemberViewModel>>(
`${rootUrl}/invite`,
body
);
return response.data;
},
getByProjectId: async (
projectId: string
): Promise<IServerResponse<IProjectMemberViewModel[]>> => {
const response = await apiClient.get<IServerResponse<IProjectMemberViewModel[]>>(
`${rootUrl}/${projectId}`
);
return response.data;
},
deleteProjectMember: async (
id: string,
currentProjectId: string
): Promise<IServerResponse<IProjectMemberViewModel>> => {
const q = toQueryString({ current_project_id: currentProjectId });
const response = await apiClient.delete<IServerResponse<IProjectMemberViewModel>>(
`${rootUrl}/${id}${q}`
);
return response.data;
},
};

View File

@@ -0,0 +1,58 @@
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import {
IAccountSetupRequest,
IAccountSetupResponse,
IProjectTemplate,
IWorklenzTemplate,
} from '@/types/project-templates/project-templates.types';
import apiClient from '../api-client';
import { ICustomProjectTemplateCreateRequest } from '@/types/project/projectTemplate.types';
const rootUrl = `${API_BASE_URL}/project-templates`;
export const projectTemplatesApiService = {
getWorklenzTemplates: async (): Promise<IServerResponse<IWorklenzTemplate[]>> => {
const response = await apiClient.get(`${rootUrl}/worklenz-templates`);
return response.data;
},
getByTemplateId: async (templateId: string): Promise<IServerResponse<IProjectTemplate>> => {
const response = await apiClient.get(`${rootUrl}/worklenz-templates/${templateId}`);
return response.data;
},
getCustomTemplates: async (): Promise<IServerResponse<IWorklenzTemplate[]>> => {
const response = await apiClient.get(`${rootUrl}/custom-templates`);
return response.data;
},
setupAccount: async (
model: IAccountSetupRequest
): Promise<IServerResponse<IAccountSetupResponse>> => {
const response = await apiClient.post(`${rootUrl}/setup`, model);
return response.data;
},
createCustomTemplate: async (body: { template_id: string }): Promise<IServerResponse<IProjectTemplate>> => {
const response = await apiClient.post(`${rootUrl}/custom-template`, body);
return response.data;
},
deleteCustomTemplate: async (id: string): Promise<IServerResponse<void>> => {
const response = await apiClient.delete(`${rootUrl}/${id}`);
return response.data;
},
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;
},
};

View File

@@ -0,0 +1,63 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '@/api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import {
IMentionMemberSelectOption,
IMentionMemberViewModel,
IProjectCommentsCreateRequest,
} from '@/types/project/projectComments.types';
import { toQueryString } from '@/utils/toQueryString';
import { IProjectUpdateCommentViewModel } from '@/types/project/project.types';
const rootUrl = `${API_BASE_URL}/project-comments`;
export const projectCommentsApiService = {
createProjectComment: async (
body: IProjectCommentsCreateRequest
): Promise<IServerResponse<IProjectCommentsCreateRequest>> => {
const url = `${rootUrl}`;
const response = await apiClient.post<IServerResponse<IProjectCommentsCreateRequest>>(
`${url}`,
body
);
return response.data;
},
getMentionMembers: async (
projectId: string,
index: number,
size: number,
field: string | null,
order: string | null,
search: string | null
): Promise<IServerResponse<IMentionMemberViewModel[]>> => {
const s = encodeURIComponent(search || '');
const url = `${rootUrl}/project-members/${projectId}${toQueryString({ index, size, field, order, search: s })}`;
const response = await apiClient.get<IServerResponse<IMentionMemberViewModel[]>>(`${url}`);
return response.data;
},
getCountByProjectId: async (projectId: string): Promise<IServerResponse<number>> => {
const url = `${rootUrl}/${projectId}/comments/count`;
const response = await apiClient.get<IServerResponse<number>>(`${url}`);
return response.data;
},
getByProjectId: async (
projectId: string,
isLimit: boolean = false
): Promise<IServerResponse<IProjectUpdateCommentViewModel[]>> => {
const url = `${rootUrl}/project-comments/${projectId}${toQueryString({ latest: isLimit })}`;
const response = await apiClient.get<IServerResponse<IProjectUpdateCommentViewModel[]>>(
`${url}`
);
return response.data;
},
deleteComment: async (commentId: string): Promise<IServerResponse<string>> => {
const url = `${rootUrl}/delete/${commentId}`;
const response = await apiClient.delete<IServerResponse<string>>(`${url}`);
return response.data;
},
};

View File

@@ -0,0 +1,125 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '@/api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { toQueryString } from '@/utils/toQueryString';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { IDeadlineTaskStats } from '@/types/project/projectInsights.types';
import {
IInsightTasks,
IProjectInsightsGetRequest,
IProjectLogs,
IProjectMemberStats,
} from '@/types/project/projectInsights.types';
import { ITaskStatusCounts } from '@/types/project/project-insights.types';
const rootUrl = `${API_BASE_URL}/project-insights`;
export const projectInsightsApiService = {
getProjectInsights: async (id: string): Promise<IServerResponse<IProjectViewModel>> => {
const url = `${rootUrl}/${id}/insights`;
const response = await apiClient.get<IServerResponse<IProjectViewModel>>(`${url}`);
return response.data;
},
getProjectOverviewData: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<IProjectInsightsGetRequest>> => {
const url = `${rootUrl}/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<IProjectInsightsGetRequest>>(url);
return response.data;
},
getLastUpdatedTasks: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<IInsightTasks[]>> => {
const url = `${rootUrl}/last-updated/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<IInsightTasks[]>>(url);
return response.data;
},
getProjectLogs: async (id: string): Promise<IServerResponse<IProjectLogs[]>> => {
const url = `${rootUrl}/logs/${id}`;
const response = await apiClient.get<IServerResponse<IProjectLogs[]>>(url);
return response.data;
},
getTaskStatusCounts: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<ITaskStatusCounts[]>> => {
const url = `${rootUrl}/status-overview/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<ITaskStatusCounts[]>>(url);
return response.data;
},
getPriorityOverview: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<ITaskStatusCounts[]>> => {
const url = `${rootUrl}/priority-overview/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<ITaskStatusCounts[]>>(url);
return response.data;
},
getOverdueTasks: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<IInsightTasks[]>> => {
const url = `${rootUrl}/overdue-tasks/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<IInsightTasks[]>>(url);
return response.data;
},
getTasksCompletedEarly: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<IInsightTasks[]>> => {
const url = `${rootUrl}/early-tasks/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<IInsightTasks[]>>(url);
return response.data;
},
getTasksCompletedLate: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<IInsightTasks[]>> => {
const url = `${rootUrl}/late-tasks/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<IInsightTasks[]>>(url);
return response.data;
},
getMemberInsightAStats: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<IProjectMemberStats>> => {
const url = `${rootUrl}/members/stats/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<IProjectMemberStats>>(url);
return response.data;
},
getMemberTasks: async (body: any): Promise<IServerResponse<IInsightTasks[]>> => {
const url = `${rootUrl}/members/tasks`;
const response = await apiClient.post<IServerResponse<IInsightTasks[]>>(url, body);
return response.data;
},
getProjectDeadlineStats: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<IDeadlineTaskStats>> => {
const url = `${rootUrl}/deadline/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<IDeadlineTaskStats>>(url);
return response.data;
},
getOverloggedTasks: async (
id: string,
include_archived: boolean
): Promise<IServerResponse<IInsightTasks[]>> => {
const url = `${rootUrl}/overlogged-tasks/${id}?archived=${include_archived}`;
const response = await apiClient.get<IServerResponse<IInsightTasks[]>>(url);
return response.data;
},
};

View File

@@ -0,0 +1,13 @@
import { IServerResponse } from '@/types/common.types';
import { API_BASE_URL } from '@/shared/constants';
import { IProjectHealth } from '@/types/project/projectHealth.types';
import apiClient from '@api/api-client';
const rootUrl = `${API_BASE_URL}/project-healths`;
export const projectHealthApiService = {
getHealthOptions: async (): Promise<IServerResponse<IProjectHealth[]>> => {
const response = await apiClient.get<IServerResponse<IProjectHealth[]>>(rootUrl);
return response.data;
},
};

View File

@@ -0,0 +1,13 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '../../api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IProjectStatus } from '@/types/project/projectStatus.types';
const rootUrl = `${API_BASE_URL}/project-statuses`;
export const projectStatusesApiService = {
getStatuses: async (): Promise<IServerResponse<IProjectStatus[]>> => {
const response = await apiClient.get<IServerResponse<IProjectStatus[]>>(rootUrl);
return response.data;
},
};

View File

@@ -0,0 +1,121 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '../api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IProjectOverviewStats, IProjectsViewModel } from '@/types/project/projectsViewModel.types';
import { toQueryString } from '@/utils/toQueryString';
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';
const rootUrl = `${API_BASE_URL}/projects`;
export const projectsApiService = {
getProjects: async (
index: number,
size: number,
field: string | null,
order: string | null,
search: string | null,
filter: number | null = null,
statuses: string | null = null,
categories: string | null = null
): Promise<IServerResponse<IProjectsViewModel>> => {
const s = encodeURIComponent(search || '');
const url = `${rootUrl}${toQueryString({ index, size, field, order, search: s, filter, statuses, categories })}`;
const response = await apiClient.get<IServerResponse<IProjectsViewModel>>(`${url}`);
return response.data;
},
getProject: async (id: string): Promise<IServerResponse<IProjectViewModel>> => {
const url = `${rootUrl}/${id}`;
const response = await apiClient.get<IServerResponse<IProjectViewModel>>(`${url}`);
return response.data;
},
toggleFavoriteProject: async (id: string): Promise<IServerResponse<IProjectsViewModel>> => {
const url = `${rootUrl}/favorite/${id}`;
const response = await apiClient.get<IServerResponse<IProjectsViewModel>>(`${url}`);
return response.data;
},
getOverViewById: async (id: string): Promise<IServerResponse<IProjectOverviewStats>> => {
const url = `${rootUrl}/overview/${id}`;
const response = await apiClient.get<IServerResponse<IProjectOverviewStats>>(`${url}`);
return response.data;
},
getOverViewMembersById: async (
id: string,
archived = false
): Promise<IServerResponse<ITeamMemberOverviewGetResponse[]>> => {
const url = `${rootUrl}/overview-members/${id}?archived=${archived}`;
const response = await apiClient.get<IServerResponse<ITeamMemberOverviewGetResponse[]>>(
`${url}`
);
return response.data;
},
getMembers: async (
id: string,
index: number,
size: number,
field: string | null,
order: string | null,
search: string | null
): Promise<IServerResponse<IProjectMembersViewModel>> => {
const s = encodeURIComponent(search || '');
const url = `${rootUrl}/members/${id}${toQueryString({ index, size, field, order, search: s })}`;
const response = await apiClient.get<IServerResponse<IProjectMembersViewModel>>(`${url}`);
return response.data;
},
createProject: async (
project: IProjectViewModel
): Promise<IServerResponse<IProjectViewModel>> => {
const url = `${rootUrl}`;
const response = await apiClient.post<IServerResponse<IProjectViewModel>>(`${url}`, project);
return response.data;
},
updateProject: async (
id: string,
project: IProjectViewModel
): Promise<IServerResponse<IProjectViewModel>> => {
const q = toQueryString({ current_project_id: id });
const url = `${rootUrl}/${id}${q}`;
const response = await apiClient.put<IServerResponse<IProjectViewModel>>(`${url}`, project);
return response.data;
},
deleteProject: async (id: string): Promise<IServerResponse<IProjectViewModel>> => {
const url = `${rootUrl}/${id}`;
const response = await apiClient.delete<IServerResponse<IProjectViewModel>>(`${url}`);
return response.data;
},
toggleArchiveProject: async (id: string): Promise<IServerResponse<any>> => {
const url = `${rootUrl}/archive/${id}`;
const response = await apiClient.get<IServerResponse<IProjectViewModel>>(`${url}`);
return response.data;
},
toggleArchiveProjectForAll: async (id: string): Promise<IServerResponse<any>> => {
const url = `${rootUrl}/archive-all/${id}`;
const response = await apiClient.get<IServerResponse<IProjectViewModel>>(`${url}`);
return response.data;
},
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;
},
getProjectManagers: async (): Promise<IServerResponse<IProjectManager[]>> => {
const url = `${API_BASE_URL}/project-managers`;
const response = await apiClient.get<IServerResponse<IProjectManager[]>>(`${url}`);
return response.data;
},
};

View File

@@ -0,0 +1,153 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { API_BASE_URL } from '@/shared/constants';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
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';
const rootUrl = '/projects';
export const projectsApi = createApi({
reducerPath: 'projectsApi',
baseQuery: fetchBaseQuery({
baseUrl: `${import.meta.env.VITE_API_URL}${API_BASE_URL}`,
prepareHeaders: headers => {
headers.set('X-CSRF-Token', getCsrfToken() || '');
headers.set('Content-Type', 'application/json');
},
credentials: 'include',
}),
tagTypes: ['Projects', 'ProjectCategories', 'ProjectMembers'],
endpoints: builder => ({
getProjects: builder.query<
IServerResponse<IProjectsViewModel>,
{
index: number;
size: number;
field: string | null;
order: string | null;
search: string | null;
filter: number | null;
statuses: string | null;
categories: string | null;
}
>({
query: ({ index, size, field, order, search, filter, statuses, categories }) => {
const params = new URLSearchParams({
index: index.toString(),
size: size.toString(),
field: field || '',
order: order || '',
search: search || '',
filter: filter?.toString() || '',
statuses: statuses || '',
categories: categories || '',
});
return `${rootUrl}?${params.toString()}`;
},
providesTags: result => [{ type: 'Projects', id: 'LIST' }],
}),
getProject: builder.query<IServerResponse<IProjectViewModel>, string>({
query: id => `${rootUrl}/${id}`,
providesTags: (result, error, id) => [{ type: 'Projects', id }],
}),
createProject: builder.mutation<IServerResponse<IProjectViewModel>, IProjectViewModel>({
query: project => ({
url: rootUrl,
method: 'POST',
body: project,
}),
invalidatesTags: [{ type: 'Projects', id: 'LIST' }],
}),
updateProject: builder.mutation<
IServerResponse<IProjectViewModel>,
{ id: string; project: IProjectViewModel }
>({
query: ({ id, project }) => ({
url: `${rootUrl}/${id}`,
method: 'PUT',
body: project,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Projects', id }],
}),
deleteProject: builder.mutation<IServerResponse<IProjectViewModel>, string>({
query: id => ({
url: `${rootUrl}/${id}`,
method: 'DELETE',
}),
invalidatesTags: [{ type: 'Projects', id: 'LIST' }],
}),
toggleFavoriteProject: builder.mutation<IServerResponse<IProjectsViewModel>, string>({
query: id => ({
url: `${rootUrl}/favorite/${id}`,
method: 'GET',
}),
invalidatesTags: (result, error, id) => [{ type: 'Projects', id }],
}),
toggleArchiveProject: builder.mutation<IServerResponse<any>, string>({
query: id => ({
url: `${rootUrl}/archive/${id}`,
method: 'GET',
}),
invalidatesTags: [{ type: 'Projects', id: 'LIST' }],
}),
toggleArchiveProjectForAll: builder.mutation<IServerResponse<any>, string>({
query: id => ({
url: `${rootUrl}/archive-all/${id}`,
method: 'GET',
}),
invalidatesTags: [{ type: 'Projects', id: 'LIST' }],
}),
getProjectCategories: builder.query<IProjectCategory[], void>({
query: () => `${rootUrl}/categories`,
providesTags: ['ProjectCategories'],
}),
getProjectMembers: builder.query<
IServerResponse<IProjectMembersViewModel>,
{
id: string;
index: number;
size: number;
field: string | null;
order: string | null;
search: string | null;
}
>({
query: ({ id, index, size, field, order, search }) => {
const params = new URLSearchParams({
index: index.toString(),
size: size.toString(),
field: field || '',
order: order || '',
search: search || '',
});
return `${rootUrl}/members/${id}?${params.toString()}`;
},
providesTags: (result, error, { id }) => [{ type: 'ProjectMembers', id }],
}),
}),
});
export const {
useGetProjectsQuery,
useGetProjectQuery,
useCreateProjectMutation,
useUpdateProjectMutation,
useDeleteProjectMutation,
useToggleFavoriteProjectMutation,
useToggleArchiveProjectMutation,
useToggleArchiveProjectForAllMutation,
useGetProjectCategoriesQuery,
useGetProjectMembersQuery,
} = projectsApi;

View File

@@ -0,0 +1,175 @@
import { toQueryString } from '@/utils/toQueryString';
import { API_BASE_URL } from '@/shared/constants';
import { ITimeLogBreakdownReq } from '@/types/reporting/reporting.types';
const rootUrl = `${import.meta.env.VITE_API_URL}${API_BASE_URL}/reporting-export`;
export const reportingExportApiService = {
exportOverviewProjectsByTeam(teamId: string, teamName: string) {
const params = toQueryString({
team_id: teamId,
team_name: teamName,
});
window.location.href = `${rootUrl}/overview/projects${params}`;
},
exportOverviewMembersByTeam(teamId: string, teamName: string) {
const params = toQueryString({
team_id: teamId,
team_name: teamName,
});
window.location.href = `${rootUrl}/overview/members${params}`;
},
exportAllocation(
archived: boolean,
teams: string[],
projects: string[],
duration: string | undefined,
date_range: string[],
billable = true,
nonBillable = true
) {
const teamsString = teams?.join(',');
const projectsString = projects.join(',');
window.location.href = `${rootUrl}/allocation/export${toQueryString({
teams: teamsString,
projects: projectsString,
duration: duration,
date_range: date_range,
include_archived: archived,
billable,
nonBillable,
})}`;
},
exportProjects(teamName: string | undefined) {
const params = toQueryString({
team_name: teamName,
});
window.location.href = `${rootUrl}/projects/export${params}`;
},
exportMembers(
teamName: string | undefined,
duration: string | null | undefined,
date_range: string[] | null,
archived: boolean
) {
const params = toQueryString({
team_name: teamName,
duration: duration,
date_range: date_range,
archived: archived,
});
window.location.href = `${rootUrl}/members/export${params}`;
},
exportProjectMembers(
projectId: string,
projectName: string,
teamName: string | null | undefined
) {
const params = toQueryString({
project_id: projectId,
project_name: projectName,
team_name: teamName ? teamName : null,
});
window.location.href = `${rootUrl}/project-members/export${params}`;
},
exportProjectTasks(projectId: string, projectName: string, teamName: string | null | undefined) {
const params = toQueryString({
project_id: projectId,
project_name: projectName,
team_name: teamName ? teamName : null,
});
window.location.href = `${rootUrl}/project-tasks/export${params}`;
},
exportMemberProjects(
memberId: string,
teamId: string | null,
memberName: string,
teamName: string | null | undefined,
archived: boolean
) {
const params = toQueryString({
team_member_id: memberId,
team_id: teamId,
team_member_name: memberName,
team_name: teamName ? teamName : null,
archived: archived,
});
window.location.href = `${rootUrl}/member-projects/export${params}`;
},
exportMemberTasks(
memberId: string,
memberName: string,
teamName: string | null | undefined,
body: any | null
) {
const params = toQueryString({
team_member_id: memberId,
team_member_name: memberName,
team_name: teamName ? teamName : null,
duration: body.duration,
date_range: body.date_range,
only_single_member: body.only_single_member ? body.only_single_member : false,
archived: body.archived ? body.archived : false,
});
window.location.href = `${rootUrl}/member-tasks/export${params}`;
},
exportFlatTasks(
memberId: string,
memberName: string,
projectId: string | null,
projectName: string | null
) {
const params = toQueryString({
team_member_id: memberId,
team_member_name: memberName,
project_id: projectId,
project_name: projectName,
});
window.location.href = `${rootUrl}/flat-tasks/export${params}`;
},
exportProjectTimeLogs(body: ITimeLogBreakdownReq, projectName: string) {
const params = toQueryString({
id: body.id,
duration: body.duration,
date_range: body.date_range,
project_name: projectName,
});
window.location.href = `${rootUrl}/projects-time-log-breakdown/export${params}`;
},
exportMemberTimeLogs(body: any | null) {
const params = toQueryString({
team_member_id: body.team_member_id,
team_id: body.team_id,
duration: body.duration,
date_range: body.date_range,
member_name: body.member_name,
team_name: body.team_name,
archived: body.archived ? body.archived : false,
});
window.location.href = `${rootUrl}/member-time-log-breakdown/export${params}`;
},
exportMemberActivityLogs(body: any | null) {
const params = toQueryString({
team_member_id: body.team_member_id,
team_id: body.team_id,
duration: body.duration,
date_range: body.date_range,
member_name: body.member_name,
team_name: body.team_name,
archived: body.archived ? body.archived : false,
});
window.location.href = `${rootUrl}/member-activity-log-breakdown/export${params}`;
},
};

View File

@@ -0,0 +1,18 @@
import { IServerResponse } from '@/types/common.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';
const rootUrl = `${API_BASE_URL}/reporting/members`;
export const reportingMembersApiService = {
getMembers: async (
body: any
): Promise<IServerResponse<IRPTMembersViewModel>> => {
const q = toQueryString(body);
const url = `${rootUrl}${q}`;
const response = await apiClient.get<IServerResponse<IRPTMembersViewModel>>(url);
return response.data;
},
};

View File

@@ -0,0 +1,43 @@
import { IServerResponse } from '@/types/common.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';
import { ITaskListGroup } from '@/types/tasks/taskList.types';
const rootUrl = `${API_BASE_URL}/reporting/projects`;
export const reportingProjectsApiService = {
getProjects: async (
body: IGetProjectsRequestBody
): Promise<IServerResponse<IRPTProjectsViewModel>> => {
const q = toQueryString(body);
const url = `${rootUrl}${q}`;
const response = await apiClient.get<IServerResponse<IRPTProjectsViewModel>>(url);
return response.data;
},
getProjectOverview: async (
projectId: string
): Promise<IServerResponse<IRPTOverviewProjectInfo>> => {
const url = `${API_BASE_URL}/reporting/overview/project/info/${projectId}`;
const response = await apiClient.get<IServerResponse<IRPTOverviewProjectInfo>>(url);
return response.data;
},
getProjectMembers: async (
projectId: string
): Promise<IServerResponse<IRPTOverviewProjectMember[]>> => {
const url = `${API_BASE_URL}/reporting/overview/project/members/${projectId}`;
const response = await apiClient.get<IServerResponse<IRPTOverviewProjectMember[]>>(url);
return response.data;
},
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);
return response.data;
},
};

View File

@@ -0,0 +1,301 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '../api-client';
import { API_BASE_URL } from '@/shared/constants';
import { toQueryString } from '@/utils/toQueryString';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import {
IProjectLogsBreakdown,
IRPTMember,
IRPTOverviewMemberInfo,
IRPTOverviewProjectInfo,
IRPTOverviewProjectMember,
IRPTOverviewStatistics,
IRPTOverviewTeamInfo,
IRPTProject,
IRPTProjectsViewModel,
IRPTReportingMemberTask,
IRPTTeam,
ITimeLogBreakdownReq,
} from '@/types/reporting/reporting.types';
import { IReportingInfo } from '@/types/reporting/reporting.types';
import {
IMemberProjectsResonse,
IMemberTaskStatGroupResonse,
IRPTMemberProject,
IRPTMemberResponse,
IRPTTimeMember,
IRPTTimeProject,
ISingleMemberActivityLogs,
ISingleMemberLogs,
} from '@/types/reporting/reporting.types';
import { ITaskListGroup } from '@/types/tasks/taskList.types';
import {
ISelectableCategory,
ISelectableProject,
} from '../../types/reporting/reporting-filters.types';
import { IAllocationViewModel } from '@/types/reporting/reporting-allocation.types';
const rootUrl = `${API_BASE_URL}/reporting`;
export const reportingApiService = {
getProject: async (id: string): Promise<IServerResponse<IProjectViewModel>> => {
const url = `${rootUrl}/${id}`;
const response = await apiClient.get<IServerResponse<IProjectViewModel>>(`${url}`);
return response.data;
},
getInfo: async (): Promise<IServerResponse<IReportingInfo>> => {
const url = `${rootUrl}/info`;
const response = await apiClient.get<IServerResponse<IReportingInfo>>(url);
return response.data;
},
getOverviewStatistics: async (
includeArchived = false
): Promise<IServerResponse<IRPTOverviewStatistics>> => {
const q = toQueryString({ archived: includeArchived });
const url = `${rootUrl}/overview/statistics${q}`;
const response = await apiClient.get<IServerResponse<IRPTOverviewStatistics>>(url);
return response.data;
},
getOverviewTeams: async (includeArchived = true): Promise<IServerResponse<IRPTTeam[]>> => {
const q = toQueryString({ archived: includeArchived });
const url = `${rootUrl}/overview/teams${q}`;
const response = await apiClient.get<IServerResponse<IRPTTeam[]>>(url);
return response.data;
},
getOverviewProjects: async (
body: any | null = null
): Promise<IServerResponse<IRPTProjectsViewModel>> => {
const q = toQueryString(body);
const url = `${rootUrl}/overview/projects${q}`;
const response = await apiClient.get<IServerResponse<IRPTProjectsViewModel>>(url);
return response.data;
},
getOverviewProjectsByTeam: async (
teamId: string,
teamMemberId?: string
): Promise<IServerResponse<IRPTProject[]>> => {
const q = toQueryString({ member: teamMemberId || null });
const url = `${rootUrl}/overview/projects/${teamId}${q}`;
const response = await apiClient.get<IServerResponse<IRPTProject[]>>(url);
return response.data;
},
getOverviewMembersByTeam: async (
teamId: string,
archived: boolean
): Promise<IServerResponse<IRPTMember[]>> => {
const q = toQueryString({ archived });
const url = `${rootUrl}/overview/members/${teamId}${q}`;
const response = await apiClient.get<IServerResponse<IRPTMember[]>>(url);
return response.data;
},
getTeamInfo: async (
teamId: string,
archived = false
): Promise<IServerResponse<IRPTOverviewTeamInfo>> => {
const q = toQueryString({ archived });
const url = `${rootUrl}/overview/team/info/${teamId}${q}`;
const response = await apiClient.get<IServerResponse<IRPTOverviewTeamInfo>>(url);
return response.data;
},
getProjectInfo: async (projectId: string): Promise<IServerResponse<IRPTOverviewProjectInfo>> => {
const url = `${rootUrl}/overview/project/info/${projectId}`;
const response = await apiClient.get<IServerResponse<IRPTOverviewProjectInfo>>(url);
return response.data;
},
getMemberInfo: async (
body: any | null = null
): Promise<IServerResponse<IRPTOverviewMemberInfo>> => {
const q = toQueryString(body);
const url = `${rootUrl}/overview/member/info/${q}`;
const response = await apiClient.get<IServerResponse<IRPTOverviewMemberInfo>>(url);
return response.data;
},
getTeamMemberInfo: async (
body: any | null = null
): Promise<IServerResponse<IRPTOverviewMemberInfo>> => {
const q = toQueryString(body);
const url = `${rootUrl}/overview/team-member/info/${q}`;
const response = await apiClient.get<IServerResponse<IRPTOverviewMemberInfo>>(url);
return response.data;
},
getProjectMembers: async (
projectId: string
): Promise<IServerResponse<IRPTOverviewProjectMember[]>> => {
const url = `${rootUrl}/overview/project/members/${projectId}`;
const response = await apiClient.get<IServerResponse<IRPTOverviewProjectMember[]>>(url);
return response.data;
},
getTasks: async (
projectId: string,
groupBy: string
): Promise<IServerResponse<ITaskListGroup[]>> => {
const q = toQueryString({ group: groupBy });
const url = `${rootUrl}/overview/project/tasks/${projectId}${q}`;
const response = await apiClient.get<IServerResponse<ITaskListGroup[]>>(url);
return response.data;
},
getTasksByMember: async (
teamMemberId: string,
projectId: string | null = null,
isMultiple: boolean,
teamId: string | null = null,
additionalBody: any | null = null
): Promise<IServerResponse<IRPTReportingMemberTask[]>> => {
const q = toQueryString({
project: projectId || null,
is_multiple: isMultiple,
teamId,
only_single_member: additionalBody.only_single_member,
duration: additionalBody.duration,
date_range: additionalBody.date_range,
archived: additionalBody.archived,
});
const url = `${rootUrl}/overview/member/tasks/${teamMemberId}${q}`;
const response = await apiClient.get<IServerResponse<IRPTReportingMemberTask[]>>(url);
return response.data;
},
getProjects: async (body: any | null = null): Promise<IServerResponse<IRPTProjectsViewModel>> => {
const q = toQueryString(body);
const url = `${rootUrl}/projects${q}`;
const response = await apiClient.get<IServerResponse<IRPTProjectsViewModel>>(url);
return response.data;
},
getProjectTimeLogs: async (
body: ITimeLogBreakdownReq
): Promise<IServerResponse<IProjectLogsBreakdown[]>> => {
const url = `${rootUrl}/project-timelogs`;
const response = await apiClient.post<IServerResponse<IProjectLogsBreakdown[]>>(url, body);
return response.data;
},
getCategories: async (
selectedTeams: string[]
): Promise<IServerResponse<ISelectableCategory[]>> => {
const url = `${rootUrl}/allocation/categories`;
const response = await apiClient.post<IServerResponse<ISelectableCategory[]>>(
url,
selectedTeams
);
return response.data;
},
getAllocationProjects: async (
selectedTeams: string[],
categories: string[],
isNoCategory: boolean
): Promise<IServerResponse<ISelectableProject[]>> => {
const body = {
selectedTeams: selectedTeams,
selectedCategories: categories,
noCategoryIncluded: isNoCategory,
};
const url = `${rootUrl}/allocation/projects`;
const response = await apiClient.post<IServerResponse<ISelectableProject[]>>(url, body);
return response.data;
},
getAllocationData: async (
body = {},
archived = false
): Promise<IServerResponse<IAllocationViewModel>> => {
const q = toQueryString({ archived });
const url = `${rootUrl}/allocation${q}`;
const response = await apiClient.post<IServerResponse<IAllocationViewModel>>(url, body);
return response.data;
},
getMembers: async (body: any | null = null): Promise<IServerResponse<IRPTMemberResponse>> => {
const q = toQueryString(body);
const url = `${rootUrl}/members${q}`;
const response = await apiClient.get<IServerResponse<IRPTMemberResponse>>(url);
return response.data;
},
getMemberProjects: async (
body: any | null = null
): Promise<IServerResponse<IRPTMemberProject[]>> => {
const q = toQueryString(body);
const url = `${rootUrl}/member-projects${q}`;
const response = await apiClient.get<IServerResponse<IRPTMemberProject[]>>(url);
return response.data;
},
getProjectTimeSheets: async (
body = {},
archived = false
): Promise<IServerResponse<IRPTTimeProject[]>> => {
const q = toQueryString({ archived });
const url = `${rootUrl}/time-reports/projects${q}`;
const response = await apiClient.post<IServerResponse<IRPTTimeProject[]>>(url, body);
return response.data;
},
getProjectEstimatedVsActual: async (
body = {},
archived = false
): Promise<IServerResponse<IRPTTimeProject[]>> => {
const q = toQueryString({ archived });
const url = `${rootUrl}/time-reports/estimated-vs-actual${q}`;
const response = await apiClient.post<IServerResponse<IRPTTimeProject[]>>(url, body);
return response.data;
},
getMemberTimeSheets: async (
body = {},
archived = false
): Promise<IServerResponse<IRPTTimeMember[]>> => {
const q = toQueryString({ archived });
const url = `${rootUrl}/time-reports/members${q}`;
const response = await apiClient.post<IServerResponse<IRPTTimeMember[]>>(url, body);
return response.data;
},
getSingleMemberActivities: async (
body: any | null = null
): Promise<IServerResponse<ISingleMemberActivityLogs[]>> => {
const url = `${rootUrl}/members/single-member-activities`;
const response = await apiClient.post<IServerResponse<ISingleMemberActivityLogs[]>>(url, body);
return response.data;
},
getSingleMemberTimeLogs: async (
body: any | null = null
): Promise<IServerResponse<ISingleMemberLogs[]>> => {
const url = `${rootUrl}/members/single-member-timelogs`;
const response = await apiClient.post<IServerResponse<ISingleMemberLogs[]>>(url, body);
return response.data;
},
getMemberTasksStats: async (
body: any | null = null
): Promise<IServerResponse<IMemberTaskStatGroupResonse>> => {
const q = toQueryString(body);
const url = `${rootUrl}/members/single-member-task-stats${q}`;
const response = await apiClient.get<IServerResponse<IMemberTaskStatGroupResonse>>(url);
return response.data;
},
getSingleMemberProjects: async (
body: any | null = null
): Promise<IServerResponse<IMemberProjectsResonse>> => {
const q = toQueryString(body);
const url = `${rootUrl}/members/single-member-projects${q}`;
const response = await apiClient.get<IServerResponse<IMemberProjectsResonse>>(url);
return response.data;
},
};

View File

@@ -0,0 +1,44 @@
import { API_BASE_URL } from '@/shared/constants';
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';
const rootUrl = `${API_BASE_URL}/reporting`;
export const reportingTimesheetApiService = {
getTimeSheetData: async (body = {}, archived = false): Promise<IServerResponse<IAllocationViewModel>> => {
const q = toQueryString({ archived });
const response = await apiClient.post(`${rootUrl}/allocation/${q}`, body);
return response.data;
},
getAllocationProjects: async (body = {}) => {
const response = await apiClient.post(`${rootUrl}/allocation/allocation-projects`, { body });
return response.data;
},
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[]>> => {
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[]>> => {
const response = await apiClient.post(`${rootUrl}/project-timelogs`, body);
return response.data;
},
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

@@ -0,0 +1,60 @@
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';
const rootUrl = `${API_BASE_URL}/schedule-gannt-v2`;
export const scheduleAPIService = {
fetchScheduleSettings: async (): Promise<IServerResponse<Settings>> => {
const response = await apiClient.get<IServerResponse<Settings>>(`${rootUrl}/settings`);
return response.data;
},
updateScheduleSettings: async ({
workingDays,
workingHours,
}: {
workingDays: string[];
workingHours: number;
}): Promise<IServerResponse<any>> => {
const response = await apiClient.put<IServerResponse<any>>(`${rootUrl}/settings`, {
workingDays,
workingHours,
});
return response.data;
},
fetchScheduleDates: async ({
type,
date,
}: {
type: string;
date: string;
}): Promise<IServerResponse<DateList>> => {
const response = await apiClient.get<IServerResponse<DateList>>(
`${rootUrl}/dates/${date}/${type}`
);
return response.data;
},
fetchScheduleMembers: async (): Promise<IServerResponse<Member[]>> => {
const response = await apiClient.get<IServerResponse<Member[]>>(`${rootUrl}/members`);
return response.data;
},
fetchMemberProjects: async ({ id }: { id: string }): Promise<IServerResponse<Project>> => {
const response = await apiClient.get<IServerResponse<Project>>(`${rootUrl}/members/projects/${id}`);
return response.data;
},
submitScheduleData: async ({
schedule
}: {
schedule: ScheduleData
}): Promise<IServerResponse<any>> => {
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/schedule`, schedule);
return response.data;
}
};

View File

@@ -0,0 +1,54 @@
import { IServerResponse } from '@/types/common.types';
import { API_BASE_URL } from '@/shared/constants';
import { IProjectCategory, IProjectCategoryViewModel } from '@/types/project/projectCategory.types';
import apiClient from '@api/api-client';
const rootUrl = `${API_BASE_URL}/project-categories`;
export const categoriesApiService = {
getCategories: async (): Promise<IServerResponse<IProjectCategoryViewModel[]>> => {
const response = await apiClient.get<IServerResponse<IProjectCategoryViewModel[]>>(rootUrl);
return response.data;
},
getCategoriesByTeam: async (
id: string
): Promise<IServerResponse<IProjectCategoryViewModel[]>> => {
const response = await apiClient.get<IServerResponse<IProjectCategoryViewModel[]>>(
`${rootUrl}/${id}`
);
return response.data;
},
getCategoriesByOrganization: async (): Promise<IServerResponse<IProjectCategoryViewModel[]>> => {
const response = await apiClient.get<IServerResponse<IProjectCategoryViewModel[]>>(
`${rootUrl}/org-categories`
);
return response.data;
},
createCategory: async (
category: Partial<IProjectCategory>
): Promise<IServerResponse<IProjectCategoryViewModel>> => {
const response = await apiClient.post<IServerResponse<IProjectCategoryViewModel>>(
rootUrl,
category
);
return response.data;
},
updateCategory: async (
category: IProjectCategoryViewModel
): Promise<IServerResponse<IProjectCategoryViewModel>> => {
const response = await apiClient.put<IServerResponse<IProjectCategoryViewModel>>(
`${rootUrl}/${category.id}`,
category
);
return response.data;
},
deleteCategory: async (id: string): Promise<IServerResponse<string>> => {
const response = await apiClient.delete<IServerResponse<string>>(`${rootUrl}/${id}`);
return response.data;
},
};

View File

@@ -0,0 +1,44 @@
import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { IJobTitle, IJobTitlesViewModel } from '@/types/job.types';
import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/job-titles`;
export const jobTitlesApiService = {
async getJobTitles(
index: number,
size: number,
field: string | null,
order: string | null,
search?: string | null
): Promise<IServerResponse<IJobTitlesViewModel>> {
const s = encodeURIComponent(search || '');
const queryString = toQueryString({ index, size, field, order, search: s });
const response = await apiClient.get<IServerResponse<IJobTitlesViewModel>>(
`${rootUrl}${queryString}`
);
return response.data;
},
async getJobTitleById(id: string): Promise<IServerResponse<IJobTitle>> {
const response = await apiClient.get<IServerResponse<IJobTitle>>(`${rootUrl}/${id}`);
return response.data;
},
async createJobTitle(body: IJobTitle): Promise<IServerResponse<IJobTitle>> {
const response = await apiClient.post<IServerResponse<IJobTitle>>(rootUrl, body);
return response.data;
},
async updateJobTitle(id: string, body: IJobTitle): Promise<IServerResponse<IJobTitle>> {
const response = await apiClient.put<IServerResponse<IJobTitle>>(`${rootUrl}/${id}`, body);
return response.data;
},
async deleteJobTitle(id: string): Promise<IServerResponse<void>> {
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/${id}`);
return response.data;
},
};

View File

@@ -0,0 +1,39 @@
import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { ITaskLabel } from '@/types/label.type';
import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/labels`;
export const labelsApiService = {
getLabels: async (projectId: string | null = null): Promise<IServerResponse<ITaskLabel>> => {
const q = toQueryString({ project: projectId });
const response = await apiClient.get<IServerResponse<ITaskLabel>>(`${rootUrl}${q}`);
return response.data;
},
getLabelsByTaskId: async (taskId: string): Promise<IServerResponse<ITaskLabel>> => {
const response = await apiClient.get<IServerResponse<ITaskLabel>>(`${rootUrl}/tasks/${taskId}`);
return response.data;
},
getByProjectId: async (projectId: string): Promise<IServerResponse<ITaskLabel>> => {
const response = await apiClient.get<IServerResponse<ITaskLabel>>(
`${rootUrl}/project/${projectId}`
);
return response.data;
},
updateColor: async (id: string, color: string): Promise<IServerResponse<ITaskLabel>> => {
const response = await apiClient.put<IServerResponse<ITaskLabel>>(`${rootUrl}/tasks/${id}`, {
color,
});
return response.data;
},
deleteLabel: async (id: string): Promise<IServerResponse<void>> => {
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/${id}`);
return response.data;
},
};

View File

@@ -0,0 +1,18 @@
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { ITimezone } from '@/types/settings/timezone.types';
import apiClient from '@api/api-client';
const rootUrl = `${API_BASE_URL}/timezones`;
export const timezonesApiService = {
update: async <T>(body: any): Promise<IServerResponse<ITimezone[]>> => {
const response = await apiClient.put<IServerResponse<ITimezone[]>>(rootUrl, body);
return response.data;
},
get: async <T>(): Promise<IServerResponse<ITimezone[]>> => {
const response = await apiClient.get<IServerResponse<ITimezone[]>>(rootUrl);
return response.data;
},
};

View File

@@ -0,0 +1,71 @@
import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { IProfileSettings } from '@/types/settings/profile.types';
import { INotificationSettings } from '@/types/settings/notifications.types';
import {
IAccountSetupRequest,
IAccountSetupResponse,
} from '@/types/project-templates/project-templates.types';
import { ITeam } from '@/types/teams/team.type';
const rootUrl = `${API_BASE_URL}/settings`;
export const profileSettingsApiService = {
getProfile: async (): Promise<IServerResponse<IProfileSettings>> => {
const response = await apiClient.get<IServerResponse<IProfileSettings>>(`${rootUrl}/profile`);
return response.data;
},
updateProfile: async (body: IProfileSettings): Promise<IServerResponse<IProfileSettings>> => {
const response = await apiClient.put<IServerResponse<IProfileSettings>>(
`${rootUrl}/profile`,
body
);
return response.data;
},
getNotificationSettings: async (): Promise<IServerResponse<INotificationSettings>> => {
const response = await apiClient.get<IServerResponse<INotificationSettings>>(
`${rootUrl}/notifications`
);
return response.data;
},
updateNotificationSettings: async (
body: INotificationSettings
): Promise<IServerResponse<INotificationSettings>> => {
const response = await apiClient.put<IServerResponse<INotificationSettings>>(
`${rootUrl}/notifications`,
body
);
return response.data;
},
setupAccount: async (
body: IAccountSetupRequest
): Promise<IServerResponse<IAccountSetupResponse>> => {
const response = await apiClient.post<IServerResponse<IAccountSetupResponse>>(
`${rootUrl}/setup`,
body
);
return response.data;
},
updateTeamName: async (id: string, body: ITeam): Promise<IServerResponse<ITeam>> => {
const response = await apiClient.put<IServerResponse<ITeam>>(`${rootUrl}/team-name/${id}`, body);
return response.data;
},
changePassword: async (body: {
new_password: string;
confirm_password: string;
password: string;
}): Promise<IServerResponse<any>> => {
const response = await apiClient.post<IServerResponse<any>>(
`${API_BASE_URL}/change-password`,
body
);
return response.data;
},
};

View File

@@ -0,0 +1,44 @@
import { API_BASE_URL } from '@/shared/constants';
import apiClient from '../api-client';
import { IServerResponse } from '@/types/common.types';
import {
ITaskTemplateGetResponse,
ITaskTemplatesGetResponse,
} from '@/types/settings/task-templates.types';
import { ITask } from '@/types/tasks/task.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
const rootUrl = `${API_BASE_URL}/task-templates`;
export const taskTemplatesApiService = {
getTemplates: async (): Promise<IServerResponse<ITaskTemplatesGetResponse[]>> => {
const url = `${rootUrl}`;
const response = await apiClient.get<IServerResponse<ITaskTemplatesGetResponse[]>>(`${url}`);
return response.data;
},
getTemplate: async (id: string): Promise<IServerResponse<ITaskTemplateGetResponse>> => {
const url = `${rootUrl}/${id}`;
const response = await apiClient.get<IServerResponse<ITaskTemplateGetResponse>>(`${url}`);
return response.data;
},
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>> => {
const url = `${rootUrl}/${id}`;
const response = await apiClient.put<IServerResponse<ITask>>(`${url}`, body);
return response.data;
},
deleteTemplate: async (id: string): Promise<IServerResponse<ITask>> => {
const url = `${rootUrl}/${id}`;
const response = await apiClient.delete<IServerResponse<ITask>>(`${url}`);
return response.data;
},
importTemplate: async (id: string, body: IProjectTask[]): Promise<IServerResponse<ITask>> => {
const url = `${rootUrl}/import/${id}`;
const response = await apiClient.post<IServerResponse<ITask>>(`${url}`, body);
return response.data;
},
};

View File

@@ -0,0 +1,40 @@
import { IServerResponse } from '@/types/common.types';
import { API_BASE_URL } from '@/shared/constants';
import apiClient from '@api/api-client';
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
const rootUrl = `${API_BASE_URL}/labels`;
export const labelsApiService = {
getLabels: async (): Promise<IServerResponse<ITaskLabel[]>> => {
const response = await apiClient.get<IServerResponse<ITaskLabel[]>>(`${rootUrl}`);
return response.data;
},
getPriorityByTask: async (taskId: string): Promise<IServerResponse<ITaskLabel[]>> => {
const response = await apiClient.get<IServerResponse<ITaskLabel[]>>(
`${rootUrl}/tasks/${taskId}`
);
return response.data;
},
getPriorityByProject: async (projectId: string): Promise<IServerResponse<ITaskLabel[]>> => {
const response = await apiClient.get<IServerResponse<ITaskLabel[]>>(
`${rootUrl}/project/${projectId}`
);
return response.data;
},
updateColor: async (labelId: string, color: string): Promise<IServerResponse<ITaskLabel>> => {
const response = await apiClient.put<IServerResponse<ITaskLabel>>(
`${rootUrl}/tasks/${labelId}/color`,
{ color }
);
return response.data;
},
deleteById: async (labelId: string): Promise<IServerResponse<void>> => {
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/team/${labelId}`);
return response.data;
},
};

View File

@@ -0,0 +1,72 @@
import apiClient from '@/api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/task-phases`;
interface UpdateSortOrderBody {
from_index: number;
to_index: number;
phases: ITaskPhase[];
project_id: string;
}
export const phasesApiService = {
addPhaseOption: async (projectId: string) => {
const q = toQueryString({ id: projectId, current_project_id: projectId });
const response = await apiClient.post<IServerResponse<ITaskPhase>>(`${rootUrl}${q}`);
return response.data;
},
getPhasesByProjectId: async (projectId: string) => {
const q = toQueryString({ id: projectId });
const response = await apiClient.get<IServerResponse<ITaskPhase[]>>(`${rootUrl}${q}`);
return response.data;
},
deletePhaseOption: async (phaseOptionId: string, projectId: string) => {
const q = toQueryString({ id: projectId, current_project_id: projectId });
const response = await apiClient.delete<IServerResponse<ITaskPhase>>(
`${rootUrl}/${phaseOptionId}${q}`
);
return response.data;
},
updatePhaseColor: async (projectId: string, body: ITaskPhase) => {
const q = toQueryString({ id: projectId, current_project_id: projectId });
const response = await apiClient.put<IServerResponse<ITaskPhase>>(
`${rootUrl}/change-color/${body.id}${q}`,
body
);
return response.data;
},
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}`,
body
);
return response.data;
},
updatePhaseOrder: async (projectId: string, body: UpdateSortOrderBody) => {
const q = toQueryString({ id: projectId, current_project_id: projectId });
const response = await apiClient.put<IServerResponse<ITaskPhase[]>>(
`${rootUrl}/update-sort-order${q}`,
body
);
return response.data;
},
updateProjectPhaseLabel: async (projectId: string, phaseLabel: string) => {
const q = toQueryString({ id: projectId, current_project_id: projectId });
const response = await apiClient.put<IServerResponse<ITaskPhase>>(
`${rootUrl}/label/${projectId}${q}`,
{ name: phaseLabel }
);
return response.data;
},
};

View File

@@ -0,0 +1,22 @@
import { IServerResponse } from '@/types/common.types';
import { API_BASE_URL } from '@/shared/constants';
import { ITaskPrioritiesGetResponse, ITaskPriority } from '@/types/tasks/taskPriority.types';
import apiClient from '@api/api-client';
const rootUrl = `${API_BASE_URL}/task-priorities`;
export const priorityApiService = {
getPriorities: async (): Promise<IServerResponse<ITaskPrioritiesGetResponse[]>> => {
const response = await apiClient.get<IServerResponse<ITaskPrioritiesGetResponse[]>>(
`${rootUrl}`
);
return response.data;
},
getPriorityById: async (priorityId: string): Promise<IServerResponse<ITaskPriority>> => {
const response = await apiClient.get<IServerResponse<ITaskPriority>>(
`${rootUrl}/${priorityId}`
);
return response.data;
},
};

View File

@@ -0,0 +1,77 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { ITaskStatus, ITaskStatusCategory } from '@/types/status.types';
import { ITaskStatusCreateRequest } from '@/types/tasks/task-status-create-request';
import { toQueryString } from '@/utils/toQueryString';
import { ITaskStatusUpdateModel } from '@/types/tasks/task-status-update-model.types';
const rootUrl = `${API_BASE_URL}/statuses`;
export const statusApiService = {
getStatuses: async (projectId: string): Promise<IServerResponse<ITaskStatus[]>> => {
const response = await apiClient.get<IServerResponse<ITaskStatus[]>>(
`${rootUrl}?project=${projectId}`
);
return response.data;
},
getStatusCategories: async (): Promise<IServerResponse<ITaskStatusCategory[]>> => {
const response = await apiClient.get<IServerResponse<ITaskStatusCategory[]>>(
`${rootUrl}/categories`
);
return response.data;
},
createStatus: async (
body: ITaskStatusCreateRequest,
currentProjectId: string
): Promise<IServerResponse<ITaskStatus>> => {
const q = toQueryString({ current_project_id: currentProjectId });
const response = await apiClient.post<IServerResponse<ITaskStatus>>(`${rootUrl}${q}`, body);
return response.data;
},
updateStatus: async (
statusId: string,
body: ITaskStatusUpdateModel,
currentProjectId: string
): Promise<IServerResponse<ITaskStatus>> => {
const q = toQueryString({ current_project_id: currentProjectId });
const response = await apiClient.put<IServerResponse<ITaskStatus>>(
`${rootUrl}/${statusId}${q}`,
body
);
return response.data;
},
updateNameOfStatus: async (
id: string,
body: ITaskStatusUpdateModel,
currentProjectId: string
): Promise<IServerResponse<ITaskStatus>> => {
const q = toQueryString({ current_project_id: currentProjectId });
const response = await apiClient.put<IServerResponse<ITaskStatus>>(
`${rootUrl}/name/${id}${q}`,
body
);
return response.data;
},
updateStatusOrder: async (
body: ITaskStatusCreateRequest,
currentProjectId: string
): Promise<IServerResponse<ITaskStatus>> => {
const q = toQueryString({ current_project_id: currentProjectId });
const response = await apiClient.put<IServerResponse<ITaskStatus>>(`${rootUrl}/order`, body);
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 });
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/${statusId}${q}`);
return response.data;
},
};

View File

@@ -0,0 +1,16 @@
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`;
export const subTasksApiService = {
getSubTasks: async (parentTaskId: string): Promise<IServerResponse<ISubTask[]>> => {
const response = await apiClient.get(`${root}/${parentTaskId}`);
return response.data;
},
};

View File

@@ -0,0 +1,15 @@
import { API_BASE_URL } from '@/shared/constants';
import apiClient from '../api-client';
import { IServerResponse } from '@/types/common.types';
import { IActivityLogsResponse } from '@/types/tasks/task-activity-logs-get-request';
const rootUrl = `${API_BASE_URL}/activity-logs`;
export const taskActivityLogsApiService = {
getActivityLogsByTaskId: async (
taskId: string
): Promise<IServerResponse<IActivityLogsResponse>> => {
const response = await apiClient.get(`${rootUrl}/${taskId}`);
return response.data;
},
};

View File

@@ -0,0 +1,46 @@
import { IServerResponse } from '@/types/common.types';
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';
import { toQueryString } from '@/utils/toQueryString';
const rootUrl = `${API_BASE_URL}/attachments`;
const taskAttachmentsApiService = {
createTaskAttachment: async (
body: ITaskAttachment
): Promise<IServerResponse<ITaskAttachmentViewModel>> => {
const response = await apiClient.post(`${rootUrl}/tasks`, body);
return response.data;
},
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[]>> => {
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 });
const response = await apiClient.get(`${rootUrl}/project/${projectId}${q}`);
return response.data;
},
deleteTaskAttachment: async (attachmentId: string): Promise<IServerResponse<void>> => {
const response = await apiClient.delete(`${rootUrl}/tasks/${attachmentId}`);
return response.data;
},
downloadTaskAttachment: async (id: string, filename: string): Promise<IServerResponse<any>> => {
const response = await apiClient.get(`${rootUrl}/download?id=${id}&file=${filename}`);
return response.data;
},
};
export default taskAttachmentsApiService;

View File

@@ -0,0 +1,44 @@
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';
const taskCommentsApiService = {
create: async (data: ITaskCommentsCreateRequest): Promise<IServerResponse<ITaskCommentsCreateRequest>> => {
const response = await apiClient.post(`${API_BASE_URL}/task-comments`, data);
return response.data;
},
getByTaskId: async (id: string): Promise<IServerResponse<ITaskCommentViewModel[]>> => {
const response = await apiClient.get(`${API_BASE_URL}/task-comments/${id}`);
return response.data;
},
update: async (id: string, body: ITaskComment): Promise<IServerResponse<ITaskComment>> => {
const response = await apiClient.put(`${API_BASE_URL}/task-comments/${id}`, body);
return response.data;
},
deleteAttachment: async (id: string, taskId: string): Promise<IServerResponse<ITaskComment>> => {
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}`);
return response.data;
},
delete: async (id: string, taskId: string): Promise<IServerResponse<ITaskComment>> => {
const response = await apiClient.delete(`${API_BASE_URL}/task-comments/${id}/${taskId}`);
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)}`);
return response.data;
},
};
export default taskCommentsApiService;

View File

@@ -0,0 +1,21 @@
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`;
export const taskDependenciesApiService = {
getTaskDependencies: async (taskId: string): Promise<IServerResponse<ITaskDependency[]>> => {
const response = await apiClient.get(`${rootUrl}/${taskId}`);
return response.data;
},
createTaskDependency: async (body: ITaskDependency): Promise<IServerResponse<ITaskDependency>> => {
const response = await apiClient.post(`${rootUrl}`, body);
return response.data;
},
deleteTaskDependency: async (dependencyId: string): Promise<IServerResponse<void>> => {
const response = await apiClient.delete(`${rootUrl}/${dependencyId}`);
return response.data;
},
};

View File

@@ -0,0 +1,77 @@
import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { ITask } from '@/types/tasks/task.types';
import {
IBulkAssignMembersRequest,
IBulkAssignRequest,
IBulkTasksArchiveRequest,
IBulkTasksDeleteRequest,
IBulkTasksLabelsRequest,
IBulkTasksPhaseChangeRequest,
IBulkTasksPriorityChangeRequest,
IBulkTasksStatusChangeRequest,
} from '@/types/tasks/bulk-action-bar.types';
import { ITaskAssigneesUpdateResponse } from '@/types/tasks/task-assignee-update-response';
const rootUrl = `${API_BASE_URL}/tasks/bulk`;
export const taskListBulkActionsApiService = {
changeStatus: async (
body: IBulkTasksStatusChangeRequest,
projectId: string
): Promise<IServerResponse<{ failed_tasks: string[] }>> => {
const response = await apiClient.put(`${rootUrl}/status?project=${projectId}`, body);
return response.data;
},
changePriority: async (
body: IBulkTasksPriorityChangeRequest,
projectId: string
): Promise<IServerResponse<ITask>> => {
const response = await apiClient.put(`${rootUrl}/priority?project=${projectId}`, body);
return response.data;
},
changePhase: async (
body: IBulkTasksPhaseChangeRequest,
projectId: string
): Promise<IServerResponse<ITask>> => {
const response = await apiClient.put(`${rootUrl}/phase?project=${projectId}`, body);
return response.data;
},
deleteTasks: async (
body: IBulkTasksDeleteRequest,
projectId: string
): Promise<IServerResponse<ITask>> => {
const response = await apiClient.put(`${rootUrl}/delete?project=${projectId}`, body);
return response.data;
},
archiveTasks: async (
body: IBulkTasksArchiveRequest,
unarchive = false
): Promise<IServerResponse<ITask>> => {
const response = await apiClient.put(
`${rootUrl}/archive?type=${unarchive ? 'unarchive' : 'archive'}&project=${body.project_id}`,
body
);
return response.data;
},
assignTasks: async (
body: IBulkAssignMembersRequest
): Promise<IServerResponse<ITaskAssigneesUpdateResponse>> => {
const response = await apiClient.put(`${rootUrl}/members?project=${body.project_id}`, body);
return response.data;
},
assignToMe: async (
body: IBulkAssignRequest
): Promise<IServerResponse<ITaskAssigneesUpdateResponse>> => {
const response = await apiClient.put(`${rootUrl}/assign-me?project=${body.project_id}`, body);
return response.data;
},
assignLabels: async (
body: IBulkTasksLabelsRequest,
projectId: string
): Promise<IServerResponse<ITask>> => {
const response = await apiClient.put(`${rootUrl}/label?project=${projectId}`, body);
return response.data;
},
};

View File

@@ -0,0 +1,32 @@
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 const taskTimeLogsApiService = {
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>> => {
const response = await apiClient.delete(`${rootUrl}/${id}?task=${taskId}`);
return response.data;
},
create: async (body: {}): Promise<IServerResponse<ITaskLogViewModel>> => {
const response = await apiClient.post(`${rootUrl}`, body);
return response.data;
},
update: async (id: string, body: {}): Promise<IServerResponse<ITaskLogViewModel>> => {
const response = await apiClient.put(`${rootUrl}/${id}`, body);
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

@@ -0,0 +1,69 @@
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,
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
});
return response.data;
},
createCustomColumn: async (
projectId: string,
columnData: {
name: string;
key: string;
field_type: string;
width: number;
is_visible: boolean;
configuration: any;
}
): Promise<IServerResponse<any>> => {
const response = await apiClient.post('/api/v1/custom-columns', {
project_id: projectId,
...columnData
});
return response.data;
},
updateCustomColumn: async (
columnId: string,
columnData: {
name: string;
field_type: string;
width: number;
is_visible: boolean;
configuration: any;
}
): Promise<IServerResponse<any>> => {
const response = await apiClient.put(`/api/v1/custom-columns/${columnId}`, columnData);
return response.data;
},
deleteCustomColumn: async (columnId: string): Promise<IServerResponse<any>> => {
const response = await apiClient.delete(`/api/v1/custom-columns/${columnId}`);
return response.data;
},
updateCustomColumnVisibility: async (
projectId: string,
item: ITaskListColumn
): Promise<IServerResponse<ITaskListColumn>> => {
const response = await apiClient.put(`/api/v1/custom-columns/project/${projectId}/columns`, item);
return response.data;
}
};

View File

@@ -0,0 +1,122 @@
import {
ITaskListColumn,
ITaskListGroup,
ITaskListMemberFilter,
} from '@/types/tasks/taskList.types';
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 { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
import { ITaskFormViewModel, ITaskViewModel } from '@/types/tasks/task.types';
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
const rootUrl = `${API_BASE_URL}/tasks`;
export interface ITaskListConfigV2 {
id: string;
field: string | null;
order: string | null;
search: string | null;
statuses: string | null;
members: string | null;
projects: string | null;
labels?: string | null;
priorities?: string | null;
archived?: boolean;
count?: boolean;
parent_task?: string;
group?: string;
isSubtasksInclude: boolean;
}
export const tasksApiService = {
getTaskList: async (config: ITaskListConfigV2): Promise<IServerResponse<ITaskListGroup[]>> => {
const q = toQueryString(config);
const response = await apiClient.get(`${rootUrl}/list/v2/${config.id}${q}`);
return response.data;
},
fetchTaskAssignees: async (
projectId: string
): Promise<IServerResponse<ITeamMemberViewModel[]>> => {
const response = await apiClient.get(`${rootUrl}/assignees/${projectId}`);
return response.data;
},
fetchTaskListColumns: async (projectId: string): Promise<IServerResponse<ITaskListColumn[]>> => {
const response = await apiClient.get(`${rootUrl}/list/columns/${projectId}`);
return response.data;
},
getFormViewModel: async (
taskId: string | null,
projectId: string | null
): Promise<IServerResponse<ITaskFormViewModel>> => {
const params = [];
if (taskId) params.push(`task_id=${taskId}`);
if (projectId) params.push(`project_id=${projectId}`);
const q = params.length ? `?${params.join('&')}` : '';
const response = await apiClient.get(`${rootUrl}/info${q}`);
return response.data;
},
deleteTask: async (taskId: string): Promise<IServerResponse<void>> => {
const response = await apiClient.delete(`${rootUrl}/${taskId}`);
return response.data;
},
toggleColumnVisibility: async (
projectId: string,
item: ITaskListColumn
): Promise<IServerResponse<ITaskListColumn>> => {
const response = await apiClient.put(`${rootUrl}/list/columns/${projectId}`, item);
return response.data;
},
getSubscribers: async (taskId: string): Promise<IServerResponse<InlineMember[]>> => {
const response = await apiClient.get(`${rootUrl}/subscribers/${taskId}`);
return response.data;
},
convertToSubtask: async (
taskId: string,
projectId: string,
parentTaskId: string,
groupBy: string,
toGroupId: string
): Promise<IServerResponse<void>> => {
const response = await apiClient.post(`${rootUrl}/convert-to-subtask`, {
id: taskId,
project_id: projectId,
parent_task_id: parentTaskId,
group_by: groupBy,
to_group_id: toGroupId,
});
return response.data;
},
convertToTask: async (taskId: string, projectId: string): Promise<IServerResponse<void>> => {
const response = await apiClient.post(`${rootUrl}/convert`, {
id: taskId,
project_id: projectId,
});
return response.data;
},
searchTask: async (
taskId: string,
projectId: string,
searchQuery: string
): Promise<IServerResponse<{ label: string; value: string }[]>> => {
const q = toQueryString({ taskId, projectId, searchQuery });
const response = await apiClient.get(`${rootUrl}/search${q}`);
return response.data;
},
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;
},
};

View File

@@ -0,0 +1,99 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '../api-client';
import { API_BASE_URL } from '@/shared/constants';
import { ITeamMemberCreateRequest } from '@/types/teamMembers/team-member-create-request';
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
import { ITeamMember } from '@/types/teamMembers/teamMember.types';
const rootUrl = `${API_BASE_URL}/team-members`;
export const teamMembersApiService = {
createTeamMember: async (
body: ITeamMemberCreateRequest
): Promise<IServerResponse<ITeamMember>> => {
const response = await apiClient.post<IServerResponse<ITeamMember>>(`${rootUrl}`, body);
return response.data;
},
get: async (
index: number,
size: number,
field: string | null,
order: string | null,
search: string | null,
all = false
): Promise<IServerResponse<ITeamMembersViewModel>> => {
const s = encodeURIComponent(search || '');
const params = new URLSearchParams({
index: index.toString(),
size: size.toString(),
...(field && { field }),
...(order && { order }),
...(s && { search: s }),
...(all && { all: all.toString() }),
});
const response = await apiClient.get<IServerResponse<ITeamMembersViewModel>>(
`${rootUrl}?${params}`
);
return response.data;
},
getById: async (id: string): Promise<IServerResponse<ITeamMemberViewModel>> => {
const response = await apiClient.get<IServerResponse<ITeamMemberViewModel>>(`${rootUrl}/${id}`);
return response.data;
},
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() : ''}`
);
return response.data;
},
update: async (id: string, body: ITeamMemberCreateRequest): Promise<IServerResponse<any>> => {
const response = await apiClient.put<IServerResponse<any>>(`${rootUrl}/${id}`, body);
return response.data;
},
delete: async (id: string): Promise<IServerResponse<any>> => {
const response = await apiClient.delete<IServerResponse<any>>(`${rootUrl}/${id}`);
return response.data;
},
getTeamMembersByProjectId: async (projectId: string): Promise<IServerResponse<any[]>> => {
const response = await apiClient.get<IServerResponse<any[]>>(`${rootUrl}/project/${projectId}`);
return response.data;
},
resendInvitation: async (id: string): Promise<IServerResponse<any>> => {
const response = await apiClient.put<IServerResponse<any>>(`${rootUrl}/resend-invitation`, {
id,
});
return response.data;
},
toggleMemberActiveStatus: async (
id: string,
active: boolean,
email: string
): Promise<IServerResponse<any>> => {
const params = new URLSearchParams({
active: active.toString(),
email,
});
const response = await apiClient.get<IServerResponse<any>>(
`${rootUrl}/deactivate/${id}?${params}`
);
return response.data;
},
addTeamMember: async (
id: string,
body: ITeamMemberCreateRequest
): Promise<IServerResponse<any>> => {
const response = await apiClient.put<IServerResponse<any>>(`${rootUrl}/add-member/${id}`, body);
return response.data;
},
};

View File

@@ -0,0 +1,51 @@
import { IServerResponse } from '@/types/common.types';
import apiClient from '../api-client';
import {
IAcceptTeamInvite,
ITeam,
ITeamActivateResponse,
ITeamGetResponse,
ITeamInvites,
} from '@/types/teams/team.type';
import { API_BASE_URL } from '@/shared/constants';
import { IOrganizationTeam } from '@/types/admin-center/admin-center.types';
const rootUrl = `${API_BASE_URL}/teams`;
export const teamsApiService = {
getTeams: async (): Promise<IServerResponse<ITeamGetResponse[]>> => {
const response = await apiClient.get<IServerResponse<ITeamGetResponse[]>>(
`${rootUrl}`
);
return response.data;
},
setActiveTeam: async (teamId: string): Promise<IServerResponse<ITeamActivateResponse>> => {
const response = await apiClient.put<IServerResponse<ITeamActivateResponse>>(
`${rootUrl}/activate`,
{ id: teamId }
);
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`
);
return response.data;
},
acceptInvitation: async (body: IAcceptTeamInvite): Promise<IServerResponse<ITeamInvites>> => {
const response = await apiClient.put<IServerResponse<ITeamInvites>>(`${rootUrl}`, body);
return response.data;
}
};

View File

@@ -1,16 +0,0 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {AccountSetupComponent} from "./account-setup/account-setup.component";
import {TeamsListComponent} from "./teams-list/teams-list.component";
const routes: Routes = [
{path: '', component: AccountSetupComponent},
{path: 'teams', component: TeamsListComponent}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AccountSetupRoutingModule {
}

View File

@@ -1,49 +0,0 @@
import {NgModule} from '@angular/core';
import {CommonModule, NgOptimizedImage} from '@angular/common';
import {AccountSetupRoutingModule} from './account-setup-routing.module';
import {AccountSetupComponent} from './account-setup/account-setup.component';
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {NzFormModule} from "ng-zorro-antd/form";
import {NzButtonModule} from "ng-zorro-antd/button";
import {NzInputModule} from "ng-zorro-antd/input";
import {NzSelectModule} from "ng-zorro-antd/select";
import {NzStepsModule} from "ng-zorro-antd/steps";
import {NzSkeletonModule} from "ng-zorro-antd/skeleton";
import {NzSpaceModule} from 'ng-zorro-antd/space';
import {NzIconModule} from 'ng-zorro-antd/icon';
import {NzTypographyModule} from 'ng-zorro-antd/typography';
import {NzDividerModule} from 'ng-zorro-antd/divider';
import {NzListModule} from "ng-zorro-antd/list";
import {TeamsListComponent} from './teams-list/teams-list.component';
import {NzRadioModule} from "ng-zorro-antd/radio";
import {ProjectTemplateImportDrawerComponent} from "@admin/components/project-template-import-drawer/project-template-import-drawer.component";
@NgModule({
declarations: [
AccountSetupComponent,
TeamsListComponent
],
imports: [
CommonModule,
FormsModule,
AccountSetupRoutingModule,
ReactiveFormsModule,
NzInputModule,
NzFormModule,
NzButtonModule,
NzSelectModule,
NzStepsModule,
NzSkeletonModule,
NzSpaceModule,
NzIconModule,
NzTypographyModule,
NzDividerModule,
NzListModule,
NgOptimizedImage,
NzRadioModule,
ProjectTemplateImportDrawerComponent
]
})
export class AccountSetupModule {
}

View File

@@ -1,161 +0,0 @@
<div class="setup-wrapper m-auto px-3">
<div class="row">
<div class="py-5">
<div class="text-center">
<img alt="Worklenz" height="50" src="/assets/images/logo.png">
</div>
<h5 nz-typography class="mb-4 mt-1 text-center">
Setup your account.
</h5>
<div class="mt-4 pt-5 col-xl-8 col-lg-10 col-md-12 mx-auto bg-white">
<nz-space nzDirection="vertical" class="w-100">
<nz-skeleton *nzSpaceItem class="d-block" [nzActive]="true" [nzLoading]="verifying">
<nz-steps class="mb-3 mt-3 justify-content-center w-steps mx-auto" [nzCurrent]="index"
[nzDirection]="'horizontal'" [nzSize]="'default'" (nzIndexChange)="onIndexChange($event)">
<nz-step [nzDisabled]="index < 0" [class.active-half]="isTeamNameValid()"></nz-step>
<nz-step [nzDisabled]="index < 1" [class.active-half]="isProjectNameValid()"></nz-step>
<nz-step [nzDisabled]="index < 2"></nz-step>
<nz-step [nzDisabled]="index < 3"></nz-step>
</nz-steps>
<form (submit)="next()" [formGroup]="form" [nzLayout]="'vertical'" nz-form
class="w-600 mx-auto mt-5 mb-5 pb-3">
<ng-container [ngSwitch]="index">
<!-- workspace name step -->
<nz-form-item *ngSwitchCase="0">
<h2 nz-typography>Name your organization.</h2>
<span nz-typography class="label-description">
Pick a name for your Worklenz account.
</span>
<nz-form-control [nzSpan]="null">
<input [formControlName]="'team_name'" nz-input [placeholder]="teamSetupPlaceholder" type="text"
tabindex="0" [id]="teamNameId"/>
</nz-form-control>
</nz-form-item>
<!-- end of workspace name -->
<!-- project name step -->
<nz-form-item *ngSwitchCase="1">
<h2 nz-typography>Create your first project.</h2>
<span nz-typography class="label-description">
What project are you working on right now?
</span>
<nz-form-control [nzSpan]="null">
<input [formControlName]="'project_name'" nz-input [placeholder]="'e.g. Worklenz marketing plan'"
[id]="projectNameId"
type="text" tabindex="0"/>
</nz-form-control>
<div class="position-relative">
<h4 nz-typography class="text-center mt-2 vert-text">or</h4>
<div class="vert-line"></div>
</div>
<button [nzType]="'primary'" nz-button (click)="openTemplateSelector()"
class="ms-auto me-auto">Import from templates
</button>
</nz-form-item>
<!-- end of project name step -->
<!-- start of add tasks step -->
<div *ngSwitchCase="2">
<h2 nz-typography>Create your first task.</h2>
<span nz-typography class="label-description">
Type few tasks that you are going to do in <span nz-typography>"<mark>{{projectName}}</mark>".</span>
</span>
<nz-list>
<nz-list-item *ngFor="let item of getTaskControls.controls; let i = index;"
[formArrayName]="'tasks'">
<input nz-input placeholder="Your Task" [attr.id]="'task-name-input-' + i" [formControlName]="i"
tabindex="0"/>
<ul nz-list-item-actions>
<nz-list-item-action>
<span nz-icon nzType="close-circle" class="dynamic-delete-button"
(click)="removeTaskRow(i, $event)"></span>
</nz-list-item-action>
</ul>
</nz-list-item>
<nz-list-item>
<button nz-button nzType="dashed" class="add-button" (click)="addNewTaskRow($event)">
<span nz-icon nzType="plus"></span>
Add another
</button>
</nz-list-item>
</nz-list>
</div>
<!-- end of add tasks step -->
<!-- start of team members step -->
<nz-form-item *ngSwitchCase="3">
<h2 nz-typography>
Invite your team to work with <br>
<span nz-typography style="font-size: 20px;font-weight:
400;">
<!-- use team name here -->
"<mark>{{workspaceName}}</mark>".
</span>
</h2>
<span nz-typography class="label-description">
Invite with email <span class="ms-1" nz-icon nzType="mail" nzTheme="outline"></span>
</span>
<nz-list>
<nz-list-item *ngFor="let item of getTeamMemberControls.controls; let i = index;"
[formArrayName]="'team_members'">
<ng-container>
<input nz-input placeholder="Email address" [attr.id]="emailInputId"
[formControlName]="i"/>
<ul nz-list-item-actions>
<nz-list-item-action>
<span nz-icon nzType="close-circle" class="dynamic-delete-button"
(click)="removeTeamMember(i, $event)"></span>
</nz-list-item-action>
</ul>
</ng-container>
</nz-list-item>
<nz-list-item>
<button nz-button nzType="dashed" class="add-button" (click)="addNewTeamMemberRow($event)">
<span nz-icon nzType="plus"></span>
Add another
</button>
</nz-list-item>
</nz-list>
</nz-form-item>
<!-- end of add team members step -->
</ng-container>
<!-- button steps for account setup -->
<div class="d-flex mt-5">
<button (click)="previous()" nz-button type="button" class="ps-0" nzType="link" *ngIf="index !== 0">
Go back
</button>
<button *ngIf="index === 3 && !loading" (click)="skipInvite()" [nzLoading]="loading" nzType="text"
type="button"
nz-button>
<span nz-typography style="font-weight:
500;color:#00000073;">Skip for now</span>
</button>
<!-- rename to Finish when goes to last step -->
<button [nzLoading]="loading" [nzType]="'primary'" [disabled]="!isValid()" nz-button
class="ms-auto">Continue
</button>
</div>
<!-- button steps for account setup -->
</form>
</nz-skeleton>
</nz-space>
</div>
</div>
</div>
</div>
<div class="bg-ant-grey" style="position: fixed;"></div>
<worklenz-project-template-import-drawer
[showBothTabs]="false"
(importProject)="templateSelected($event)">
</worklenz-project-template-import-drawer>

View File

@@ -1,116 +0,0 @@
nz-steps {
transform: scale(0.83);
@media (max-width: 480px) {
transform: scale(0.6);
}
}
.setup-wrapper {
width: 100%;
// max-width: 380px;
position: relative;
z-index: 1;
}
.w-600 {
width: 600px;
@media(max-width: 860px) {
width: 90%;
}
}
.bg-ant-grey {
background: #FAFAFA;
position: absolute;
bottom: 0;
top: 0;
left: 0;
right: 0;
z-index: 0;
}
.w-steps {
width: 725px;
@media(max-width: 860px) {
width: 100%;
}
}
.label-description {
font-size: 14px;
font-weight: 500;
color: #00000073;
margin-bottom: 16px;
}
.setup-success {
-webkit-animation: scale-in-center 0.15s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
animation: scale-in-center 0.15s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
}
.success-icon {
font-size: 36px;
margin-bottom: 16px;
}
@-webkit-keyframes scale-in-center {
0% {
-webkit-transform: scale(0);
transform: scale(0);
opacity: 1;
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
}
@keyframes scale-in-center {
0% {
-webkit-transform: scale(0);
transform: scale(0);
opacity: 1;
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
}
h2.ant-typography, div.ant-typography-h2, div.ant-typography-h2 > textarea, .ant-typography h2 {
margin-bottom: 1em;
}
.dynamic-delete-button {
font-size: 20px !important;
color: #999 !important;
cursor: pointer !important;
position: relative !important;
transition: all .3s !important;
margin-left: 5px !important;
}
.vert-text {
max-width: 40px;
background-color: #fff;
position: relative;
z-index: 99;
margin-left: auto;
margin-right: auto;
}
.vert-line {
position: absolute;
left: 0;
right: 0;
width: 100%;
content: '';
height: 1px;
background-color: #00000047;
bottom: 0;
top: 0;
margin-bottom: auto;
margin-top: auto;
}

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {AccountSetupComponent} from './account-setup.component';
describe('AccountSetupComponent', () => {
let component: AccountSetupComponent;
let fixture: ComponentFixture<AccountSetupComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AccountSetupComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AccountSetupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,298 +0,0 @@
import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core';
import {
AbstractControl,
FormArray,
FormBuilder,
FormControl,
FormGroup,
ValidationErrors,
ValidatorFn,
Validators
} from "@angular/forms";
import {AppService} from "@services/app.service";
import {AuthService} from "@services/auth.service";
import {Router} from "@angular/router";
import {log_error, smallId} from "@shared/utils";
import {ProfileSettingsApiService} from "@api/profile-settings-api.service";
import {EMAIL_REGEXP, WHITESPACE_REGEXP} from "@shared/constants";
import {
ProjectTemplateImportDrawerComponent
} from "@admin/components/project-template-import-drawer/project-template-import-drawer.component";
import {ProjectTemplateApiService} from "@api/project-template-api.service";
export interface IAccountSetupRequest {
team_name?: string;
project_name?: string;
tasks: string[];
team_members: string[];
}
export interface IAccountSetupResponse {
id?: string;
has_invitations?: boolean;
}
@Component({
selector: 'worklenz-account-setup',
templateUrl: './account-setup.component.html',
styleUrls: ['./account-setup.component.scss']
})
export class AccountSetupComponent implements OnInit, AfterViewInit {
@ViewChild(ProjectTemplateImportDrawerComponent) projectTemplateDrawer!: ProjectTemplateImportDrawerComponent;
form!: FormGroup;
inputsMap: { [x: number]: string } = {};
validateForm!: FormGroup;
validateFormMember!: FormGroup;
loading = false;
verifying = false;
readonly teamNameId = smallId(6);
readonly projectNameId = smallId(6);
readonly emailInputId = smallId(6);
skipInviteClicked = false;
selectedTemplateId: string | null = null;
constructor(
private fb: FormBuilder,
private app: AppService,
private auth: AuthService,
private api: ProfileSettingsApiService,
private templateApi: ProjectTemplateApiService,
private router: Router
) {
this.form = this.fb.group({
team_name: [null, [Validators.required, Validators.pattern(WHITESPACE_REGEXP)]],
project_name: [null, [Validators.required, Validators.pattern(WHITESPACE_REGEXP)]],
tasks: this.fb.array([], [Validators.minLength(1), Validators.pattern(WHITESPACE_REGEXP)]),
team_members: this.fb.array([], [Validators.minLength(1), this.validEmail(EMAIL_REGEXP)])
})
this.app.setTitle('Setup your account');
}
get profile() {
return this.auth.getCurrentSession();
}
get teamSetupPlaceholder() {
return `e.g., ${this.profile?.name}'s Team`;
}
get projectName() {
return this.form.value.project_name;
}
get workspaceName() {
return this.form.value.team_name;
}
_index = 0;
get index() {
return this._index;
}
set index(i) {
this._index = i;
}
get getTaskControls() {
return <FormArray>this.form.get('tasks');
}
get getTasks() {
return this.form.controls['tasks'] as FormArray;
}
get getTeamMemberControls() {
return <FormArray>this.form.get('team_members');
}
get getTeamMembers() {
return this.form.controls['team_members'] as FormArray;
}
ngOnInit(): void {
void this.reauthorize();
this.validateForm = this.fb.group({});
this.validateFormMember = this.fb.group({});
this.addNewTaskRow();
this.addNewTeamMemberRow();
}
ngAfterViewInit() {
this.inputsMap = {
0: this.teamNameId,
1: this.projectNameId,
2: 'task-name-input-0',
3: this.emailInputId
};
this.focusInput();
}
public async submit() {
if (this.loading) return;
try {
this.loading = true;
const model = this.form.value;
model.tasks = model.tasks.filter((t: any) => t?.trim().length);
model.template_id = this.selectedTemplateId;
let res: any;
if (model.template_id) {
res = await this.templateApi.setupAccount(model);
} else {
res = await this.api.setupAccount(model);
}
await this.auth.authorize();
if (res.done && res.body.id) {
await this.auth.authorize();
const url = (res.body.has_invitations)
? `/worklenz/setup/teams`
: `/worklenz/projects/${res.body.id}`;
await this.router.navigate([url]);
}
this.loading = false;
} catch (e) {
log_error(e);
this.loading = false;
}
}
previous() {
if (this.index > 0) {
this.index -= 1;
}
}
next() {
if (!this.isValid()) return;
if ((this.index + 1) > 3) {
void this.submit();
} else {
this.index++;
this.focusInput();
}
}
onIndexChange(index: number) {
this.index = index;
this.focusInput();
}
validEmail(pattern: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
let valid = false;
if (Array.isArray(control.value)) {
valid = control.value.every(email => pattern.test(email));
} else {
valid = pattern.test(control.value);
}
return valid ? null : {email: {value: control.value}};
};
}
isValidTasksInput() {
if (this.getTasks.length && this.getTasks.valid) return true;
return this.getTasks.valid;
}
isValidTeamMembers() {
return this.getTeamMembers.length && this.getTeamMembers.valid;
}
isValid() {
if (this.index === 0) return this.form.controls["team_name"].valid;
if (this.index === 1) return this.form.controls["project_name"].valid;
if (this.index === 2) return this.isValidTasksInput();
if (this.index === 3) return this.isValidTeamMembers();
return false;
}
addNewTaskRow(event?: MouseEvent): void {
if (event) event.preventDefault();
const emptyTaskInput = new FormControl(null, [Validators.required]);
this.getTasks.push(emptyTaskInput);
// Focus new input
setTimeout(() => {
const element = document.querySelector(`#task-name-input-${this.getTaskControls.controls.length - 1}`) as HTMLInputElement;
element?.focus();
}, 100);
}
removeTaskRow(i: number, e: MouseEvent): void {
e.preventDefault();
const tasks = this.getTasks;
if (tasks.length > 1) {
tasks.removeAt(i);
}
}
addNewTeamMemberRow(e?: MouseEvent): void {
if (e) {
e.preventDefault();
}
const teamMembers = this.getTeamMembers;
const teamMemberForm = new FormControl('', [Validators.email]);
teamMembers.push(teamMemberForm);
}
removeTeamMember(i: number, e: MouseEvent): void {
e.preventDefault();
const teamMembers = this.getTeamMembers;
if (teamMembers.length > 1) {
teamMembers.removeAt(i);
}
}
isTeamNameValid() {
return this.form.controls["team_name"].valid
}
isProjectNameValid() {
return this.form.controls["project_name"].valid
}
private async reauthorize() {
this.verifying = true;
await this.auth.authorize();
if (this.auth.getCurrentSession()?.setup_completed)
return this.router.navigate(['/worklenz/home']);
this.verifying = false;
return null;
}
private focusInput() {
setTimeout(() => {
const id = this.inputsMap[this.index];
const element = document.querySelector(`#${id}`) as HTMLInputElement;
element?.focus();
}, 250);
}
skipInvite() {
this.skipInviteClicked = false;
this.form.controls['team_members'].reset([]);
void this.submit();
}
openTemplateSelector() {
this.projectTemplateDrawer.open();
}
templateSelected(event: any) {
this.selectedTemplateId = event.template_id;
this.submit();
}
}

View File

@@ -1,50 +0,0 @@
<div class="setup-wrapper m-auto px-3">
<div class="row">
<div class="py-5">
<div class="text-center">
<img alt="Worklenz" height="50" ngSrc="/assets/images/logo.png" width="235">
</div>
<div class="pt-5 col-md-3 mx-auto bg-white">
<nz-skeleton [nzActive]="true" [nzLoading]="loading">
<nz-radio-group [(ngModel)]="selectedTeamId" [name]="'team'" class="w-100">
<ng-container *ngIf="teams.length">
<h5 [ngStyle]="{ 'margin-bottom.px': 16 }" nz-typography>Teams</h5>
<nz-list nzBordered>
<nz-list-item
*ngFor="let item of teams"
[class.selected]="selectedTeamId === item.id"
(click)="selectTeam(item.id, false)"
>
<span nz-typography>{{item.name}}</span>
<label nz-radio [nzValue]="item.id"></label>
</nz-list-item>
</nz-list>
</ng-container>
<ng-container *ngIf="invites.length">
<h5 [ngStyle]="{ margin: '16px 0' }" nz-typography>Invitations</h5>
<nz-list nzBordered>
<nz-list-item
*ngFor="let item of invites"
[class.selected]="selectedTeamId === item.team_id"
(click)="selectTeam(item.team_id, true)"
>
<span nz-typography>{{item.team_name}}</span>
<label nz-radio [nzValue]="item.team_id"></label>
</nz-list-item>
</nz-list>
</ng-container>
</nz-radio-group>
<nz-divider></nz-divider>
<button [nzLoading]="switching" (click)="continueWithSelection()" nz-button [nzType]="'default'"
[nzSize]="'large'" nzBlock>
<span nz-icon nzType="check" nzTheme="outline"></span> Continue with selection
</button>
</nz-skeleton>
</div>
</div>
</div>
</div>

View File

@@ -1,16 +0,0 @@
nz-list-item {
cursor: pointer;
&:hover {
background: #f8f7f9;
}
&.selected {
background: rgba(24, 144, 255, 0.0784313725) !important;
}
}
nz-radio-group {
max-height: calc(100vh - 270px);
overflow: auto;
}

View File

@@ -1,21 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {TeamsListComponent} from './teams-list.component';
describe('TeamsListComponent', () => {
let component: TeamsListComponent;
let fixture: ComponentFixture<TeamsListComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TeamsListComponent]
});
fixture = TestBed.createComponent(TeamsListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,132 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {TeamsApiService} from "@api/teams-api.service";
import {log_error} from "@shared/utils";
import {ITeamGetResponse} from "@interfaces/api-models/team-get-response";
import {ITeamInvites} from "@interfaces/team";
import {AuthService} from "@services/auth.service";
import {Router} from "@angular/router";
import {IServerResponse} from "@interfaces/api-models/server-response";
import {AppService} from "@services/app.service";
interface ITeamViewModel extends ITeamGetResponse {
active?: boolean;
pending_invitation?: boolean;
}
@Component({
selector: 'worklenz-teams-list',
templateUrl: './teams-list.component.html',
styleUrls: ['./teams-list.component.scss']
})
export class TeamsListComponent implements OnInit {
teams: ITeamViewModel[] = [];
invites: ITeamInvites[] = [];
selectedTeamId: string | undefined = undefined;
loading = false;
switching = false;
isInvitation = false;
constructor(
private readonly app: AppService,
private readonly auth: AuthService,
private readonly teamsApi: TeamsApiService,
private readonly router: Router
) {
this.app.setTitle("Teams & Invitations");
}
ngOnInit() {
void this.getData();
}
private updateDefaultSelection() {
if (this.teams.length) {
const activeTeam = this.teams.find(t => t.active);
if (activeTeam) {
this.selectedTeamId = activeTeam.id;
} else {
this.selectedTeamId = this.teams[0].id;
}
} else if (this.invites.length) {
this.selectedTeamId = this.invites[0].team_id;
}
}
private async getData() {
this.loading = true;
await this.getTeams();
await this.getInvites();
this.updateDefaultSelection();
this.loading = false;
}
private async getTeams() {
try {
const res: IServerResponse<ITeamViewModel[]> = await this.teamsApi.get();
if (res.done) {
this.teams = res.body.filter(t => !t.pending_invitation);
}
} catch (e) {
log_error(e);
}
}
private async getInvites() {
try {
const res = await this.teamsApi.getInvites();
if (res.done) {
this.invites = res.body;
}
} catch (e) {
log_error(e);
}
}
selectTeam(id: string | undefined, isInvitation: boolean) {
if (id) {
this.selectedTeamId = id;
this.isInvitation = isInvitation;
}
}
async continueWithSelection() {
if (this.selectedTeamId) {
try {
this.switching = true;
if (this.isInvitation) {
const accepted = await this.acceptInvitation();
if (!accepted) {
this.switching = false;
this.app.notify("Request failed!", "Invitation accept failed. Please try again.", false);
return;
}
}
const res = await this.teamsApi.activate(this.selectedTeamId);
if (res.done) {
await this.handleSelectionDone();
}
this.switching = false;
} catch (e) {
this.switching = false;
}
}
}
private async handleSelectionDone() {
await this.auth.authorize();
await this.router.navigate(["/worklenz"]);
}
private async acceptInvitation() {
const invitation = this.invites.find(i => i.team_id === this.selectedTeamId);
if (invitation) {
const res = await this.teamsApi.accept({team_member_id: invitation.team_member_id});
return res.done;
}
return false;
}
}

View File

@@ -1,28 +0,0 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {OverviewComponent} from './overview/overview.component';
import {UsersComponent} from './users/users.component';
import {TeamsComponent} from './teams/teams.component';
import {LayoutComponent} from './layout/layout.component';
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{path: "", redirectTo: "overview", pathMatch: "full"},
{path: "overview", component: OverviewComponent},
{path: "users", component: UsersComponent},
{path: "teams", component: TeamsComponent},
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminCenterRoutingModule {
}

View File

@@ -1,27 +0,0 @@
import {Injectable} from '@angular/core';
import {Subject} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class AdminCenterService {
private readonly _teamCreateSbj$ = new Subject<void>();
private readonly _teamNameChangeSbj$ = new Subject<{ teamId: string, teamName: string }>();
get onCreateTeam() {
return this._teamCreateSbj$.asObservable();
}
get onTeamNameChange() {
return this._teamNameChangeSbj$.asObservable();
}
public emitCreateTeam() {
this._teamCreateSbj$.next();
}
public emitTeamNameChange(response: { teamId: string, teamName: string }) {
this._teamNameChangeSbj$.next(response);
}
}

View File

@@ -1,86 +0,0 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {AdminCenterRoutingModule} from './admin-center-routing.module';
import {NzLayoutModule} from 'ng-zorro-antd/layout';
import {NzPageHeaderModule} from 'ng-zorro-antd/page-header';
import {SidebarComponent} from './sidebar/sidebar.component';
import {NzMenuModule} from 'ng-zorro-antd/menu';
import {NzIconModule} from 'ng-zorro-antd/icon';
import {OverviewComponent} from './overview/overview.component';
import {NzCardModule} from 'ng-zorro-antd/card';
import {NzTypographyModule} from 'ng-zorro-antd/typography';
import {NzTableModule} from 'ng-zorro-antd/table';
import {UsersComponent} from './users/users.component';
import {TeamsComponent} from './teams/teams.component';
import {LayoutComponent} from './layout/layout.component';
import {NzSpaceModule} from 'ng-zorro-antd/space';
import {NzFormModule} from 'ng-zorro-antd/form';
import {NzInputModule} from 'ng-zorro-antd/input';
import {NzButtonModule} from 'ng-zorro-antd/button';
import {NzSkeletonModule} from 'ng-zorro-antd/skeleton';
import {NzAvatarModule} from 'ng-zorro-antd/avatar';
import {NzBadgeModule} from 'ng-zorro-antd/badge';
import {NzToolTipModule} from 'ng-zorro-antd/tooltip';
import {NzDropDownModule} from 'ng-zorro-antd/dropdown';
import {NzRadioModule} from 'ng-zorro-antd/radio';
import {NzDrawerModule} from 'ng-zorro-antd/drawer';
import {NzSelectModule} from 'ng-zorro-antd/select';
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {AvatarsComponent} from "../components/avatars/avatars.component";
import {NzTabsModule} from "ng-zorro-antd/tabs";
import {FirstCharUpperPipe} from "@pipes/first-char-upper.pipe";
import {NzModalModule} from "ng-zorro-antd/modal";
import {NzProgressModule} from "ng-zorro-antd/progress";
import {NzDividerModule} from "ng-zorro-antd/divider";
import {NzSegmentedModule} from "ng-zorro-antd/segmented";
import {NzPopconfirmModule} from "ng-zorro-antd/popconfirm";
import {NzCheckboxModule} from "ng-zorro-antd/checkbox";
import {NzAutocompleteModule} from "ng-zorro-antd/auto-complete";
@NgModule({
declarations: [
SidebarComponent,
OverviewComponent,
UsersComponent,
TeamsComponent,
LayoutComponent
],
imports: [
CommonModule,
AdminCenterRoutingModule,
NzLayoutModule,
NzPageHeaderModule,
NzMenuModule,
NzIconModule,
NzCardModule,
NzTypographyModule,
NzTableModule,
NzSpaceModule,
NzFormModule,
NzInputModule,
NzButtonModule,
NzSkeletonModule,
NzBadgeModule,
NzAvatarModule,
NzToolTipModule,
NzDropDownModule,
NzRadioModule,
NzDrawerModule,
NzSelectModule,
ReactiveFormsModule,
AvatarsComponent,
FormsModule,
NzTabsModule,
FirstCharUpperPipe,
NzModalModule,
NzProgressModule,
NzDividerModule,
NzSegmentedModule,
NzPopconfirmModule,
NzCheckboxModule,
NzAutocompleteModule
]
})
export class AdminCenterModule {
}

View File

@@ -1,13 +0,0 @@
<div class="container">
<nz-page-header [nzGhost]="false" class="px-0">
<nz-page-header-title>Admin Center</nz-page-header-title>
</nz-page-header>
<nz-layout class="inner-layout">
<nz-sider [nzWidth]="'240px'">
<worklenz-admin-center-sidebar></worklenz-admin-center-sidebar>
</nz-sider>
<nz-content class="px-4 bg-white">
<router-outlet></router-outlet>
</nz-content>
</nz-layout>
</div>

View File

@@ -1,3 +0,0 @@
[nz-submenu] {
transition: none !important;
}

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LayoutComponent} from './layout.component';
describe('LayoutComponent', () => {
let component: LayoutComponent;
let fixture: ComponentFixture<LayoutComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LayoutComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,10 +0,0 @@
import {Component} from '@angular/core';
@Component({
selector: 'worklenz-layout',
templateUrl: './layout.component.html',
styleUrls: ['./layout.component.scss']
})
export class LayoutComponent {
}

View File

@@ -1,75 +0,0 @@
<nz-page-header [nzGhost]="false" class="px-0">
<nz-page-header-title>Overview</nz-page-header-title>
</nz-page-header>
<nz-card>
<h4>Organization Name</h4>
<div class="card-content">
<p nz-typography nzEditable [(nzContent)]="organizationDetails.name"
(nzContentChange)="updateOrganizationName();"></p>
</div>
</nz-card>
<div class="mt-4"></div>
<nz-card>
<h4>Organization Owner</h4>
<div class="card-content">
<nz-skeleton [nzLoading]="loadingName" [nzActive]="true" [nzParagraph]="{rows: 3}">
<p nz-typography>{{organizationDetails.owner_name}}</p>
<div>
<nz-space class="align-items-center">
<span *nzSpaceItem nz-icon nzType="mail" nzTheme="outline" nz-tooltip
[nzTooltipTitle]="'Email Address'"></span>
<span *nzSpaceItem nz-typography>{{organizationDetails.email}}</span>
</nz-space>
</div>
<div class="mt-2">
<nz-space class="align-items-center" style="margin-left: -4px">
<span *nzSpaceItem nz-icon nzType="phone" nzTheme="outline" nz-tooltip
[nzTooltipTitle]="'Contact Number'"></span>&nbsp;
<span *nzSpaceItem>
<nz-space class="align-items-center">
<div *nzSpaceItem class="position-relative" style="min-height: 32px">
<span *ngIf="organizationDetails.contact_number && !isNumberEditing" nz-typography
style="line-height: 32px">{{organizationDetails.contact_number}}</span>
<span *ngIf="!organizationDetails.contact_number && !isNumberEditing" (click)="focusNumberInput()"
nz-typography class="text-btn">Add Contact Number</span>
<div *ngIf="isNumberEditing" class="number-input">
<input nz-input type="tel" placeholder="Add Contact Number"
[(ngModel)]="organizationDetails.contact_number"
(input)="sanitizeContactNumber($event)"
(blur)="updateOwnerContactNumber();"
(keydown.enter)="updateOwnerContactNumber()"
maxlength="20" #numberInput/>
</div>
</div>
<ng-container *nzSpaceItem>
<ng-container *ngIf="organizationDetails.contact_number">
<span *ngIf="!isNumberEditing" (click)="focusNumberInput()" nz-icon nzType="edit"
nzTheme="outline" class="edit-btn"></span>
</ng-container>
</ng-container>
</nz-space>
</span>
</nz-space>
</div>
</nz-skeleton>
</div>
</nz-card>
<div class="mt-4"></div>
<nz-card>
<h4>Organization Admins</h4>
<div class="card-content">
<nz-skeleton [nzLoading]="loadingAdmins" [nzActive]="true" [nzParagraph]="{rows: 5}">
<nz-table [nzNoResult]="" [nzData]="organizationAdmins" [nzPaginationType]="'small'" #adminsTable>
<tbody>
<tr *ngFor="let item of adminsTable.data">
<td class="left-td">{{item.name}} <span nz-typography *ngIf="item.is_owner" nzType="secondary">(Owner)</span>
</td>
<td class="b-65">{{item.email}}</td>
</tr>
</tbody>
</nz-table>
</nz-skeleton>
</div>
</nz-card>

View File

@@ -1,42 +0,0 @@
.card-content {
padding-top: 8px;
}
.ant-card-bordered {
// border: 1px solid #d9d9d9;
}
h4 {
font-size: 16px;
}
p {
margin-bottom: 8px;
}
.b-65 {
color: rgba(0, 0, 0, 0.65);
}
.left-td {
width: 250px;
}
.number-input {
position: absolute;
left: 0;
top: 0;
min-width: 200px;
width: 100%;
}
.edit-btn {
cursor: pointer;
color: #1890ff;
}
.text-btn {
line-height: 32px;
cursor: pointer;
color: #188fff
}

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {OverviewComponent} from './overview.component';
describe('OverviewComponent', () => {
let component: OverviewComponent;
let fixture: ComponentFixture<OverviewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OverviewComponent]
})
.compileComponents();
fixture = TestBed.createComponent(OverviewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,104 +0,0 @@
import {Component, ElementRef, NgZone, OnInit, ViewChild} from '@angular/core';
import {AccountCenterApiService} from "@api/account-center-api.service";
import {log_error} from "@shared/utils";
import {IOrganization, IOrganizationAdmin} from "@interfaces/account-center";
@Component({
selector: 'worklenz-overview',
templateUrl: './overview.component.html',
styleUrls: ['./overview.component.scss']
})
export class OverviewComponent implements OnInit {
@ViewChild('numberInput') private numberInput: ElementRef | undefined;
loadingName = false;
loadingAdmins = false;
isNumberEditing = false;
organizationDetails: IOrganization = {};
organizationAdmins: IOrganizationAdmin[] = [];
constructor(
private api: AccountCenterApiService,
private readonly ngZone: NgZone) {
}
ngOnInit() {
void this.getOrganizationName();
void this.getOrganizationAdmins();
}
async getOrganizationName() {
try {
this.loadingName = true;
const res = await this.api.getOrganizationName();
if (res.done) {
this.loadingName = false;
this.organizationDetails = res.body;
}
} catch (e) {
this.loadingName = false;
log_error(e);
}
}
async getOrganizationAdmins() {
try {
this.loadingAdmins = true;
const res = await this.api.getOrganizationAdmins();
if (res.done) {
this.loadingAdmins = false;
this.organizationAdmins = res.body;
}
} catch (e) {
this.loadingAdmins = false;
log_error(e);
}
}
async updateOrganizationName() {
try {
this.loadingName = true;
const res = await this.api.updateOrganizationName({name: this.organizationDetails.name});
if (res.done) {
this.loadingName = false;
}
} catch (e) {
this.loadingName = false;
log_error(e);
}
await this.getOrganizationName();
}
async updateOwnerContactNumber() {
try {
this.loadingName = true;
this.isNumberEditing = false;
const res = await this.api.updateOwnerContactNumber({contact_number: this.organizationDetails.contact_number || ''});
if (res.done) {
this.loadingName = false;
await this.getOrganizationName();
}
} catch (e) {
this.loadingName = false;
log_error(e);
}
}
focusNumberInput() {
this.isNumberEditing = true;
this.ngZone.runOutsideAngular(() => {
setTimeout(() => {
this.numberInput?.nativeElement.focus();
this.numberInput?.nativeElement.select();
}, 100)
});
}
sanitizeContactNumber(event: any) {
const input = event.target as HTMLInputElement;
const sanitizedValue = input.value.replace(/[^0-9()+ -]/g, '');
this.organizationDetails.contact_number = sanitizedValue;
}
}

View File

@@ -1,11 +0,0 @@
<ul class="border-0" nz-menu [nzMode]="'vertical'">
<li class="rounded-4" [routerLink]="'overview'" [routerLinkActive]="['ant-menu-item-selected']"
[nzTitle]="'Overview'" [nzIcon]="'appstore'" nz-submenu>
</li>
<li class="rounded-4" [routerLink]="'users'" [routerLinkActive]="['ant-menu-item-selected']" [nzTitle]="'Users'"
[nzIcon]="'user'" nz-submenu>
</li>
<li class="rounded-4" [routerLink]="'teams'" [routerLinkActive]="['ant-menu-item-selected']" [nzTitle]="'Teams'"
[nzIcon]="'team'" nz-submenu>
</li>
</ul>

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SidebarComponent} from './sidebar.component';
describe('SidebarComponent', () => {
let component: SidebarComponent;
let fixture: ComponentFixture<SidebarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SidebarComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,18 +0,0 @@
import {Component} from '@angular/core';
import {AuthService} from "@services/auth.service";
@Component({
selector: 'worklenz-admin-center-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.scss']
})
export class SidebarComponent {
constructor(
private readonly auth: AuthService
) {
}
get profile() {
return this.auth.getCurrentSession();
}
}

View File

@@ -1,260 +0,0 @@
<nz-page-header [nzGhost]="false" class="px-0">
<nz-page-header-title>Teams</nz-page-header-title>
</nz-page-header>
<nz-page-header class="site-page-header pt-0 ps-0">
<nz-page-header-subtitle>{{this.total ? this.total : 0}} teams</nz-page-header-subtitle>
<nz-page-header-extra>
<nz-space>
<button *nzSpaceItem nz-button nz-tooltip nzShape="circle" (click)="getTeams()"
nzTooltipTitle="Refresh teams" nzType="default">
<span nz-icon nzTheme="outline" nzType="sync"></span>
</button>
<form *nzSpaceItem [nzLayout]="'vertical'" nz-form [formGroup]="searchForm">
<nz-input-group [nzSuffix]="suffixIconSearch">
<input nz-input placeholder="Search by name" [formControlName]="'search'"
type="text"/>
</nz-input-group>
<ng-template #suffixIconSearch>
<span nz-icon nzType="search"></span>
</ng-template>
</form>
<span *nzSpaceItem>
<button nz-button nzType="primary" (click)="openNewTeam()">Add
Team</button>
</span>
</nz-space>
</nz-page-header-extra>
</nz-page-header>
<nz-card>
<nz-skeleton [nzActive]="false" [nzLoading]="false">
<nz-table #teamsTable
(nzQueryParams)="onQueryParamsChange($event)"
[nzData]="teams || []"
[nzLoading]="loading"
[nzPageIndex]="pageIndex"
[nzPageSizeOptions]="paginationSizes"
[nzFrontPagination]="false"
[nzPageSize]="pageSize"
[nzTotal]="total"
class="custom-table"
nzShowSizeChanger
nzSize="small" [nzNoResult]="currentTeam ? noDataTemplate1 : noDataTemplate">
<thead>
<tr>
<th scope="col">Team</th>
<th scope="col" class="text-center">Members Count</th>
<th scope="col">Members</th>
<th scope="col" class="text-center"></th>
</tr>
</thead>
<tbody>
<tr class="actions-row" *ngIf="currentTeam">
<td class="cursor-pointer">
<nz-badge nzColor="#52c41a" [nzText]="currentTeam.name"></nz-badge>
</td>
<td class="cursor-pointer text-center">
{{currentTeam.members_count}}
</td>
<td class="cursor-pointer">
<worklenz-avatars [names]="currentTeam.names"></worklenz-avatars>
</td>
<td class="cursor-pointer">
<div class="actions text-center">
<nz-space>
<button *nzSpaceItem nz-button nz-tooltip
nzSize="small"
[nzTooltipPlacement]="'top'"
[nzTooltipTitle]="'Settings'"
[nzType]="'default'"
(click)="openTeamDrawer(currentTeam)">
<span nz-icon nzType="setting"></span>
</button>
<button *nzSpaceItem (nzOnConfirm)="deleteTeam(currentTeam.id)"
nz-button nz-popconfirm nz-tooltip [nzOkText]="'Yes'"
[nzPopconfirmTitle]="'Are you sure?'" [nzSize]="'small'" [nzTooltipPlacement]="'top'"
[nzTooltipTitle]="'Delete'"
[nzType]="'default'">
<span nz-icon nzType="delete"></span>
</button>
</nz-space>
</div>
</td>
</tr>
<tr class="actions-row" *ngFor="let team of teamsTable.data">
<td class="cursor-pointer">
{{team.name}}
</td>
<td class="cursor-pointer text-center">
{{team.members_count}}
</td>
<td class="cursor-pointer">
<worklenz-avatars [names]="team.names"></worklenz-avatars>
</td>
<td class="cursor-pointer">
<div class="actions text-center">
<nz-space>
<button *nzSpaceItem nz-button nz-tooltip
nzSize="small"
[nzTooltipPlacement]="'top'"
[nzTooltipTitle]="'Settings'"
[nzType]="'default'"
(click)="openTeamDrawer(team)">
<span nz-icon nzType="setting"></span>
</button>
<button *nzSpaceItem (nzOnConfirm)="deleteTeam(team.id)"
nz-button nz-popconfirm nz-tooltip [nzOkText]="'Yes'"
[nzPopconfirmTitle]="'Are you sure?'" [nzSize]="'small'" [nzTooltipPlacement]="'top'"
[nzTooltipTitle]="'Delete'"
[nzType]="'default'">
<span nz-icon nzType="delete"></span>
</button>
</nz-space>
</div>
</td>
</tr>
</tbody>
</nz-table>
</nz-skeleton>
</nz-card>
<ng-template #noDataTemplate>
<div class="pt-4 pb-5">
<div class="no-data-img-holder mx-auto mb-3">
<img src="/assets/images/empty-box.webp" class="img-fluid" alt="">
</div>
<span nz-typography class="no-data-text">No teams found in the organization.</span>
</div>
</ng-template>
<ng-template #noDataTemplate1>
</ng-template>
<nz-drawer
[nzClosable]="true"
[nzVisible]="visible"
nzPlacement="right"
[nzTitle]="'Team Settings'"
[nzSize]="'large'"
[nzWidth]="'550px'"
(nzOnClose)="close()">
<ng-container *nzDrawerContent>
<form nz-form [formGroup]="editTeamForm" (ngSubmit)="submit()" [nzLayout]="'vertical'">
<nz-form-item>
<nz-form-label
[nzSpan]="null"
nzRequired>
Team Name
</nz-form-label>
<input nz-input [formControlName]="'name'" placeholder="Name of the team"/>
</nz-form-item>
<nz-form-item class="d-block">
<nz-form-label [nzSpan]="null" nzRequired>
Users ({{teamMembers.controls.length}})
</nz-form-label>
<!-- <nz-form-item>-->
<!-- <nz-form-label [nzSpan]="null">-->
<!-- Add Team Members-->
<!-- </nz-form-label>-->
<!-- <nz-select nzShowSearch nzAllowClear nzPlaceHolder="Type to search" (nzOnSearch)="getUsers($event)"-->
<!-- nzServerSearch [formControlName]="'search'" [nzNotFoundContent]="notFoundContentTemplate">-->
<!-- <nz-option *ngFor="let item of users" nzCustomContent [nzLabel]="item.name || ''" [nzValue]="item.email">-->
<!-- <div class="d-flex align-items-center user-select-none">-->
<!-- <nz-avatar-->
<!-- nz-tooltip-->
<!-- [nzSize]="28"-->
<!-- [nzText]="item.name | firstCharUpper"-->
<!-- [nzTooltipTitle]="item.name"-->
<!-- [style.background-color]="item.avatar_url ? '#ececec' : item.color_code"-->
<!-- [nzSrc]="item.avatar_url"-->
<!-- [nzTooltipPlacement]="'top'"-->
<!-- class="mt-auto mb-auto me-2"-->
<!-- ></nz-avatar>-->
<!-- <div style="line-height: 15px;">-->
<!-- <span class="d-block" nz-typography>{{item.name}}</span>-->
<!-- <small nz-typography nzType="secondary">{{item.email}}</small>-->
<!-- </div>-->
<!-- </div>-->
<!-- </nz-option>-->
<!-- </nz-select>-->
<!-- <ng-template #notFoundContentTemplate>-->
<!-- <button nz-button nzType="primary" nzBlock [disabled]="!isValueIsAnEmail() || inviting" [nzLoading]="inviting"-->
<!-- (click)="sendInvitation()">-->
<!-- <span nz-icon nzType="mail" nzTheme="outline"></span> {{buttonText}}-->
<!-- </button>-->
<!-- <div nz-typography nzType="secondary" class="mt-2 mb-0" style="font-size: 12px;">-->
<!-- Invitees will be added to the team and project either they accept the invitation or not.-->
<!-- </div>-->
<!-- </ng-template>-->
<!-- </nz-form-item>-->
<nz-table [nzData]="teamMembers.controls" [nzFrontPagination]="false" [formArrayName]="'teamMembers'"
[nzNoResult]="emptyData">
<thead>
<tr>
<th>User</th>
<th style="width: 150px;">Role</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of teamMembers.controls; let i = index;" [formGroupName]="i">
<td>
<nz-avatar class="me-2" [nzSize]="28" [nzText]="item.value.name | firstCharUpper"
[nzSrc]="item.value.avatar_url"
[style.background-color]="item.value.avatar_url ? '#ececec' : getColor('name')"></nz-avatar>
<nz-badge>{{item.value.name}}</nz-badge>
</td>
<td>
<nz-select style="width: 150px;" [formControlName]="'role_name'" [attr.id]="'member_' + i"
[nzDisabled]="item.value.role_name === 'Owner'">
<nz-option nzValue="Admin" nzLabel="Admin"></nz-option>
<nz-option nzValue="Member" nzLabel="Member"></nz-option>
<nz-option nzValue="Owner" [nzDisabled]="item.value.role_name !== 'Owner'" nzLabel="Owner"></nz-option>
</nz-select>
</td>
<td>
<button *ngIf="item.value.role_name !== 'Owner'" (nzOnConfirm)="deleteTeamMember(item.value.id)"
nz-button nz-popconfirm nz-tooltip [nzOkText]="'Yes'"
[nzPopconfirmTitle]="'Are you sure?'" [nzSize]="'small'" [nzTooltipPlacement]="'top'"
[nzTooltipTitle]="'Delete'"
[nzType]="'default'">
<span nz-icon nzType="delete"></span>
</button>
</td>
</tr>
</tbody>
</nz-table>
</nz-form-item>
<button nz-button nzType="primary" nzBlock>Save</button>
</form>
</ng-container>
</nz-drawer>
<nz-drawer
[nzClosable]="true"
[nzVisible]="visibleNewTeam"
nzPlacement="right"
nzTitle="Create New Team"
[nzSize]="'large'"
[nzWidth]="'350px'"
(nzOnClose)="closeNewTeam()">
<ng-container *nzDrawerContent>
<form nz-form [formGroup]="form" (submit)="createTeam()">
<nz-form-item class="d-block">
<nz-form-label nzRequired>
Team name
</nz-form-label>
<nz-form-control>
<input nz-input type="text" [formControlName]="'name'">
</nz-form-control>
</nz-form-item>
<button nz-button nzType="primary" type="submit" nzBlock>Create</button>
</form>
</ng-container>
</nz-drawer>
<ng-template #emptyData></ng-template>

View File

@@ -1,11 +0,0 @@
nz-page-header-subtitle {
color: hsla(0, 0%, 0%, 0.85);
font-weight: 500;
font-size: 16px;
}
.no-data-img-holder {
width: 100px;
margin-top: 42px;
}

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {TeamsComponent} from './teams.component';
describe('TeamsComponent', () => {
let component: TeamsComponent;
let fixture: ComponentFixture<TeamsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TeamsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TeamsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,293 +0,0 @@
import {Component} from '@angular/core';
import {AvatarNamesMap, DEFAULT_PAGE_SIZE} from "@shared/constants";
import {FormArray, FormBuilder, FormGroup, Validators} from "@angular/forms";
import {AccountCenterApiService} from "@api/account-center-api.service";
import {NzTableQueryParams} from "ng-zorro-antd/table";
import {isValidateEmail, log_error} from "@shared/utils";
import {IOrganizationTeam, IOrganizationTeamMember, IOrganizationUser} from "@interfaces/account-center";
import {AppService} from "@services/app.service";
import {TeamsApiService} from "@api/teams-api.service";
import {AdminCenterService} from "../admin-center-service.service";
import {ITeamMemberCreateRequest} from "@interfaces/api-models/team-member-create-request";
import {TeamMembersApiService} from "@api/team-members-api.service";
import {ProjectMembersApiService} from "@api/project-members-api.service";
import {AuthService} from "@services/auth.service";
@Component({
selector: 'worklenz-teams',
templateUrl: './teams.component.html',
styleUrls: ['./teams.component.scss']
})
export class TeamsComponent {
visible = false;
visibleNewTeam = false;
teams: IOrganizationTeam[] = [];
currentTeam: IOrganizationTeam | null = null;
loading = false;
// Table sorting & pagination
total = 0;
pageSize = DEFAULT_PAGE_SIZE;
pageIndex = 1;
paginationSizes = [5, 10, 15, 20, 50, 100];
sortField: string | null = null;
sortOrder: string | null = null;
form!: FormGroup;
editTeamForm!: FormGroup;
searchForm!: FormGroup;
loadingTeamDetails = false;
teamData: IOrganizationTeam = {};
selectedTeam: IOrganizationTeam = {};
updatingTeam = false;
users: IOrganizationUser[] = []
totalUsers = 0;
searchingName: string | null = null;
projectId: string | null = null;
inviting = false;
get buttonText() {
return this.isValueIsAnEmail() ? 'Invite as a member' : 'Invite a new member by email';
}
constructor(
private readonly api: AccountCenterApiService,
private readonly teamMembersApi: TeamMembersApiService,
private readonly teamsApiService: TeamsApiService,
private fb: FormBuilder,
private app: AppService,
private readonly service: AdminCenterService,
private readonly membersApi: ProjectMembersApiService,
private readonly auth: AuthService
) {
this.app.setTitle("Admin Center - Teams");
this.form = this.fb.group({
name: [null, [Validators.required]]
});
this.editTeamForm = this.fb.group({
name: [null, [Validators.required]],
teamMembers: this.fb.array([]),
search: [null]
});
this.searchForm = this.fb.group({search: []});
this.searchForm.valueChanges.subscribe(() => this.getTeams());
this.editTeamForm.controls["search"]?.valueChanges.subscribe((value) => this.handleMemberSelect(value));
}
get teamMembers() {
return <FormArray>this.editTeamForm.get('teamMembers');
}
getColor(name?: string) {
return AvatarNamesMap[name?.charAt(0).toUpperCase() || 'A'];
}
async onQueryParamsChange(params: NzTableQueryParams) {
const {pageSize, pageIndex, sort} = params;
this.pageIndex = pageIndex;
this.pageSize = pageSize;
const currentSort = sort.find(item => item.value !== null);
this.sortField = (currentSort && currentSort.key) || null;
this.sortOrder = (currentSort && currentSort.value) || null;
await this.getTeams();
}
async getTeams() {
try {
this.loading = true;
const res = await this.api.getOrganizationTeams(this.pageIndex, this.pageSize, this.sortField, this.sortOrder, this.searchForm.value.search);
if (res.done) {
this.total = res.body.total || 0;
this.teams = res.body.data?.filter(t => t.id !== this.auth.getCurrentSession()?.team_id) || [];
this.currentTeam = res.body.current_team_data || null;
this.loading = false;
}
} catch (e) {
this.loading = false;
log_error(e);
}
}
async createTeam() {
if (!this.form.value || !this.form.value.name || this.form.value.name.trim() === "") return;
try {
if (this.form.valid) {
this.loading = true;
const res = await this.teamsApiService.create({name: this.form.value.name});
if (res.done) {
this.closeNewTeam();
void this.getTeams();
this.service.emitCreateTeam();
}
} else {
this.app.displayErrorsOf(this.form);
}
} catch (e) {
this.loading = false;
log_error(e);
}
this.loading = false;
}
async openTeamDrawer(team: IOrganizationTeam) {
if (!team.id) return;
try {
this.loadingTeamDetails = true;
this.selectedTeam = team;
this.getTeamMembers(team);
} catch (e) {
this.loadingTeamDetails = false;
log_error(e);
}
this.visible = true;
}
async getTeamMembers(team: IOrganizationTeam) {
if (!team.id) return;
try {
const res = await this.api.getOrganizationTeam(team.id);
if (res.done) {
this.teamMembers.clear();
this.teamData = res.body;
this.total = this.teamData.team_members?.length || 0;
this.editTeamForm.patchValue({name: this.teamData.name});
if (this.teamData.team_members?.map((member: IOrganizationTeamMember) => {
const tempForm = this.fb.group({
id: member.id,
user_id: member.user_id,
name: member.name,
role_name: member.role_name,
avatar_url: member.avatar_url
});
this.teamMembers.push(tempForm);
}))
this.loadingTeamDetails = false;
}
} catch (e) {
this.loadingTeamDetails = false;
}
}
close(): void {
this.teamMembers.clear();
this.editTeamForm.reset();
this.visible = false;
}
openNewTeam() {
this.visibleNewTeam = true;
}
closeNewTeam() {
this.visibleNewTeam = false;
this.form.reset();
}
async submit() {
if (!this.teamData.id) return;
if (!this.editTeamForm.value || !this.editTeamForm.value.name || this.editTeamForm.value.name.trim() === "") return;
try {
this.updatingTeam = true;
const res = await this.api.updateTeam(this.teamData.id, this.editTeamForm.value);
if (res.done) {
this.service.emitTeamNameChange({teamId: this.teamData.id, teamName: this.editTeamForm.value.name});
this.close();
void this.getTeams();
}
} catch (e) {
log_error(e);
}
}
async deleteTeam(id: string | undefined) {
if (!id) return;
try {
const res = await this.api.deleteTeam(id);
if (res.done) {
await this.getTeams();
}
} catch (e) {
log_error(e);
}
}
async handleMemberSelect(value: string) {
if (!value || !this.selectedTeam.id) return;
if (this.editTeamForm.valid) {
try {
this.loading = true;
const body: ITeamMemberCreateRequest = {
job_title: null,
emails: [value],
is_admin: false
};
const res = await this.teamMembersApi.addTeamMember(this.selectedTeam.id, body);
if (res.done) {
this.editTeamForm.controls["search"]?.setValue(null);
this.getTeamMembers(this.selectedTeam);
}
this.loading = false;
} catch (e) {
log_error(e);
this.loading = false;
}
} else {
this.app.displayErrorsOf(this.form);
}
}
async deleteTeamMember(id: string | undefined) {
if (!id || !this.selectedTeam.id) return;
try {
const res = await this.api.removeTeamMember(id, this.selectedTeam.id);
if (res.done) {
if (id === this.auth.getCurrentSession()?.team_member_id) {
window.location.reload();
} else {
await this.getTeamMembers(this.selectedTeam);
}
}
} catch (e) {
log_error(e);
}
}
isValueIsAnEmail() {
if (!this.searchingName) return false;
return isValidateEmail(this.searchingName);
}
async sendInvitation() {
if (!this.projectId) return;
if (typeof this.searchingName !== "string" || !this.searchingName.length) return;
try {
const email = this.searchingName.trim().toLowerCase();
const request = {
project_id: this.projectId,
email
};
this.inviting = true;
const res = await this.membersApi.createByEmail(request);
this.inviting = false;
if (res.done) {
// this.resetSearchInput();
}
} catch (e) {
this.inviting = false;
}
}
}

View File

@@ -1,75 +0,0 @@
<nz-page-header [nzGhost]="false" class="px-0">
<nz-page-header-title>Users</nz-page-header-title>
</nz-page-header>
<nz-page-header class="site-page-header pt-0 ps-0">
<nz-page-header-subtitle>{{total}} users</nz-page-header-subtitle>
<nz-page-header-extra>
<nz-space>
<button *nzSpaceItem nz-button nz-tooltip nzShape="circle" (click)="getUsers()"
nzTooltipTitle="Refresh users" nzType="default">
<span nz-icon nzTheme="outline" nzType="sync"></span>
</button>
<form *nzSpaceItem [formGroup]="searchForm" [nzLayout]="'vertical'" nz-form>
<nz-input-group [nzSuffix]="suffixIconSearch">
<input [formControlName]="'search'" nz-input placeholder="Search by name" type="text"/>
</nz-input-group>
<ng-template #suffixIconSearch>
<span nz-icon nzType="search"></span>
</ng-template>
</form>
</nz-space>
</nz-page-header-extra>
</nz-page-header>
<nz-card>
<nz-skeleton [nzActive]="false" [nzLoading]="false">
<nz-table #usersTable
(nzQueryParams)="onQueryParamsChange($event)"
[nzData]="users || []"
[nzFrontPagination]="false"
[nzLoading]="loading"
[nzPageIndex]="pageIndex"
[nzPageSizeOptions]="paginationSizes"
[nzPageSize]="pageSize"
[nzTotal]="total"
class="custom-table"
nzShowSizeChanger
nzSize="small" [nzNoResult]="noDataTemplate">
<thead>
<tr>
<th scope="col">User</th>
<th scope="col">Email</th>
<th scope="col">Last Activity</th>
</tr>
</thead>
<tbody>
<tr class="actions-row" *ngFor="let item of usersTable.data">
<td class="cursor-pointer">
<nz-avatar class="me-2" [nzSize]="28" nzText="{{item.name | firstCharUpper}}"
[nzSrc]="item.avatar_url"
[style.background-color]="item.avatar_url ? '#ececec' : getColor(item.name)"></nz-avatar>
<nz-badge>
{{item.name}}
<span nz-typography
[nzType]="'secondary'">{{item.is_admin ? '(Admin)' : item.is_owner ? '(Owner)' : ''}}</span>
</nz-badge>
</td>
<td class="cursor-pointer">
{{item.email}}
</td>
<td class="cursor-pointer">
{{(item.last_logged | date: 'medium') || '-'}}
</td>
</tr>
</tbody>
</nz-table>
</nz-skeleton>
</nz-card>
<ng-template #noDataTemplate>
<div class="pt-4 pb-5">
<div class="no-data-img-holder mx-auto mb-3">
<img src="/assets/images/empty-box.webp" class="img-fluid" alt="">
</div>
<span nz-typography class="no-data-text">No users found in the organization.</span>
</div>
</ng-template>

View File

@@ -1,45 +0,0 @@
.you-text {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.admin-text {
font-size: 12px;
background-color: rgba(250, 173, 20, 0.1);
font-weight: 500;
padding: 0px 4px;
border-radius: 12px;
color: #FAAD14;
}
.ant-dropdown {
position: relative;
margin: 0;
padding: 4px 0;
text-align: left;
list-style-type: none;
background-color: #fff;
background-clip: padding-box;
border-radius: 2px;
outline: none;
box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d;
}
.ant-form-item-label > label {
float: left !important;
}
.role-selector {
margin-top: 24px;
}
nz-page-header-subtitle {
color: hsla(0, 0%, 0%, 0.85);
font-weight: 500;
font-size: 16px;
}
.no-data-img-holder {
width: 100px;
margin-top: 42px;
}

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {UsersComponent} from './users.component';
describe('UsersComponent', () => {
let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UsersComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UsersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,75 +0,0 @@
import {Component} from '@angular/core';
import {IOrganizationUser} from "@interfaces/account-center";
import {AvatarNamesMap, DEFAULT_PAGE_SIZE} from "@shared/constants";
import {NzTableQueryParams} from "ng-zorro-antd/table";
import {log_error} from "@shared/utils";
import {AccountCenterApiService} from "@api/account-center-api.service";
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
@Component({
selector: 'worklenz-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.scss']
})
export class UsersComponent {
visible = false;
visibleNewMember = false;
users: IOrganizationUser[] = []
loading = false;
// Table sorting & pagination
total = 0;
pageSize = DEFAULT_PAGE_SIZE;
pageIndex = 1;
paginationSizes = [5, 10, 15, 20, 50, 100];
sortField: string | null = null;
sortOrder: string | null = null;
form!: FormGroup;
searchForm!: FormGroup;
constructor(
private api: AccountCenterApiService,
private fb: FormBuilder,
) {
this.form = this.fb.group({
name: [null, [Validators.required]]
});
this.searchForm = this.fb.group({search: []});
this.searchForm.valueChanges.subscribe(() => this.getUsers());
}
getColor(name?: string) {
return AvatarNamesMap[name?.charAt(0).toUpperCase() || 'A'];
}
async onQueryParamsChange(params: NzTableQueryParams) {
const {pageSize, pageIndex, sort} = params;
this.pageIndex = pageIndex;
this.pageSize = pageSize;
const currentSort = sort.find(item => item.value !== null);
this.sortField = (currentSort && currentSort.key) || null;
this.sortOrder = (currentSort && currentSort.value) || null;
await this.getUsers();
}
async getUsers() {
try {
this.loading = true;
const res = await this.api.getOrganizationUsers(this.pageIndex, this.pageSize, this.sortField, this.sortOrder, this.searchForm.value.search);
if (res.done) {
this.total = res.body.total || 0;
this.users = res.body.data || [];
this.loading = false;
}
} catch (e) {
this.loading = false;
log_error(e);
}
}
}

View File

@@ -1,55 +0,0 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {LayoutComponent} from './layout/layout.component';
import {TeamOwnerOrAdminGuard} from '../guards/team-owner-or-admin-guard.service';
import {TeamNameGuard} from '../guards/team-name.guard';
const routes: Routes = [
{
path: '',
component: LayoutComponent,
canActivate: [TeamNameGuard],
children: [
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: 'dashboard', redirectTo: 'home', pathMatch: 'full'}, // Remove after a couple releases
{
path: 'home',
loadChildren: () => import('./my-dashboard/my-dashboard.module').then(m => m.MyDashboardModule)
},
{
path: 'projects',
canActivate: [TeamOwnerOrAdminGuard],
loadChildren: () => import('./projects/projects.module').then(m => m.ProjectsModule)
},
{
path: 'settings',
loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule)
},
{
path: 'schedule',
canActivate: [TeamOwnerOrAdminGuard],
loadChildren: () => import('./schedule/schedule.module').then(m => m.ScheduleModule)
},
{
path: 'reporting',
canActivate: [TeamOwnerOrAdminGuard],
loadChildren: () => import('./reporting/reporting.module').then(m => m.ReportingModule)
}, {
path: 'admin-center',
canActivate: [TeamOwnerOrAdminGuard],
loadChildren: () => import('./admin-center/admin-center.module').then(m => m.AdminCenterModule)
}
]
},
{
path: 'setup',
loadChildren: () => import('./account-setup/account-setup.module').then(m => m.AccountSetupModule)
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdministratorRoutingModule {
}

View File

@@ -1,91 +0,0 @@
import {NgModule} from '@angular/core';
import {CommonModule, NgOptimizedImage} from '@angular/common';
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {NotificationsDrawerComponent} from "./layout/notifications-drawer/notifications-drawer.component";
import {AdministratorRoutingModule} from './administrator-routing.module';
import {LayoutComponent} from './layout/layout.component';
import {NzSpinModule} from "ng-zorro-antd/spin";
import {NzAffixModule} from "ng-zorro-antd/affix";
import {NzAlertModule} from "ng-zorro-antd/alert";
import {NzLayoutModule} from "ng-zorro-antd/layout";
import {NzMenuModule} from "ng-zorro-antd/menu";
import {NzTypographyModule} from "ng-zorro-antd/typography";
import {NzToolTipModule} from "ng-zorro-antd/tooltip";
import {NzDropDownModule} from "ng-zorro-antd/dropdown";
import {NzIconModule} from "ng-zorro-antd/icon";
import {NzBadgeModule} from "ng-zorro-antd/badge";
import {NzAvatarModule} from "ng-zorro-antd/avatar";
import {NzBreadCrumbModule} from "ng-zorro-antd/breadcrumb";
import {NzDrawerModule} from "ng-zorro-antd/drawer";
import {NzEmptyModule} from "ng-zorro-antd/empty";
import {NzSpaceModule} from "ng-zorro-antd/space";
import {NzButtonModule} from "ng-zorro-antd/button";
import {FromNowPipe} from "@pipes/from-now.pipe";
import {NzMessageServiceModule} from "ng-zorro-antd/message";
import {AlertsComponent} from './layout/alerts/alerts.component';
import {HeaderComponent} from './layout/header/header.component';
import {SafeStringPipe} from "@pipes/safe-string.pipe";
import {NzTagModule} from "ng-zorro-antd/tag";
import {
NotificationTemplateComponent
} from './layout/notifications-drawer/notification-template/notification-template.component';
import {TagBackgroundPipe} from './layout/notifications-drawer/tag-background.pipe';
import {NzSegmentedModule} from "ng-zorro-antd/segmented";
import {NzDividerModule} from "ng-zorro-antd/divider";
import {NzSkeletonModule} from "ng-zorro-antd/skeleton";
import {CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport} from "@angular/cdk/scrolling";
import {LicensingAlertsComponent} from './layout/licensing-alerts/licensing-alerts.component';
import {NzResultModule} from "ng-zorro-antd/result";
import {NzListModule} from "ng-zorro-antd/list";
import {TeamMembersFormComponent} from "@admin/components/team-members-form/team-members-form.component";
@NgModule({
declarations: [
LayoutComponent,
AlertsComponent,
HeaderComponent,
NotificationTemplateComponent,
TagBackgroundPipe,
LicensingAlertsComponent,
NotificationsDrawerComponent,
],
exports: [],
imports: [
CommonModule,
AdministratorRoutingModule,
FormsModule,
ReactiveFormsModule,
NzSpinModule,
NzAffixModule,
NzAlertModule,
NzLayoutModule,
NzMenuModule,
NzTypographyModule,
NzToolTipModule,
NzDropDownModule,
NzIconModule,
NzBadgeModule,
NzAvatarModule,
NzBreadCrumbModule,
NzDrawerModule,
NzEmptyModule,
NzMessageServiceModule,
NzSpaceModule,
NzButtonModule,
FromNowPipe,
NgOptimizedImage,
SafeStringPipe,
NzTagModule,
NzSegmentedModule,
NzDividerModule,
NzSkeletonModule,
CdkVirtualScrollViewport,
CdkVirtualForOf,
CdkFixedSizeVirtualScroll,
NzResultModule,
NzListModule,
TeamMembersFormComponent
]
})
export class AdministratorModule {
}

View File

@@ -1,29 +0,0 @@
<nz-avatar-group>
<ng-container [ngSwitch]="showDot">
<ng-container *ngSwitchCase="true">
<nz-badge
*ngFor="let item of names || []" [nzOffset]="[-4,24]"
[nzStyle]="{background:'#52c41a',border:'4px solid #52c41a'}" nzDot>
<ng-container *ngTemplateOutlet="templateRef; context: { $implicit: item }"></ng-container>
</nz-badge>
</ng-container>
<ng-container *ngSwitchCase="false">
<ng-container *ngFor="let item of names || []">
<ng-container *ngTemplateOutlet="templateRef; context: { $implicit: item }"></ng-container>
</ng-container>
</ng-container>
</ng-container>
</nz-avatar-group>
<ng-template let-item #templateRef>
<nz-avatar
[nzSize]="28"
[class]="avatarClass"
[nzText]="item.end ? item.name : (item.name | firstCharUpper)"
[style.background-color]="item.avatar_url ? '#ececec' : item.color_code"
[nzSrc]="item.avatar_url"
nz-tooltip
[nzTooltipTitle]="item.end && item.names ? item.names : item.name"
[nzTooltipPlacement]="'top'"
></nz-avatar>
</ng-template>

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {AvatarsComponent} from './avatars.component';
describe('AvatarsComponent', () => {
let component: AvatarsComponent;
let fixture: ComponentFixture<AvatarsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AvatarsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AvatarsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,31 +0,0 @@
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {NgForOf, NgSwitch, NgSwitchCase, NgTemplateOutlet} from "@angular/common";
import {NzAvatarModule} from "ng-zorro-antd/avatar";
import {NzToolTipModule} from "ng-zorro-antd/tooltip";
import {InlineMember} from "@interfaces/api-models/inline-member";
import {FirstCharUpperPipe} from "@pipes/first-char-upper.pipe";
import {NzBadgeModule} from "ng-zorro-antd/badge";
@Component({
selector: 'worklenz-avatars',
templateUrl: './avatars.component.html',
styleUrls: ['./avatars.component.scss'],
imports: [
NzAvatarModule,
NzToolTipModule,
NgForOf,
FirstCharUpperPipe,
NzBadgeModule,
NgSwitch,
NgTemplateOutlet,
NgSwitchCase
],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AvatarsComponent {
@Input() names: InlineMember[] = [];
@Input() avatarClass: string | null = null;
@Input() showDot = false;
}

View File

@@ -1,21 +0,0 @@
<form [formGroup]="form" [nzLayout]="'vertical'" nz-form>
<nz-form-item>
<nz-form-label [nzSpan]="null" [nzTooltipTitle]="'You can manage clients under settings.'">Client</nz-form-label>
<nz-form-control [nzSpan]="null">
<input [formControlName]="'name'" (ngModelChange)="search()" [nzAutocomplete]="jobTitlesAutoComplete"
nz-input
placeholder="Select client"/>
<nz-autocomplete #jobTitlesAutoComplete>
<nz-auto-option *ngIf="searching">
<span class="loading-icon" nz-icon nzType="loading"></span>
Loading Data...
</nz-auto-option>
<span *ngIf="!searching">
<nz-auto-option *ngIf="isNew" nzValue="{{ newName }}">+ ADD "{{ newName }}"</nz-auto-option>
<nz-auto-option *ngFor="let item of clients" [nzValue]="item.name">{{ item.name }}</nz-auto-option>
</span>
</nz-autocomplete>
</nz-form-control>
</nz-form-item>
</form>

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ClientsAutocompleteComponent} from './clients-autocomplete.component';
describe('ClientsAutocompleteComponent', () => {
let component: ClientsAutocompleteComponent;
let fixture: ComponentFixture<ClientsAutocompleteComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ClientsAutocompleteComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ClientsAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,88 +0,0 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {FormBuilder, FormGroup, ReactiveFormsModule} from "@angular/forms";
import {IClient} from "@interfaces/client";
import {ClientsApiService} from "@api/clients-api.service";
import {log_error} from "@shared/utils";
import {NzFormModule} from "ng-zorro-antd/form";
import {NzAutocompleteModule} from "ng-zorro-antd/auto-complete";
import {NzIconModule} from "ng-zorro-antd/icon";
import {NgForOf, NgIf} from "@angular/common";
import {NzInputModule} from "ng-zorro-antd/input";
@Component({
selector: 'worklenz-clients-autocomplete',
templateUrl: './clients-autocomplete.component.html',
styleUrls: ['./clients-autocomplete.component.scss'],
imports: [
ReactiveFormsModule,
NzFormModule,
NzAutocompleteModule,
NzIconModule,
NgIf,
NgForOf,
NzInputModule
],
standalone: true
})
export class ClientsAutocompleteComponent implements OnInit {
@Output() nameChange: EventEmitter<string> = new EventEmitter<string>();
@Input() name: string | null = null;
form!: FormGroup;
searching = false;
isNew = false;
newName: string | null = null;
clients: IClient[] = [];
total = 0;
constructor(
private api: ClientsApiService,
private fb: FormBuilder
) {
this.form = this.fb.group({
name: []
});
}
async ngOnInit() {
this.form.controls["name"].setValue(this.name || null);
this.form.get('name')?.valueChanges.subscribe((value) => {
if (value) {
this.newName = value;
this.isNew = !this.clients.some((i) => i.name === value);
return;
}
this.isNew = false;
});
await this.get();
}
async get() {
try {
const res = await this.api.get(1, 5, null, null, this.form.value.name || null);
if (res.done) {
this.clients = res.body.data || [];
this.total = this.clients.length;
}
} catch (e) {
log_error(e);
}
}
async search() {
this.emitChange();
this.searching = true;
await this.get();
this.searching = false;
}
private emitChange() {
if (this.form.valid)
this.nameChange.emit(this.form.value.name.trim());
}
}

View File

@@ -1,50 +0,0 @@
<nz-modal [(nzVisible)]="showConvertTasksModal" nzTitle="Choose a parent task" [nzStyle]="{ top: '20px' }"
(nzOnCancel)="closeModal()">
<ng-container *nzModalContent>
<ng-container *ngIf="converting">
<div class="spinner">
<nz-spin nzSimple [nzIndicator]="indicatorTemplate"></nz-spin>
</div>
</ng-container>
<div class="modal-content">
<div class="search-task">
<input [(ngModel)]="searchText" nz-input placeholder="Search by task name"/>
</div>
<div class="scrollable mt-3">
<div *ngFor="let group of groups; let i=index" class="task-group">
<div *ngIf="group.tasks.length" [class.active]="isExpanded[i]" nz-typography
[style.background]="group.color_code"
class="py-1 px-2 mb-0 ant-typography d-block btn" (click)="toggleGroup($event, i)">
<span class="accordion-icon" nz-icon nzType="right" nzTheme="outline"></span> {{group.name}}
</div>
<div *ngIf="group.tasks.length" [class.show]="isExpanded[i]" class="mt-0 mb-3 panel" #panel>
<div class="panel-left-border" [style.background]="group.color_code"></div>
<ul nz-menu class="border-bottom">
<ng-container *ngFor="let item of group.tasks | searchByName: searchText;">
<li *ngIf="!item.parent_task_id && selectedTask?.id !== item?.id"
class="m-0 d-flex px-0 single-task-cont" nz-menu-item (click)="convertToSubTask(group.id, item.id)">
<div class="d-flex align-items-center justify-content-center hover-bg-change px-2 ">
<div class="d-flex" style="width:90px;">
<nz-tag nz-tooltip [nzTooltipTitle]="item.task_key"
style="width:auto; max-width: 90px; overflow:hidden; text-overflow: ellipsis;font-size: 12px;">{{item.task_key}}</nz-tag>
</div>
<div nz-tooltip [nzTooltipTitle]="item.name"
style="width: 340px; overflow:hidden; text-overflow: ellipsis;">
<span nz-typography>{{item.name}}</span>
</div>
</div>
</li>
</ng-container>
</ul>
</div>
</div>
</div>
</div>
</ng-container>
<div *nzModalFooter>
</div>
</nz-modal>
<ng-template #indicatorTemplate><span nz-icon nzType="loading"></span></ng-template>

View File

@@ -1,86 +0,0 @@
.scrollable {
max-height: 65vh;
overflow-y: auto;
}
label span {
display: flex;
}
.hover-bg-change:hover {
background-color: #f5f5f5;
}
.spinner {
position: absolute;
z-index: 9;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgb(255 255 255 / 30%);
display: flex;
align-items: center;
justify-content: center;
}
.panel {
position: relative;
padding: 0 0;
background-color: white;
max-height: 0px;
overflow: hidden;
transition: max-height 0.1s ease-out;
border-right: 1px solid #f0f0f0;
}
.panel.show {
transition: max-height 0.1s ease-out;
max-height: 100%;
}
.panel-left-border {
position: absolute;
content: '';
top: 0;
bottom: 0;
width: 3px;
z-index: 3;
border-bottom-left-radius: 4px;
}
.btn {
width: max-content;
padding-left: 16px !important;
padding-right: 32px !important;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
cursor: pointer;
}
.btn.active {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.btn .accordion-icon {
transform: rotate(0deg);
}
.btn.active .accordion-icon {
transform: rotate(90deg);
}
.single-task-cont {
border-top: 1px solid #f0f0f0;
padding-top: 3px;
padding-bottom: 3px;
height: auto;
}
.border-bottom {
border-bottom: 1px solid #f0f0f0;
border-right: none !important;
}

View File

@@ -1,25 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ConvertToSubtaskModalComponent} from './convert-to-subtask-modal.component';
describe('ConvertToSubtaskModalComponent', () => {
let component: ConvertToSubtaskModalComponent;
let fixture: ComponentFixture<ConvertToSubtaskModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ConvertToSubtaskModalComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ConvertToSubtaskModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,229 +0,0 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, NgZone, OnDestroy, Renderer2} from '@angular/core';
import {NzListModule} from "ng-zorro-antd/list";
import {NzButtonModule} from "ng-zorro-antd/button";
import {NgForOf, NgIf} from "@angular/common";
import {NzSkeletonModule} from "ng-zorro-antd/skeleton";
import {NzSpinModule} from "ng-zorro-antd/spin";
import {NzModalModule} from 'ng-zorro-antd/modal';
import {TaskListHashMapService} from "../../modules/task-list-v2/task-list-hash-map.service";
import {ITaskListConfigV2, ITaskListGroup} from 'app/administrator/modules/task-list-v2/interfaces';
import {TasksApiService} from '@api/tasks-api.service';
import {TaskListV2Service} from 'app/administrator/modules/task-list-v2/task-list-v2.service';
import {IServerResponse} from '@interfaces/api-models/server-response';
import {NzInputModule} from 'ng-zorro-antd/input';
import {SearchByNamePipe} from "../../../pipes/search-by-name.pipe";
import {NzTagModule} from 'ng-zorro-antd/tag';
import {NzToolTipModule} from 'ng-zorro-antd/tooltip';
import {SocketEvents} from '@shared/socket-events';
import {Socket} from 'ngx-socket-io';
import {
SubtaskConvertService
} from 'app/administrator/modules/task-list-v2/task-list-table/task-list-context-menu/subtask-convert-service.service';
import {Subject, takeUntil} from 'rxjs';
import {IProjectTask} from '@interfaces/api-models/project-tasks-view-model';
import {
ISubtaskConvertRequest
} from 'app/administrator/modules/task-list-v2/task-list-table/task-list-context-menu/interfaces/convert-subtask-request';
import {NzMenuModule} from 'ng-zorro-antd/menu';
import {FormsModule} from '@angular/forms';
import {NzIconModule} from 'ng-zorro-antd/icon';
import {KanbanV2Service} from 'app/administrator/modules/kanban-view-v2/kanban-view-v2.service';
import {AuthService} from "@services/auth.service";
@Component({
selector: 'worklenz-convert-to-subtask-modal',
templateUrl: './convert-to-subtask-modal.component.html',
styleUrls: ['./convert-to-subtask-modal.component.scss'],
standalone: true,
imports: [
NzListModule,
NzButtonModule,
NgIf,
NgForOf,
NzSkeletonModule,
NzSpinModule,
NzModalModule,
NzInputModule,
SearchByNamePipe,
NzTagModule,
NzToolTipModule,
NzMenuModule,
FormsModule,
NzIconModule
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ConvertToSubtaskModalComponent implements OnDestroy {
projectId?: string | null;
searchText: string | null = null;
selectedTaskId: string | null = null;
selectedTask?: IProjectTask | null;
showConvertTasksModal = false;
loadingGroups = false;
converting = false;
protected groupIds: string[] = [];
isExpanded: boolean[] = [];
groups: ITaskListGroup[] = [];
private readonly destroy$ = new Subject<void>();
constructor(
private readonly map: TaskListHashMapService,
private readonly api: TasksApiService,
private readonly service: TaskListV2Service,
private readonly subTaskConvertService: SubtaskConvertService,
private readonly cdr: ChangeDetectorRef,
private readonly socket: Socket,
private readonly ngZone: NgZone,
private readonly kanbanService: KanbanV2Service,
private readonly auth: AuthService,
) {
this.subTaskConvertService.onConvertingSubtask
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.getTaskData(value);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
getTaskData(value: ISubtaskConvertRequest) {
this.projectId = value.projectId;
this.selectedTask = value.selectedTask;
void this.getGroups();
}
async getGroups() {
if (!this.projectId) return;
try {
this.map.deselectAll();
this.loadingGroups = true;
const config = this.getConf();
const res = await this.api.getTaskListV2(config) as IServerResponse<ITaskListGroup[]>;
if (res.done) {
this.groups = res.body;
this.groupIds = res.body.map(g => g.id);
await this.mapTasks(this.service.groups);
this.showConvertTasksModal = true;
}
this.loadingGroups = false;
} catch (e) {
this.loadingGroups = false;
}
this.cdr.detectChanges();
}
private getConf(parentTaskId?: string): ITaskListConfigV2 {
const config: ITaskListConfigV2 = {
id: this.projectId as string,
group: this.service.getCurrentGroup().value,
field: null,
order: null,
search: null,
statuses: null,
members: null,
projects: null,
isSubtasksInclude: false
};
if (parentTaskId)
config.parent_task = parentTaskId;
return config;
}
private mapTasks(groups: ITaskListGroup[]) {
for (const group of groups) {
this.map.registerGroup(group);
for (const task of group.tasks) {
if (task.start_date) task.start_date = new Date(task.start_date) as any;
if (task.end_date) task.end_date = new Date(task.end_date) as any;
}
}
setTimeout(() => {
// expanding panels after groups loaded
this.isExpanded = this.groups.map(() => true);
}, 50);
}
async convertToSubTask(toGroupId: string, parentTaskId?: string) {
const selectedTask = this.selectedTask;
if (!selectedTask) return;
const groupBy = this.service.getCurrentGroup();
if (groupBy.value === this.service.GROUP_BY_STATUS_VALUE) {
this.handleStatusChange(toGroupId, this.selectedTask?.id);
} else if (groupBy.value === this.service.GROUP_BY_PRIORITY_VALUE) {
this.handlePriorityChange(toGroupId, this.selectedTask?.id as string);
}
try {
this.converting = true;
const res = await this.api.convertToSubTask(
selectedTask.id as string,
selectedTask.project_id as string,
parentTaskId as string,
groupBy.value,
toGroupId
);
if (res.done) {
this.service.updateTaskGroup(res.body, false);
if (groupBy.value === this.service.GROUP_BY_PHASE_VALUE)
this.service.emitRefresh();
}
this.reset();
} catch (e) {
this.converting = false;
}
this.kanbanService.emitRefreshGroups();
this.cdr.detectChanges();
}
handleStatusChange(statusId: string, taskId?: string) {
if (!taskId) return;
this.socket.emit(SocketEvents.TASK_STATUS_CHANGE.toString(), JSON.stringify({
task_id: taskId,
status_id: statusId,
team_id: this.auth.getCurrentSession()?.team_id
}));
this.socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), taskId);
}
handlePriorityChange(priorityId: string, taskId: string) {
this.socket.emit(SocketEvents.TASK_PRIORITY_CHANGE.toString(), JSON.stringify({
task_id: taskId,
priority_id: priorityId
}));
}
closeModal() {
this.showConvertTasksModal = false;
}
reset() {
this.converting = false;
this.showConvertTasksModal = false;
this.loadingGroups = false;
this.groups = [];
this.groupIds = [];
this.searchText = null;
this.selectedTaskId = null;
}
toggleGroup(event: MouseEvent, index: number) {
this.ngZone.runOutsideAngular(() => {
const target = event.target as Element;
if (!target) return;
this.isExpanded[index] = !this.isExpanded[index];
});
}
}

View File

@@ -1,48 +0,0 @@
<nz-drawer
[nzBodyStyle]="{ overflow: 'auto' }"
[nzWidth]="650"
[nzVisible]="drawerVisible"
[nzTitle]="'Import tasks'"
[nzFooter]="footerTpl"
(nzOnClose)="closeDrawer()"
(nzVisibleChange)="onVisibleChange($event)"
>
<form nz-form *nzDrawerContent [formGroup]="form">
<div nz-row [nzGutter]="8">
<div nz-col nzSpan="24">
<nz-form-item class="mb-4">
<nz-form-label>Select Template</nz-form-label>
<nz-form-control nzErrorTip="Please select a template!">
<nz-select name="template" [nzPlaceHolder]="'Please select a template to load tasks'" #templateSelect
[formControlName]="'template'" [nzLoading]="loadingTemplates">
<nz-option *ngFor="let template of templates; let i = index;" [nzLabel]="template.name | safeString"
[nzValue]="template.id"></nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-divider></nz-divider>
<span nz-typography class="fw-bold">Selected Tasks ({{tasks.length}})</span>
<ul nz-list nzBordered class="mt-4" [nzDataSource]="tasks" [nzLoading]="loadingData"
[nzNoResult]="'No template selected! Please select a template to load tasks.'">
<li nz-list-item *ngFor="let task of tasks; let i = index;">
<ul nz-list-item-actions>
<nz-list-item-action>
<a (click)="removeTask(i)">Remove</a>
</nz-list-item-action>
</ul>
{{task.name}}
</li>
</ul>
</div>
</div>
</form>
<ng-template #footerTpl>
<div style="float: right">
<button nz-button style="margin-right: 8px;" (click)="closeDrawer()">Cancel</button>
<button nz-button nzType="primary" (click)="importFromTemplate()" [nzLoading]="importing">Import</button>
</div>
</ng-template>
</nz-drawer>

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ImportTasksTemplateComponent} from './import-tasks-template.component';
describe('ImportTasksTemplateComponent', () => {
let component: ImportTasksTemplateComponent;
let fixture: ComponentFixture<ImportTasksTemplateComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ImportTasksTemplateComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ImportTasksTemplateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,190 +0,0 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
NgZone,
OnDestroy,
Output,
ViewChild
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {NzDrawerModule} from "ng-zorro-antd/drawer";
import {NzGridModule} from "ng-zorro-antd/grid";
import {NzSelectComponent, NzSelectModule} from "ng-zorro-antd/select";
import {NzButtonModule} from "ng-zorro-antd/button";
import {NzFormModule} from "ng-zorro-antd/form";
import {TaskTemplatesService} from "@api/task-templates.service";
import {ITaskTemplatesGetResponse} from "@interfaces/api-models/task-templates-get-response";
import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
import {IProjectTask} from "@interfaces/api-models/project-tasks-view-model";
import {NzListModule} from "ng-zorro-antd/list";
import {NzEmptyModule} from "ng-zorro-antd/empty";
import {AppService} from "@services/app.service";
import {NzDividerModule} from "ng-zorro-antd/divider";
import {SafeStringPipe} from "@pipes/safe-string.pipe";
import {NzTypographyModule} from "ng-zorro-antd/typography";
import {DRAWER_ANIMATION_INTERVAL} from "@shared/constants";
@Component({
selector: 'worklenz-import-tasks-template',
templateUrl: './import-tasks-template.component.html',
styleUrls: ['./import-tasks-template.component.scss'],
standalone: true,
imports: [CommonModule, NzDrawerModule, NzGridModule, NzSelectModule, NzButtonModule, NzFormModule, FormsModule, NzListModule, NzEmptyModule, ReactiveFormsModule, NzDividerModule, SafeStringPipe, NzTypographyModule],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImportTasksTemplateComponent implements OnDestroy {
@ViewChild("templateSelect", {static: false}) selectTemplate!: NzSelectComponent;
@Input() drawerVisible = false;
@Input() projectId: string | null = null;
@Output() onImportDone = new EventEmitter();
@Output() onCancel = new EventEmitter();
form!: FormGroup;
selectedId: string | null = null;
loadingTemplates = false;
loadingData = false;
importing = false;
templates: ITaskTemplatesGetResponse[] = [];
tasks: IProjectTask[] = [];
constructor(
private readonly api: TaskTemplatesService,
private readonly ngZone: NgZone,
private readonly app: AppService,
private readonly fb: FormBuilder,
private readonly cdr: ChangeDetectorRef
) {
this.form = this.fb.group({
template: [null, Validators.required],
});
this.form.get('template')?.valueChanges.subscribe(changes => {
if (changes) {
this.selectedId = changes;
this.templateSelected();
}
});
}
ngOnDestroy() {
this.reset();
this.cdr.markForCheck();
}
open(): void {
this.drawerVisible = true;
this.cdr.markForCheck();
}
closeDrawer(): void {
this.form.reset();
this.tasks = [];
this.onCancel.emit();
this.cdr.markForCheck();
}
onVisibleChange(event: boolean) {
if (event) {
void this.getTaskTemplate();
this.ngZone.runOutsideAngular(() => {
setTimeout(() => {
this.selectTemplate?.focus();
}, DRAWER_ANIMATION_INTERVAL)
});
}
this.cdr.markForCheck();
}
async getTaskTemplate() {
try {
this.loadingTemplates = true;
const res = await this.api.get();
if (res.done) {
this.templates = res.body;
this.loadingTemplates = false;
}
} catch (e) {
this.loadingTemplates = false;
}
this.cdr.markForCheck();
}
async getTemplateData() {
if (!this.selectedId) return;
try {
this.loadingData = true;
const res = await this.api.getById(this.selectedId);
if (res.done) {
this.tasks = res.body.tasks || [];
this.loadingData = false;
}
} catch (e) {
this.loadingData = false;
}
this.cdr.markForCheck();
}
templateSelected() {
void this.getTemplateData();
}
removeTask(index: number) {
if (this.tasks.length > 1) {
this.tasks.splice(index, 1);
} else {
this.tasks = [];
}
}
validateForm() {
for (const controlName in this.form.controls) {
this.form.controls[controlName].updateValueAndValidity();
}
this.cdr.markForCheck();
}
async importFromTemplate() {
if (!this.projectId) return;
try {
this.validateForm();
if (this.form.invalid) {
this.form.markAsTouched();
return;
}
if (this.tasks.length) {
this.importing = true;
const res = await this.api.import(this.projectId, this.tasks);
if (res.done) {
this.api.emitOnImport();
this.onImportDone.emit();
this.reset();
this.drawerVisible = false;
}
this.importing = false;
} else {
this.app.notify("Incomplete request!", "No tasks to import", false);
}
} catch (e) {
this.importing = false;
}
this.cdr.markForCheck();
}
private reset() {
this.tasks = [];
this.selectedId = null;
this.form.reset();
this.cdr.markForCheck();
}
}

View File

@@ -1,22 +0,0 @@
<form [formGroup]="form" [nzLayout]="'vertical'" nz-form>
<nz-form-item>
<nz-form-label [nzSpan]="null">Job Title</nz-form-label>
<nz-form-control [nzSpan]="null">
<input [formControlName]="'name'" (ngModelChange)="search()" [nzAutocomplete]="jobTitlesAutoComplete"
nz-input
[placeholder]="placeholder"/>
<nz-autocomplete #jobTitlesAutoComplete>
<nz-auto-option *ngIf="searching">
<span class="loading-icon" nz-icon nzType="loading"></span>
Loading Data...
</nz-auto-option>
<span *ngIf="!searching">
<nz-auto-option *ngIf="isNew" nzValue="{{ newTitle }}">+ ADD "{{ newTitle }}"</nz-auto-option>
<nz-auto-option *ngFor="let item of jobTitles" [nzValue]="item.name">{{ item.name }}</nz-auto-option>
</span>
</nz-autocomplete>
</nz-form-control>
</nz-form-item>
</form>

View File

@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {JobTitlesAutocompleteComponent} from './job-titles-autocomplete.component';
describe('JobTitlesAutocompleteComponent', () => {
let component: JobTitlesAutocompleteComponent;
let fixture: ComponentFixture<JobTitlesAutocompleteComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [JobTitlesAutocompleteComponent]
})
.compileComponents();
fixture = TestBed.createComponent(JobTitlesAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,100 +0,0 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {FormBuilder, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {JobTitlesApiService} from '@api/job-titles-api.service';
import {IJobTitle} from '@interfaces/job-title';
import {log_error} from "@shared/utils";
import {NzInputModule} from "ng-zorro-antd/input";
import {NgForOf, NgIf} from "@angular/common";
import {NzAutocompleteModule} from "ng-zorro-antd/auto-complete";
import {NzFormModule} from "ng-zorro-antd/form";
import {NzIconModule} from "ng-zorro-antd/icon";
@Component({
selector: 'worklenz-job-titles-autocomplete',
templateUrl: './job-titles-autocomplete.component.html',
styleUrls: ['./job-titles-autocomplete.component.scss'],
imports: [
NzInputModule,
NgIf,
ReactiveFormsModule,
NgForOf,
NzAutocompleteModule,
NzFormModule,
NzIconModule
],
standalone: true
})
export class JobTitlesAutocompleteComponent implements OnInit {
@Output() titleChange: EventEmitter<string> = new EventEmitter<string>();
@Input() title: string | null = null;
@Input() placeholder = "Job Title";
form!: FormGroup;
@Input() loading = false;
@Output() loadingChange: EventEmitter<boolean> = new EventEmitter<boolean>();
searching = false;
isNew = false;
newTitle: string | null = null;
jobTitles: IJobTitle[] = [];
total = 0;
constructor(
private api: JobTitlesApiService,
private fb: FormBuilder
) {
this.form = this.fb.group({
name: [null]
});
}
async ngOnInit() {
this.form.controls['name'].setValue(this.title || null);
this.form.get('name')?.valueChanges.subscribe((value) => {
if (value) {
this.newTitle = value;
this.isNew = !this.jobTitles.some((i) => i.name === value);
return;
}
this.isNew = false;
});
await this.get();
}
async get() {
try {
this.setLoading(true);
const res = await this.api.get(1, 5, null, null, this.form.value.name || null);
if (res.done) {
this.jobTitles = res.body.data || [];
this.total = this.jobTitles.length;
}
this.setLoading(false);
} catch (e) {
this.setLoading(false);
log_error(e);
}
}
async search() {
this.emitChange();
this.searching = true;
await this.get();
this.searching = false;
}
private setLoading(loading: boolean) {
this.loading = loading;
this.loadingChange.emit(this.loading);
}
private emitChange() {
if (this.form.valid)
this.titleChange.emit(this.form.value.name.trim());
}
}

View File

@@ -1 +0,0 @@
<span nz-typography [nzType]="'secondary'">-</span>

View File

@@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NaComponent } from './na.component';
describe('NaComponent', () => {
let component: NaComponent;
let fixture: ComponentFixture<NaComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NaComponent]
});
fixture = TestBed.createComponent(NaComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Some files were not shown because too many files have changed in this diff Show More