Init
This commit is contained in:
143
frontend/src/app/App.tsx
Normal file
143
frontend/src/app/App.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { generalSettingsStore } from '@/app';
|
||||
import { authStore } from '@/modules/auth';
|
||||
import {
|
||||
ErrorBoundary,
|
||||
GTMUtil,
|
||||
UserRole,
|
||||
WholePageLoaderWithLogo,
|
||||
envUtil,
|
||||
serverEventService,
|
||||
useCheckBrowserSupport,
|
||||
} from '@/shared';
|
||||
import { useDocumentTitle } from '@mantine/hooks';
|
||||
import { when } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Suspense, useEffect, useMemo } from 'react';
|
||||
import { Helmet, type HelmetProps } from 'react-helmet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { createRouter } from './routes';
|
||||
import { appStore } from './store';
|
||||
|
||||
const App = observer(() => {
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
useDocumentTitle(envUtil.appName);
|
||||
|
||||
const isBrowserSupported = useCheckBrowserSupport();
|
||||
|
||||
const router = useMemo(() => createRouter({ isBrowserSupported }), [isBrowserSupported]);
|
||||
|
||||
useEffect(() => {
|
||||
GTMUtil.initializeGTM();
|
||||
|
||||
authStore.startAuth();
|
||||
|
||||
when(
|
||||
() => authStore.isAuthenticated,
|
||||
() => {
|
||||
const { user: currentUser } = authStore;
|
||||
|
||||
if (currentUser && currentUser.role !== UserRole.PARTNER) serverEventService.connect();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
serverEventService.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
when(
|
||||
() => appStore.isLoaded,
|
||||
() => {
|
||||
const changeLanguage = (lng: string) => {
|
||||
i18n.changeLanguage(lng);
|
||||
|
||||
document.documentElement.lang = lng;
|
||||
};
|
||||
|
||||
const { accountSettings } = generalSettingsStore;
|
||||
|
||||
if (accountSettings && accountSettings.language !== i18n.language)
|
||||
changeLanguage(accountSettings.language);
|
||||
}
|
||||
);
|
||||
}, [i18n]);
|
||||
|
||||
const helmetHTMLAttributes = useMemo<HelmetProps['htmlAttributes']>(
|
||||
() => ({ lang: i18n.language }),
|
||||
[i18n.language]
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<WholePageLoaderWithLogo />}>
|
||||
<Helmet htmlAttributes={helmetHTMLAttributes}>
|
||||
<meta name="description" content={t('meta_description', { company: envUtil.appName })} />
|
||||
|
||||
{/* icons */}
|
||||
<link
|
||||
sizes="180x180"
|
||||
rel="apple-touch-icon"
|
||||
href={`/favicons/${envUtil.appNameLowerCase}/apple_touch_180x180.png`}
|
||||
/>
|
||||
<link
|
||||
sizes="167x167"
|
||||
rel="apple-touch-icon"
|
||||
href={`/favicons/${envUtil.appNameLowerCase}/apple_touch_167x167.png`}
|
||||
/>
|
||||
<link
|
||||
sizes="152x262"
|
||||
rel="apple-touch-icon"
|
||||
href={`/favicons/${envUtil.appNameLowerCase}/apple_touch_152x152.png`}
|
||||
/>
|
||||
<link
|
||||
sizes="120x120"
|
||||
rel="apple-touch-icon"
|
||||
href={`/favicons/${envUtil.appNameLowerCase}/apple_touch_120x120.png`}
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
sizes="16x16"
|
||||
type="image/svg+xml"
|
||||
href={`/favicons/${envUtil.appNameLowerCase}/favicon_16x16.svg`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
sizes="32x32"
|
||||
type="image/svg+xml"
|
||||
href={`/favicons/${envUtil.appNameLowerCase}/favicon_32x32.svg`}
|
||||
/>
|
||||
|
||||
{/* .png icons for Safari */}
|
||||
<link
|
||||
rel="icon"
|
||||
sizes="16x16"
|
||||
type="image/png"
|
||||
href={`/favicons/${envUtil.appNameLowerCase}/favicon_16x16.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
sizes="32x32"
|
||||
type="image/png"
|
||||
href={`/favicons/${envUtil.appNameLowerCase}/favicon_32x32.png`}
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
export { App };
|
||||
|
||||
// I was here through multiple MVPs and paradigm shifts. Some code was
|
||||
// written, and some architecture was developed, though not as well as it could
|
||||
// have been due to a lack of time and resources.
|
||||
|
||||
// If you're reading this and have any questions, feel free to reach out to me! 😎
|
||||
|
||||
// https://github.com/kr4chinin (2022-2024)
|
||||
83
frontend/src/app/api/ApiRoutes.ts
Normal file
83
frontend/src/app/api/ApiRoutes.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
export enum ApiRoutes {
|
||||
/* IAM START */
|
||||
|
||||
// subscription
|
||||
GET_SUBSCRIPTION = '/api/iam/subscriptions',
|
||||
GET_SUBSCRIPTION_FOR_ACCOUNT = '/api/iam/subscriptions/:accountId',
|
||||
GET_SUBSCRIPTION_PLANS = '/api/iam/subscriptions/stripe/plans',
|
||||
GET_SUBSCRIPTION_CHECKOUT_URL = '/api/iam/subscriptions/stripe/checkout',
|
||||
GET_SUBSCRIPTION_PORTAL_URL = '/api/iam/subscriptions/stripe/portal',
|
||||
GET_CURRENT_DISCOUNT = '/api/iam/subscriptions/discount/current',
|
||||
UPDATE_SUBSCRIPTION = '/api/iam/subscriptions/:accountId',
|
||||
// users
|
||||
GET_USERS = '/api/iam/users',
|
||||
GET_USER = '/api/iam/users/:id',
|
||||
ADD_USER = '/api/iam/users',
|
||||
UPDATE_USER = '/api/iam/users/:id',
|
||||
DELETE_USER = '/api/iam/users/:id',
|
||||
UPLOAD_USER_AVATAR = '/api/iam/users/:id/avatar',
|
||||
REMOVE_USER_AVATAR = '/api/iam/users/:id/avatar',
|
||||
CHANGE_USER_PASSWORD = '/api/iam/users/change-password',
|
||||
// user profile
|
||||
GET_USER_PROFILE = '/api/iam/users/:id/profile',
|
||||
UPDATE_USER_PROFILE = '/api/iam/users/:id/profile',
|
||||
|
||||
/* IAM END */
|
||||
|
||||
// builder
|
||||
ADD_ENTITY_TYPE = '/api/crm/entity-types',
|
||||
GET_FEATURES = '/api/crm/features',
|
||||
// feed
|
||||
GET_FEED_ITEMS = '/api/crm/entities/:entityId/events/:filter',
|
||||
// entity types
|
||||
GET_ENTITY_TYPES = '/api/crm/entity-types',
|
||||
GET_ENTITY_TYPE = '/api/crm/entity-types/:id',
|
||||
DELETE_ENTITY_TYPE = '/api/crm/entity-types/:id',
|
||||
UPDATE_ENTITY_TYPE = '/api/crm/entity-types/:id',
|
||||
UPDATE_ENTITY_TYPE_FIELDS = '/api/crm/entity-types/:id/fields',
|
||||
UPDATE_FIELDS_SETTINGS = '/api/crm/entity-types/:entityTypeId/fields-settings',
|
||||
// identity
|
||||
GET_ALL_IDENTITY_POOLS = '/api/crm/identities/all',
|
||||
GET_IDENTITY_POOL = '/api/crm/identities/:name',
|
||||
// boards
|
||||
GET_BOARDS = '/api/crm/boards',
|
||||
GET_BOARD = '/api/crm/boards/:id',
|
||||
ADD_BOARD = '/api/crm/boards',
|
||||
UPDATE_BOARD = '/api/crm/boards/:id',
|
||||
DELETE_BOARD = '/api/crm/boards/:id',
|
||||
// stages
|
||||
CREATE_STAGE = '/api/crm/boards/:boardId/stages',
|
||||
GET_STAGES = '/api/crm/boards/:boardId/stages',
|
||||
UPDATE_STAGE = '/api/crm/boards/:boardId/stages/:stageId',
|
||||
GET_STAGE = '/api/crm/boards/:boardId/stages/:stageId',
|
||||
DELETE_STAGE = '/api/crm/boards/:boardId/stages/:stageId',
|
||||
// storage
|
||||
GET_FILE_INFO = '/api/storage/info/:fileId',
|
||||
UPLOAD_FILES = '/api/storage/upload',
|
||||
DELETE_FILE = '/api/storage/file/:id',
|
||||
DELETE_FILE_LINK = '/api/crm/file-link/:id',
|
||||
DELETE_FILE_LINKS = '/api/crm/file-links',
|
||||
// form
|
||||
SEND_CONTACT_US_FORM = '/api/forms/contact-us',
|
||||
// account
|
||||
CREATE_ACCOUNT = '/api/iam/account',
|
||||
GET_ACCOUNT = '/api/iam/account',
|
||||
SEARCH_ACCOUNTS = '/api/iam/account/search',
|
||||
UPLOAD_ACCOUNT_LOGO = '/api/iam/account/logo',
|
||||
REMOVE_ACCOUNT_LOGO = '/api/iam/account/logo',
|
||||
GET_ACCOUNT_SETTINGS = '/api/iam/account/settings',
|
||||
UPDATE_ACCOUNT_SETTINGS = '/api/iam/account/settings',
|
||||
// general settings
|
||||
GET_DEMO_DATA_EXISTS = '/api/setup/demo-data/exists',
|
||||
DELETE_DEMO_DATA = '/api/setup/demo-data',
|
||||
// version
|
||||
GET_LATEST_FRONTEND_VERSION = '/api/support/version/frontend/latest',
|
||||
// frontend objects
|
||||
GET_FRONTEND_OBJECT = '/api/frontend/objects/:key',
|
||||
UPSERT_FRONTEND_OBJECT = '/api/frontend/objects',
|
||||
DELETE_FRONTEND_OBJECT = '/api/frontend/objects/:key',
|
||||
// dadata
|
||||
// proxy on https://dadata.ru/api/
|
||||
GET_BANK_REQUISITES = '/api/data-enrichment/requisites/bank',
|
||||
GET_ORG_REQUISITES = '/api/data-enrichment/requisites/org',
|
||||
}
|
||||
50
frontend/src/app/api/AppQueryKeys.ts
Normal file
50
frontend/src/app/api/AppQueryKeys.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Nullable, Optional } from '@/shared';
|
||||
|
||||
const queryKeys = {
|
||||
app: ['app'],
|
||||
latestFrontendVersion(currentVersion: string) {
|
||||
return [...this.app, 'latest-frontend-version', currentVersion];
|
||||
},
|
||||
|
||||
stages: ['stages'],
|
||||
stage({
|
||||
stageId,
|
||||
boardId,
|
||||
}: {
|
||||
stageId: Optional<Nullable<number>>;
|
||||
boardId: Optional<Nullable<number>>;
|
||||
}) {
|
||||
return [...this.stages, 'stage', boardId, stageId];
|
||||
},
|
||||
stagesByBoardId(boardId: Optional<Nullable<number>>) {
|
||||
return [...this.stages, 'board-id', boardId];
|
||||
},
|
||||
|
||||
boards: ['boards'],
|
||||
tasksBoards() {
|
||||
return [...this.boards, 'tasks'];
|
||||
},
|
||||
board(boardId: Optional<Nullable<number>>) {
|
||||
return [...this.boards, 'board', boardId];
|
||||
},
|
||||
boardsByEntityTypeId(entityTypeId: Optional<Nullable<number>>) {
|
||||
return [...this.boards, 'entity-type-id', entityTypeId];
|
||||
},
|
||||
|
||||
userProfiles: ['user-profiles'],
|
||||
userProfile(id: number) {
|
||||
return [...this.userProfiles, 'user-profile', id];
|
||||
},
|
||||
|
||||
fileInfo: ['file-info'],
|
||||
fileInfoById(fileId: string) {
|
||||
return [...this.fileInfo, fileId];
|
||||
},
|
||||
|
||||
subscriptions: ['subscriptions'],
|
||||
subscriptionByAccountId(accountId: number) {
|
||||
return [...this.subscriptions, accountId];
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const APP_QUERY_KEYS = Object.freeze(queryKeys);
|
||||
53
frontend/src/app/api/BaseApi/BaseApi.ts
Normal file
53
frontend/src/app/api/BaseApi/BaseApi.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { UrlUtil } from '@/shared';
|
||||
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
|
||||
|
||||
export const determineApiHost = (): string => {
|
||||
const hostname = UrlUtil.getCurrentHostname();
|
||||
|
||||
// for testing local backend
|
||||
if (hostname.includes('.loc')) return `http://${hostname}:8000`;
|
||||
|
||||
// Use relative URLs for production (empty string means relative to current domain)
|
||||
return import.meta.env.VITE_BASE_API_URL || '';
|
||||
};
|
||||
|
||||
class BaseApi {
|
||||
private _axios = axios.create({
|
||||
baseURL: determineApiHost(),
|
||||
headers: {
|
||||
'X-Api-Key': import.meta.env.VITE_BASE_API_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
get = async (url: string, config?: AxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
return this._axios.get(url, config);
|
||||
};
|
||||
|
||||
post = async (url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
return this._axios.post(url, data, config);
|
||||
};
|
||||
|
||||
put = async (url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
return this._axios.put(url, data, config);
|
||||
};
|
||||
|
||||
patch = async (url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
return this._axios.patch(url, data, config);
|
||||
};
|
||||
|
||||
delete = async (url: string, config?: AxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
return this._axios.delete(url, config);
|
||||
};
|
||||
|
||||
setAuthToken = (token: string): void => {
|
||||
this.setRequestHeader({ Authorization: `Bearer ${token}` });
|
||||
};
|
||||
|
||||
setRequestHeader = (data: Record<string, string>): void => {
|
||||
for (const key in data) {
|
||||
this._axios.defaults.headers.common[key] = data[key] as string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const baseApi = new BaseApi();
|
||||
53
frontend/src/app/api/BoardApi/BoardApi.ts
Normal file
53
frontend/src/app/api/BoardApi/BoardApi.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Board, BoardType, UrlTemplateUtil } from '@/shared';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import { type CreateBoardDto, type UpdateBoardDto } from '../dtos';
|
||||
|
||||
class BoardApi {
|
||||
getBoardsByEntityTypeId = async (entityTypeId: number): Promise<Board[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_BOARDS, {
|
||||
params: { recordId: entityTypeId },
|
||||
});
|
||||
|
||||
return Board.fromDtos(response.data);
|
||||
};
|
||||
|
||||
getTasksBoards = async (): Promise<Board[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_BOARDS, { params: { type: BoardType.TASK } });
|
||||
|
||||
return Board.fromDtos(response.data);
|
||||
};
|
||||
|
||||
getBoardById = async (id: number): Promise<Board> => {
|
||||
const response = await baseApi.get(UrlTemplateUtil.toPath(ApiRoutes.GET_BOARD, { id }));
|
||||
|
||||
return Board.fromDto(response.data);
|
||||
};
|
||||
|
||||
updateBoard = async ({
|
||||
dto,
|
||||
boardId,
|
||||
}: {
|
||||
dto: UpdateBoardDto;
|
||||
boardId: number;
|
||||
}): Promise<Board> => {
|
||||
const response = await baseApi.patch(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.UPDATE_BOARD, { id: boardId }),
|
||||
dto
|
||||
);
|
||||
|
||||
return Board.fromDto(response.data);
|
||||
};
|
||||
|
||||
addBoard = async (dto: CreateBoardDto): Promise<Board> => {
|
||||
const response = await baseApi.post(ApiRoutes.ADD_BOARD, dto);
|
||||
|
||||
return Board.fromDto(response.data);
|
||||
};
|
||||
|
||||
deleteBoard = async (id: number): Promise<void> => {
|
||||
await baseApi.delete(UrlTemplateUtil.toPath(ApiRoutes.DELETE_BOARD, { id }));
|
||||
};
|
||||
}
|
||||
|
||||
export const boardApi = new BoardApi();
|
||||
154
frontend/src/app/api/BoardApi/BoardApiUtil.ts
Normal file
154
frontend/src/app/api/BoardApi/BoardApiUtil.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { boardApi, type CreateBoardDto, stageApiUtil } from '@/app';
|
||||
import { queryClient } from '@/index';
|
||||
import type { Board, Nullable, Option, Optional } from '@/shared';
|
||||
import {
|
||||
queryOptions,
|
||||
skipToken,
|
||||
useQueries,
|
||||
useQuery,
|
||||
type UseQueryResult,
|
||||
} from '@tanstack/react-query';
|
||||
import { APP_QUERY_KEYS } from '../AppQueryKeys';
|
||||
import { UpdateBoardDto } from '../dtos';
|
||||
|
||||
const BOARDS_STALE_TIME = 10 * 60 * 1000;
|
||||
|
||||
// this class encapsulate all board-related queries and helpers
|
||||
// this is in fact static class, but do not refactor methods to static,
|
||||
// because this will violate the rules of hooks
|
||||
class BoardApiUtil {
|
||||
private _getBoardQueryOptions = (boardId: Optional<Nullable<number>>) => {
|
||||
return queryOptions({
|
||||
queryKey: APP_QUERY_KEYS.board(boardId),
|
||||
queryFn: boardId ? async (): Promise<Board> => boardApi.getBoardById(boardId) : skipToken,
|
||||
staleTime: BOARDS_STALE_TIME,
|
||||
});
|
||||
};
|
||||
|
||||
private _getBoardsByEntityTypeIdQueryOptions = ({
|
||||
entityTypeId,
|
||||
enabled,
|
||||
}: {
|
||||
entityTypeId: Optional<Nullable<number>>;
|
||||
enabled?: boolean;
|
||||
}) => {
|
||||
return queryOptions({
|
||||
queryKey: APP_QUERY_KEYS.boardsByEntityTypeId(entityTypeId),
|
||||
queryFn: entityTypeId
|
||||
? async (): Promise<Board[]> => boardApi.getBoardsByEntityTypeId(entityTypeId)
|
||||
: skipToken,
|
||||
staleTime: BOARDS_STALE_TIME,
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
private _getTasksBoardsQueryOptions = () => {
|
||||
return queryOptions({
|
||||
queryKey: APP_QUERY_KEYS.tasksBoards(),
|
||||
staleTime: BOARDS_STALE_TIME,
|
||||
queryFn: boardApi.getTasksBoards,
|
||||
});
|
||||
};
|
||||
|
||||
useGetBoard = (boardId: Optional<Nullable<number>>): UseQueryResult<Board, Error> => {
|
||||
return useQuery(this._getBoardQueryOptions(boardId));
|
||||
};
|
||||
|
||||
useGetBoardsByEntityTypeId = ({
|
||||
entityTypeId,
|
||||
enabled,
|
||||
}: {
|
||||
entityTypeId: Optional<Nullable<number>>;
|
||||
enabled?: boolean;
|
||||
}): UseQueryResult<Board[], Error> => {
|
||||
return useQuery(this._getBoardsByEntityTypeIdQueryOptions({ entityTypeId, enabled }));
|
||||
};
|
||||
|
||||
useGetBoardsByEntityTypeIds = (
|
||||
entityTypeIds: number[]
|
||||
): (UseQueryResult<Board[], Error> & { et?: number })[] => {
|
||||
return useQueries({
|
||||
queries: entityTypeIds.map(entityTypeId =>
|
||||
this._getBoardsByEntityTypeIdQueryOptions({ entityTypeId })
|
||||
),
|
||||
combine: res => res.map((r, idx) => ({ ...r, et: entityTypeIds[idx] })),
|
||||
});
|
||||
};
|
||||
|
||||
useGetBoardsByEntityTypeIdOptions = (
|
||||
entityTypeId: Optional<Nullable<number>>
|
||||
): Option<number>[] => {
|
||||
const { data: boards } = useQuery(this._getBoardsByEntityTypeIdQueryOptions({ entityTypeId }));
|
||||
|
||||
return (
|
||||
boards?.map<Option<number>>(b => ({
|
||||
value: b.id,
|
||||
label: b.name,
|
||||
})) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
useGetTasksBoards = (): UseQueryResult<Board[], Error> => {
|
||||
return useQuery(this._getTasksBoardsQueryOptions());
|
||||
};
|
||||
|
||||
getBoard = async (boardId: number): Promise<Board> => {
|
||||
return await queryClient.ensureQueryData(this._getBoardQueryOptions(boardId));
|
||||
};
|
||||
|
||||
invalidateBoards = async (): Promise<void> => {
|
||||
await queryClient.invalidateQueries({ queryKey: APP_QUERY_KEYS.boards });
|
||||
};
|
||||
|
||||
updateBoard = async ({
|
||||
boardId,
|
||||
dto,
|
||||
}: {
|
||||
boardId: number;
|
||||
dto: UpdateBoardDto;
|
||||
}): Promise<void> => {
|
||||
await boardApi.updateBoard({ dto, boardId });
|
||||
|
||||
this.invalidateBoards();
|
||||
};
|
||||
|
||||
addBoard = async (dto: CreateBoardDto): Promise<void> => {
|
||||
try {
|
||||
await boardApi.addBoard(dto);
|
||||
|
||||
await Promise.all([stageApiUtil.invalidateStages(), this.invalidateBoards()]);
|
||||
} catch (e) {
|
||||
throw new Error(`Error while adding board ${dto.name}: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
deleteBoard = async (boardId: number): Promise<void> => {
|
||||
try {
|
||||
await boardApi.deleteBoard(boardId);
|
||||
} catch (e) {
|
||||
console.log(`Failed to delete board: ${e}`);
|
||||
}
|
||||
|
||||
this.invalidateBoards();
|
||||
};
|
||||
|
||||
changeBoardSortOrder = async ({
|
||||
boardId,
|
||||
newSortOrder,
|
||||
}: {
|
||||
boardId: number;
|
||||
newSortOrder: number;
|
||||
}): Promise<void> => {
|
||||
const dto = new UpdateBoardDto({ sortOrder: newSortOrder });
|
||||
|
||||
try {
|
||||
await boardApi.updateBoard({ dto, boardId });
|
||||
} catch (error) {
|
||||
console.error(`Failed to change board sortOrder: ${error}`);
|
||||
} finally {
|
||||
this.invalidateBoards();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const boardApiUtil = new BoardApiUtil();
|
||||
29
frontend/src/app/api/DadataApi/DadataApi.ts
Normal file
29
frontend/src/app/api/DadataApi/DadataApi.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { DadataBankRequisitesSuggestion, DadataOrgRequisitesSuggestion } from '@/shared';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
|
||||
export class DadataApi {
|
||||
// proxy on https://dadata.ru/api/suggest/bank/
|
||||
getBankRequisites = async (query: string): Promise<DadataBankRequisitesSuggestion[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_BANK_REQUISITES, {
|
||||
params: {
|
||||
query,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// proxy on https://dadata.ru/api/suggest/party/
|
||||
getOrgRequisites = async (query: string): Promise<DadataOrgRequisitesSuggestion[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_ORG_REQUISITES, {
|
||||
params: {
|
||||
query,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
}
|
||||
|
||||
export const dadataApi = new DadataApi();
|
||||
67
frontend/src/app/api/EntityTypeApi/EntityTypeApi.ts
Normal file
67
frontend/src/app/api/EntityTypeApi/EntityTypeApi.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { type EtSectionBuilderModel } from '@/modules/builder';
|
||||
import { Field, FieldGroup, type ProjectFieldsSettings } from '@/modules/fields';
|
||||
import { EntityType, UrlTemplateUtil } from '@/shared';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import { type UpdateEntityTypeDto } from '../dtos/EntityType/UpdateEntityTypeDto';
|
||||
import { type UpdateEntityTypeFieldsModel } from '../dtos/EntityType/UpdateEntityTypeFieldsModel';
|
||||
|
||||
class EntityTypeApi {
|
||||
getEntityTypes = async (): Promise<EntityType[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_ENTITY_TYPES);
|
||||
|
||||
return EntityType.fromDtos(response.data);
|
||||
};
|
||||
|
||||
getEntityType = async (id: number): Promise<EntityType> => {
|
||||
const response = await baseApi.get(UrlTemplateUtil.toPath(ApiRoutes.GET_ENTITY_TYPE, { id }));
|
||||
|
||||
return EntityType.fromDto(response.data);
|
||||
};
|
||||
|
||||
createEntityType = async (data: EtSectionBuilderModel): Promise<EntityType> => {
|
||||
const response = await baseApi.post(ApiRoutes.ADD_ENTITY_TYPE, data);
|
||||
|
||||
return EntityType.fromDto(response.data);
|
||||
};
|
||||
|
||||
deleteEntityType = async (id: number): Promise<void> => {
|
||||
await baseApi.delete(UrlTemplateUtil.toPath(ApiRoutes.DELETE_ENTITY_TYPE, { id }));
|
||||
};
|
||||
|
||||
updateEntityType = async (dto: UpdateEntityTypeDto): Promise<EntityType> => {
|
||||
const response = await baseApi.put(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.UPDATE_ENTITY_TYPE, { id: dto.id }),
|
||||
dto
|
||||
);
|
||||
|
||||
return EntityType.fromDto(response.data);
|
||||
};
|
||||
|
||||
updateEntityTypeFields = async (model: UpdateEntityTypeFieldsModel): Promise<EntityType> => {
|
||||
const response = await baseApi.put(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.UPDATE_ENTITY_TYPE_FIELDS, { id: model.entityTypeId }),
|
||||
{
|
||||
fieldGroups: FieldGroup.toDtos(model.fieldGroups),
|
||||
fields: Field.toDtos(model.fields),
|
||||
}
|
||||
);
|
||||
|
||||
return EntityType.fromDto(response.data);
|
||||
};
|
||||
|
||||
updateFieldsSettings = async ({
|
||||
entityTypeId,
|
||||
fieldsSettings,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
fieldsSettings: ProjectFieldsSettings;
|
||||
}): Promise<void> => {
|
||||
await baseApi.put(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.UPDATE_FIELDS_SETTINGS, { entityTypeId }),
|
||||
fieldsSettings
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const entityTypeApi = new EntityTypeApi();
|
||||
13
frontend/src/app/api/FeatureApi/FeatureApi.ts
Normal file
13
frontend/src/app/api/FeatureApi/FeatureApi.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import { type FeatureDto } from '../dtos/Builder/FeatureDto';
|
||||
|
||||
class FeatureApi {
|
||||
getFeatures = async (): Promise<FeatureDto[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_FEATURES);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
}
|
||||
|
||||
export const featureApi = new FeatureApi();
|
||||
17
frontend/src/app/api/FeedbackApi/FeedbackApi.ts
Normal file
17
frontend/src/app/api/FeedbackApi/FeedbackApi.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { MailingApiRoutes } from '@/modules/mailing';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import { SendFeedbackDto, type FeedbackPayload, type FeedbackType } from '../dtos';
|
||||
|
||||
class FeedbackApi {
|
||||
sendFeedback = async ({
|
||||
type,
|
||||
payload,
|
||||
}: {
|
||||
type: FeedbackType;
|
||||
payload: FeedbackPayload;
|
||||
}): Promise<void> => {
|
||||
await baseApi.post(MailingApiRoutes.SEND_FEEDBACK, new SendFeedbackDto({ type, payload }));
|
||||
};
|
||||
}
|
||||
|
||||
export const feedbackApi = new FeedbackApi();
|
||||
66
frontend/src/app/api/FileApi/FileApi.ts
Normal file
66
frontend/src/app/api/FileApi/FileApi.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { FileInfo, type Nullable } from '@/shared';
|
||||
import { UrlTemplateUtil } from '@/shared/lib/utils/UrlTemplateUtil';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
|
||||
export interface FileUploadResult {
|
||||
key: string;
|
||||
id: string;
|
||||
downloadUrl: string;
|
||||
previewUrl: Nullable<string>;
|
||||
}
|
||||
|
||||
class FileApi {
|
||||
getFileInfo = async (fileId: string): Promise<FileInfo> => {
|
||||
const response = await baseApi.get(UrlTemplateUtil.toPath(ApiRoutes.GET_FILE_INFO, { fileId }));
|
||||
|
||||
return FileInfo.fromResultDto(response.data);
|
||||
};
|
||||
|
||||
uploadFiles = async (formData: FormData): Promise<FileUploadResult[]> => {
|
||||
const response = await baseApi.post(ApiRoutes.UPLOAD_FILES, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
downloadFile = async ({ url, fileName }: { url: string; fileName: string }): Promise<void> => {
|
||||
const response = await baseApi.get(url, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
fileDownload(response.data, fileName);
|
||||
};
|
||||
|
||||
getMediaBlobObjectUrl = async (url: string): Promise<Nullable<string>> => {
|
||||
const result = await baseApi.get(url, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
if (result) return URL.createObjectURL(result.data);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
deleteFile = async (fileId: string): Promise<void> => {
|
||||
await baseApi.delete(UrlTemplateUtil.toPath(ApiRoutes.DELETE_FILE, { id: fileId }));
|
||||
};
|
||||
|
||||
deleteFileLink = async (id: number): Promise<void> => {
|
||||
await baseApi.delete(UrlTemplateUtil.toPath(ApiRoutes.DELETE_FILE_LINK, { id }));
|
||||
};
|
||||
|
||||
deleteFileLinks = async (ids: number[]): Promise<void> => {
|
||||
await baseApi.delete(ApiRoutes.DELETE_FILE_LINKS, {
|
||||
params: {
|
||||
ids: ids.join(','),
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const fileApi = new FileApi();
|
||||
15
frontend/src/app/api/FileApi/queries/useGetFileInfos.ts
Normal file
15
frontend/src/app/api/FileApi/queries/useGetFileInfos.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQueries } from '@tanstack/react-query';
|
||||
import { APP_QUERY_KEYS } from '../../AppQueryKeys';
|
||||
import { fileApi } from '../FileApi';
|
||||
|
||||
export const useGetFileInfos = (fileIds: string[]) =>
|
||||
useQueries({
|
||||
queries: fileIds.map(fileId => ({
|
||||
queryKey: APP_QUERY_KEYS.fileInfoById(fileId),
|
||||
queryFn: () => fileApi.getFileInfo(fileId),
|
||||
})),
|
||||
combine: res => ({
|
||||
data: res.map(r => r.data),
|
||||
isLoading: res.some(result => result.isPending),
|
||||
}),
|
||||
});
|
||||
47
frontend/src/app/api/FormApi/FormApi.ts
Normal file
47
frontend/src/app/api/FormApi/FormApi.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { envUtil, SiteFormResult } from '@/shared';
|
||||
import axios from 'axios';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import type { SendContactUsFormDto, SiteFormDataDto, SiteFormResultDto } from '../dtos';
|
||||
|
||||
class FormApi {
|
||||
sendContactUsForm = async (dto: SendContactUsFormDto): Promise<void> => {
|
||||
await baseApi.post(ApiRoutes.SEND_CONTACT_US_FORM, dto);
|
||||
};
|
||||
|
||||
sendRequestSetupHeadlessForm = async (dto: SiteFormDataDto): Promise<SiteFormResultDto> => {
|
||||
const response = await axios.post<SiteFormResultDto>(envUtil.requestSetupHeadlessFormUrl, dto);
|
||||
|
||||
return SiteFormResult.fromDto(response.data);
|
||||
};
|
||||
|
||||
sendBpmnRequestForm = async (dto: SiteFormDataDto): Promise<SiteFormResultDto> => {
|
||||
const response = await axios.post<SiteFormResultDto>(
|
||||
envUtil.submitRequestBpmnWorkspaceFormUrl,
|
||||
dto
|
||||
);
|
||||
|
||||
return SiteFormResult.fromDto(response.data);
|
||||
};
|
||||
|
||||
sendAdditionalStorageRequestForm = async (dto: SiteFormDataDto): Promise<SiteFormResultDto> => {
|
||||
const response = await axios.post<SiteFormResultDto>(
|
||||
envUtil.submitRequestAdditionalStorageWorkspaceFormUrl,
|
||||
dto
|
||||
);
|
||||
|
||||
return SiteFormResult.fromDto(response.data);
|
||||
};
|
||||
|
||||
// This is is a new headless mywork form that is used to generate invoice
|
||||
sendMyworkInvoiceForm = async (dto: SiteFormDataDto): Promise<SiteFormResultDto> => {
|
||||
const response = await axios.post<SiteFormResultDto>(
|
||||
envUtil.submitRequestMyworkInvoiceWorkspaceFormUrl,
|
||||
dto
|
||||
);
|
||||
|
||||
return SiteFormResult.fromDto(response.data);
|
||||
};
|
||||
}
|
||||
|
||||
export const formApi = new FormApi();
|
||||
@@ -0,0 +1,31 @@
|
||||
import { FrontendObject, UrlTemplateUtil, type Nullable } from '@/shared';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import type { CreateFrontendObjectDto } from '../dtos';
|
||||
|
||||
class FrontendObjectsApi {
|
||||
// null – object not found by the key
|
||||
getFrontendObject = async <T extends unknown = unknown>(
|
||||
key: string
|
||||
): Promise<Nullable<FrontendObject<T>>> => {
|
||||
const response = await baseApi.get(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.GET_FRONTEND_OBJECT, { key })
|
||||
);
|
||||
|
||||
return response.data ? FrontendObject.fromDto<T>(response.data) : null;
|
||||
};
|
||||
|
||||
upsertFrontendObject = async <T extends unknown = unknown>(
|
||||
dto: CreateFrontendObjectDto<T>
|
||||
): Promise<FrontendObject<T>> => {
|
||||
const response = await baseApi.post(ApiRoutes.UPSERT_FRONTEND_OBJECT, dto);
|
||||
|
||||
return FrontendObject.fromDto<T>(response.data);
|
||||
};
|
||||
|
||||
deleteFrontendObject = async (key: string): Promise<void> => {
|
||||
await baseApi.delete(UrlTemplateUtil.toPath(ApiRoutes.DELETE_FRONTEND_OBJECT, { key }));
|
||||
};
|
||||
}
|
||||
|
||||
export const frontendObjectsApi = new FrontendObjectsApi();
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Account, AccountSettings } from '@/modules/settings';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import type { UpdateAccountSettingsDto } from '../dtos';
|
||||
|
||||
class GeneralSettingsApi {
|
||||
getAccount = async (): Promise<Account> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_ACCOUNT);
|
||||
|
||||
return Account.fromDto(response.data);
|
||||
};
|
||||
|
||||
searchAccounts = async (search: string): Promise<Account[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.SEARCH_ACCOUNTS, { params: { search } });
|
||||
|
||||
return Account.fromDtos(response.data);
|
||||
};
|
||||
|
||||
getAccountSettings = async (): Promise<AccountSettings> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_ACCOUNT_SETTINGS);
|
||||
|
||||
return AccountSettings.fromDto(response.data);
|
||||
};
|
||||
|
||||
updateAccountSettings = async (dto: UpdateAccountSettingsDto): Promise<AccountSettings> => {
|
||||
const response = await baseApi.put(ApiRoutes.UPDATE_ACCOUNT_SETTINGS, dto);
|
||||
|
||||
return AccountSettings.fromDto(response.data);
|
||||
};
|
||||
|
||||
uploadAccountLogo = async (formData: FormData): Promise<Account> => {
|
||||
const response = await baseApi.post(ApiRoutes.UPLOAD_ACCOUNT_LOGO, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
return Account.fromDto(response.data);
|
||||
};
|
||||
|
||||
removeAccountLogo = async (): Promise<Account> => {
|
||||
const response = await baseApi.delete(ApiRoutes.REMOVE_ACCOUNT_LOGO);
|
||||
|
||||
return Account.fromDto(response.data);
|
||||
};
|
||||
|
||||
getDemoDataExists = async (): Promise<boolean> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_DEMO_DATA_EXISTS);
|
||||
|
||||
return Boolean(response.data);
|
||||
};
|
||||
|
||||
deleteDemoData = async (): Promise<AccountSettings> => {
|
||||
const response = await baseApi.delete(ApiRoutes.DELETE_DEMO_DATA);
|
||||
|
||||
return AccountSettings.fromDto(response.data);
|
||||
};
|
||||
}
|
||||
|
||||
export const generalSettingsApi = new GeneralSettingsApi();
|
||||
32
frontend/src/app/api/IdentityApi/IdentityApi.ts
Normal file
32
frontend/src/app/api/IdentityApi/IdentityApi.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { UrlTemplateUtil } from '@/shared/lib/utils/UrlTemplateUtil';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
|
||||
export enum SequenceName {
|
||||
FIELD = 'field_id_seq',
|
||||
FIELD_GROUP = 'field_group_id_seq',
|
||||
FIELD_OPTION = 'field_option_id_seq',
|
||||
}
|
||||
|
||||
export interface IdentityPool {
|
||||
name: string;
|
||||
values: number[];
|
||||
}
|
||||
|
||||
class IdentityApi {
|
||||
getAllIdPools = async (): Promise<IdentityPool[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_ALL_IDENTITY_POOLS);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getIdPoolValues = async (name: SequenceName): Promise<number[]> => {
|
||||
const response = await baseApi.get(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.GET_IDENTITY_POOL, { name })
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
}
|
||||
|
||||
export const identityApi = new IdentityApi();
|
||||
76
frontend/src/app/api/StageApi/StageApi.ts
Normal file
76
frontend/src/app/api/StageApi/StageApi.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Stage, UrlTemplateUtil } from '@/shared';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import { CreateStageDto, UpdateStageDto } from '../dtos';
|
||||
|
||||
class StageApi {
|
||||
createStage = async ({
|
||||
boardId,
|
||||
dto,
|
||||
}: {
|
||||
boardId: number;
|
||||
dto: CreateStageDto;
|
||||
}): Promise<Stage> => {
|
||||
const response = await baseApi.post(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.CREATE_STAGE, { boardId }),
|
||||
dto
|
||||
);
|
||||
|
||||
return Stage.fromDto(response.data);
|
||||
};
|
||||
|
||||
getBoardStages = async (boardId: number): Promise<Stage[]> => {
|
||||
const response = await baseApi.get(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.GET_STAGES, { boardId: boardId })
|
||||
);
|
||||
|
||||
return Stage.fromDtos(response.data);
|
||||
};
|
||||
|
||||
getStageById = async ({
|
||||
stageId,
|
||||
boardId,
|
||||
}: {
|
||||
stageId: number;
|
||||
boardId: number;
|
||||
}): Promise<Stage> => {
|
||||
const response = await baseApi.get(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.GET_STAGE, { boardId, stageId })
|
||||
);
|
||||
|
||||
return Stage.fromDto(response.data);
|
||||
};
|
||||
|
||||
updateStage = async ({
|
||||
boardId,
|
||||
stageId,
|
||||
dto,
|
||||
}: {
|
||||
boardId: number;
|
||||
stageId: number;
|
||||
dto: UpdateStageDto;
|
||||
}): Promise<Stage> => {
|
||||
const response = await baseApi.patch(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.UPDATE_STAGE, { boardId, stageId }),
|
||||
dto
|
||||
);
|
||||
|
||||
return Stage.fromDto(response.data);
|
||||
};
|
||||
|
||||
deleteStage = async ({
|
||||
boardId,
|
||||
stageId,
|
||||
newStageId,
|
||||
}: {
|
||||
boardId: number;
|
||||
stageId: number;
|
||||
newStageId: number;
|
||||
}): Promise<void> => {
|
||||
await baseApi.delete(UrlTemplateUtil.toPath(ApiRoutes.DELETE_STAGE, { boardId, stageId }), {
|
||||
params: { newStageId },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const stageApi = new StageApi();
|
||||
149
frontend/src/app/api/StageApi/StageApiUtil.ts
Normal file
149
frontend/src/app/api/StageApi/StageApiUtil.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { CreateStageDto, UpdateStageDto } from '@/app';
|
||||
import { queryClient } from '@/index';
|
||||
import type { Nullable, Optional, Stage } from '@/shared';
|
||||
import {
|
||||
queryOptions,
|
||||
skipToken,
|
||||
useQueries,
|
||||
useQuery,
|
||||
type UseQueryResult,
|
||||
} from '@tanstack/react-query';
|
||||
import { APP_QUERY_KEYS } from '../AppQueryKeys';
|
||||
import { stageApi } from './StageApi';
|
||||
|
||||
const STAGES_STALE_TIME = 10 * 60 * 1000;
|
||||
|
||||
// this class encapsulate all stages-related queries and helpers
|
||||
// this is in fact static class, but do not refactor methods to static,
|
||||
// because this will violate the rules of hooks
|
||||
class StageApiUtil {
|
||||
private _getStageQueryOptions = ({
|
||||
stageId,
|
||||
boardId,
|
||||
enabled,
|
||||
}: {
|
||||
stageId: Optional<Nullable<number>>;
|
||||
boardId: Optional<Nullable<number>>;
|
||||
enabled?: boolean;
|
||||
}) => {
|
||||
return queryOptions({
|
||||
queryKey: APP_QUERY_KEYS.stage({ stageId, boardId }),
|
||||
queryFn:
|
||||
stageId && boardId
|
||||
? async (): Promise<Stage> => await stageApi.getStageById({ stageId, boardId })
|
||||
: skipToken,
|
||||
staleTime: STAGES_STALE_TIME,
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
private _getStagesByBoardIdQueryOptions = ({
|
||||
boardId,
|
||||
enabled,
|
||||
}: {
|
||||
boardId: Optional<Nullable<number>>;
|
||||
enabled?: boolean;
|
||||
}) => {
|
||||
return queryOptions({
|
||||
queryKey: APP_QUERY_KEYS.stagesByBoardId(boardId),
|
||||
queryFn: boardId
|
||||
? async (): Promise<Stage[]> => await stageApi.getBoardStages(boardId)
|
||||
: skipToken,
|
||||
staleTime: STAGES_STALE_TIME,
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
useGetStage = ({
|
||||
stageId,
|
||||
boardId,
|
||||
enabled,
|
||||
}: {
|
||||
stageId: Optional<Nullable<number>>;
|
||||
boardId: Optional<Nullable<number>>;
|
||||
enabled?: boolean;
|
||||
}): UseQueryResult<Stage, Error> => {
|
||||
return useQuery(this._getStageQueryOptions({ stageId, boardId, enabled }));
|
||||
};
|
||||
|
||||
useGetStagesByBoardId = ({
|
||||
boardId,
|
||||
enabled,
|
||||
}: {
|
||||
boardId: Optional<Nullable<number>>;
|
||||
enabled?: boolean;
|
||||
}): UseQueryResult<Stage[], Error> => {
|
||||
return useQuery(this._getStagesByBoardIdQueryOptions({ boardId, enabled }));
|
||||
};
|
||||
|
||||
useGetStagesByBoardIds = (boardIds: number[]): Stage[] => {
|
||||
return useQueries({
|
||||
queries: boardIds.map(boardId => this._getStagesByBoardIdQueryOptions({ boardId })),
|
||||
combine: res => {
|
||||
if (res.some(r => r.isLoading)) return [];
|
||||
|
||||
const data: Stage[] = [];
|
||||
|
||||
res.forEach(r => {
|
||||
if (r.data) data.push(...r.data);
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
getStagesByBoardId = async (boardId: Optional<Nullable<number>>): Promise<Stage[]> => {
|
||||
return await queryClient.ensureQueryData(this._getStagesByBoardIdQueryOptions({ boardId }));
|
||||
};
|
||||
|
||||
invalidateStages = async (): Promise<void> => {
|
||||
await queryClient.refetchQueries({ queryKey: APP_QUERY_KEYS.stages });
|
||||
};
|
||||
|
||||
createStage = async ({
|
||||
boardId,
|
||||
dto,
|
||||
}: {
|
||||
boardId: number;
|
||||
dto: CreateStageDto;
|
||||
}): Promise<Stage> => {
|
||||
const stage = await stageApi.createStage({ boardId, dto });
|
||||
|
||||
this.invalidateStages();
|
||||
|
||||
return stage;
|
||||
};
|
||||
|
||||
updateStage = async ({
|
||||
boardId,
|
||||
stageId,
|
||||
dto,
|
||||
}: {
|
||||
boardId: number;
|
||||
stageId: number;
|
||||
dto: UpdateStageDto;
|
||||
}): Promise<Stage> => {
|
||||
const stage = await stageApi.updateStage({ boardId, stageId, dto });
|
||||
|
||||
this.invalidateStages();
|
||||
|
||||
return stage;
|
||||
};
|
||||
|
||||
deleteStage = async ({
|
||||
boardId,
|
||||
stageId,
|
||||
newStageId,
|
||||
}: {
|
||||
boardId: number;
|
||||
stageId: number;
|
||||
newStageId: number;
|
||||
}): Promise<void> => {
|
||||
await stageApi.deleteStage({ boardId, stageId, newStageId });
|
||||
|
||||
this.invalidateStages();
|
||||
};
|
||||
}
|
||||
|
||||
export const stageApiUtil = new StageApiUtil();
|
||||
78
frontend/src/app/api/SubscriptionApi/SubscriptionApi.ts
Normal file
78
frontend/src/app/api/SubscriptionApi/SubscriptionApi.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { UpdateSubscriptionDto } from '@/app';
|
||||
import { CurrentDiscount } from '@/modules/settings';
|
||||
import { Nullable, UrlTemplateUtil } from '@/shared';
|
||||
import { Subscription } from '../../../shared/lib/models/Subscription/Subscription';
|
||||
import { SubscriptionPlan } from '../../../shared/lib/models/Subscription/SubscriptionPlan';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
|
||||
class SubscriptionApi {
|
||||
getSubscription = async (): Promise<Subscription> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_SUBSCRIPTION);
|
||||
|
||||
return Subscription.fromDto(response.data);
|
||||
};
|
||||
|
||||
getSubscriptionForAccount = async (id: number): Promise<Subscription> => {
|
||||
const response = await baseApi.get(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.GET_SUBSCRIPTION_FOR_ACCOUNT, { accountId: id })
|
||||
);
|
||||
|
||||
return Subscription.fromDto(response.data);
|
||||
};
|
||||
|
||||
getSubscriptionPlans = async (): Promise<SubscriptionPlan[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_SUBSCRIPTION_PLANS);
|
||||
|
||||
return SubscriptionPlan.fromDtos(response.data);
|
||||
};
|
||||
|
||||
getCheckoutUrl = async ({
|
||||
amount,
|
||||
priceId,
|
||||
productId,
|
||||
couponId,
|
||||
numberOfUsers,
|
||||
}: {
|
||||
numberOfUsers: number;
|
||||
amount?: number;
|
||||
priceId?: string;
|
||||
couponId?: string;
|
||||
productId?: string;
|
||||
}): Promise<string> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_SUBSCRIPTION_CHECKOUT_URL, {
|
||||
params: { productId, amount, priceId, couponId, numberOfUsers },
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getPortalUrl = async (): Promise<string> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_SUBSCRIPTION_PORTAL_URL);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getCurrentDiscount = async (): Promise<Nullable<CurrentDiscount>> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_CURRENT_DISCOUNT);
|
||||
|
||||
return response.data ? CurrentDiscount.fromDto(response.data) : null;
|
||||
};
|
||||
|
||||
updateSubscription = async ({
|
||||
accountId,
|
||||
dto,
|
||||
}: {
|
||||
accountId: number;
|
||||
dto: UpdateSubscriptionDto;
|
||||
}): Promise<Subscription> => {
|
||||
const response = await baseApi.patch(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.UPDATE_SUBSCRIPTION, { accountId }),
|
||||
dto
|
||||
);
|
||||
|
||||
return Subscription.fromDto(response.data);
|
||||
};
|
||||
}
|
||||
|
||||
export const subscriptionApi = new SubscriptionApi();
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Optional } from '@/shared';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { APP_QUERY_KEYS } from '../../AppQueryKeys';
|
||||
import { subscriptionApi } from '../SubscriptionApi';
|
||||
|
||||
export const useGetSubscriptionForAccount = (accountId: Optional<number>) =>
|
||||
useQuery({
|
||||
queryKey: APP_QUERY_KEYS.subscriptionByAccountId(accountId ?? -1),
|
||||
queryFn: () => subscriptionApi.getSubscriptionForAccount(accountId ?? -1),
|
||||
enabled: Boolean(accountId),
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Subscription } from '@/shared';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { APP_QUERY_KEYS } from '../../AppQueryKeys';
|
||||
import { UpdateSubscriptionDto } from '../../dtos';
|
||||
import { subscriptionApi } from '../SubscriptionApi';
|
||||
|
||||
export const useUpdateSubscription = (accountId: number) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: APP_QUERY_KEYS.subscriptionByAccountId(accountId),
|
||||
mutationFn: (dto: UpdateSubscriptionDto) =>
|
||||
subscriptionApi.updateSubscription({ accountId, dto: dto }),
|
||||
onMutate: async (): Promise<void> => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: APP_QUERY_KEYS.subscriptionByAccountId(accountId),
|
||||
});
|
||||
},
|
||||
onSuccess: async (subscription: Subscription): Promise<void> => {
|
||||
queryClient.setQueryData(APP_QUERY_KEYS.subscriptionByAccountId(accountId), subscription);
|
||||
},
|
||||
});
|
||||
};
|
||||
78
frontend/src/app/api/UserApi/UserApi.ts
Normal file
78
frontend/src/app/api/UserApi/UserApi.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { UrlTemplateUtil, User } from '@/shared';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import type { ChangeUserPasswordDto, CreateUserDto, UpdateUserDto, UserDto } from '../dtos';
|
||||
|
||||
class UserApi {
|
||||
getUserById = async (userId: number): Promise<User> => {
|
||||
const response = await baseApi.get(UrlTemplateUtil.toPath(ApiRoutes.GET_USER, { id: userId }));
|
||||
|
||||
return User.fromDto(response.data);
|
||||
};
|
||||
|
||||
getUsers = async (): Promise<User[]> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_USERS);
|
||||
|
||||
return User.fromDtos(response.data);
|
||||
};
|
||||
|
||||
addUser = async (dto: CreateUserDto): Promise<User> => {
|
||||
const response = await baseApi.post(ApiRoutes.ADD_USER, dto);
|
||||
|
||||
return User.fromDto(response.data);
|
||||
};
|
||||
|
||||
updateUser = async ({ id, dto }: { id: number; dto: UpdateUserDto }): Promise<User> => {
|
||||
const response = await baseApi.put(UrlTemplateUtil.toPath(ApiRoutes.UPDATE_USER, { id }), dto);
|
||||
|
||||
return User.fromDto(response.data);
|
||||
};
|
||||
|
||||
deleteUser = async ({
|
||||
userId,
|
||||
newUserId,
|
||||
}: {
|
||||
userId: number;
|
||||
newUserId?: number;
|
||||
}): Promise<void> => {
|
||||
await baseApi.delete(UrlTemplateUtil.toPath(ApiRoutes.DELETE_USER, { id: userId }), {
|
||||
params: { newUserId },
|
||||
});
|
||||
};
|
||||
|
||||
uploadUserAvatar = async ({
|
||||
userId,
|
||||
formData,
|
||||
}: {
|
||||
userId: number;
|
||||
formData: FormData;
|
||||
}): Promise<User> => {
|
||||
const response = await baseApi.post(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.UPLOAD_USER_AVATAR, { id: userId }),
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return User.fromDto(response.data as UserDto);
|
||||
};
|
||||
|
||||
removeUserAvatar = async (userId: number): Promise<User> => {
|
||||
const response = await baseApi.delete(
|
||||
UrlTemplateUtil.toPath(ApiRoutes.REMOVE_USER_AVATAR, { id: userId })
|
||||
);
|
||||
|
||||
return User.fromDto(response.data);
|
||||
};
|
||||
|
||||
changeUserPassword = async (dto: ChangeUserPasswordDto): Promise<boolean> => {
|
||||
const response = await baseApi.post(ApiRoutes.CHANGE_USER_PASSWORD, dto);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
}
|
||||
|
||||
export const userApi = new UserApi();
|
||||
28
frontend/src/app/api/UserProfileApi/UserProfileApi.ts
Normal file
28
frontend/src/app/api/UserProfileApi/UserProfileApi.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { UrlTemplateUtil } from '@/shared/lib/utils/UrlTemplateUtil';
|
||||
import { UserProfile } from '../../../shared/lib/models/Profile/UserProfile';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
import { type UpdateUserProfileDto } from '../dtos/Profile/UpdateUserProfileDto';
|
||||
import { type UserProfileDto } from '../dtos/Profile/UserProfileDto';
|
||||
|
||||
class UserProfileApi {
|
||||
getUserProfile = async (id: number): Promise<UserProfile> => {
|
||||
const response = await baseApi.get(UrlTemplateUtil.toPath(ApiRoutes.GET_USER_PROFILE, { id }));
|
||||
|
||||
const dto: UserProfileDto = response.data;
|
||||
|
||||
return new UserProfile(dto);
|
||||
};
|
||||
|
||||
updateUserProfile = async ({
|
||||
id,
|
||||
dto,
|
||||
}: {
|
||||
id: number;
|
||||
dto: UpdateUserProfileDto;
|
||||
}): Promise<void> => {
|
||||
await baseApi.patch(UrlTemplateUtil.toPath(ApiRoutes.UPDATE_USER_PROFILE, { id }), dto);
|
||||
};
|
||||
}
|
||||
|
||||
export const userProfileApi = new UserProfileApi();
|
||||
@@ -0,0 +1,5 @@
|
||||
import { queryClient } from '@/index';
|
||||
import { APP_QUERY_KEYS } from '../../AppQueryKeys';
|
||||
|
||||
export const invalidateUserProfilesInCache = () =>
|
||||
queryClient.invalidateQueries({ queryKey: APP_QUERY_KEYS.userProfiles });
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useQueries } from '@tanstack/react-query';
|
||||
import { APP_QUERY_KEYS } from '../../AppQueryKeys';
|
||||
import { userProfileApi } from '../UserProfileApi';
|
||||
|
||||
export const useGetUserProfiles = ({ userIds }: { userIds: number[] }) => {
|
||||
return useQueries({
|
||||
queries: userIds.map(userId => ({
|
||||
queryKey: APP_QUERY_KEYS.userProfile(userId),
|
||||
queryFn: () => userProfileApi.getUserProfile(userId),
|
||||
})),
|
||||
combine: res => ({
|
||||
data: res.map((result, idx) => ({
|
||||
userId: userIds[idx]!,
|
||||
settings: result.data,
|
||||
})),
|
||||
isLoading: res.some(result => result.isPending),
|
||||
}),
|
||||
});
|
||||
};
|
||||
17
frontend/src/app/api/UtilityApi/UtilityApi.ts
Normal file
17
frontend/src/app/api/UtilityApi/UtilityApi.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
|
||||
const IP_REGISTRY = 'https://api.ipregistry.co';
|
||||
|
||||
class UtilityApi {
|
||||
getCountryCode = async (): Promise<string> => {
|
||||
const response = await baseApi.get(IP_REGISTRY, {
|
||||
params: {
|
||||
key: import.meta.env.VITE_IP_REGISTRY_API_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data.location.country.code.toLowerCase();
|
||||
};
|
||||
}
|
||||
|
||||
export const utilityApi = new UtilityApi();
|
||||
17
frontend/src/app/api/VersionApi/VersionApi.ts
Normal file
17
frontend/src/app/api/VersionApi/VersionApi.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type Nullable, Version } from '@/shared';
|
||||
import { ApiRoutes } from '../ApiRoutes';
|
||||
import { baseApi } from '../BaseApi/BaseApi';
|
||||
|
||||
class VersionApi {
|
||||
getLatestFrontendVersion = async (currentVersion: string): Promise<Nullable<Version>> => {
|
||||
const response = await baseApi.get(ApiRoutes.GET_LATEST_FRONTEND_VERSION, {
|
||||
params: {
|
||||
currentVersion,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data ? Version.fromDto(response.data) : null;
|
||||
};
|
||||
}
|
||||
|
||||
export const versionApi = new VersionApi();
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { APP_QUERY_KEYS } from '../../AppQueryKeys';
|
||||
import { versionApi } from '../VersionApi';
|
||||
|
||||
export const useGetLatestFrontendVersionPolling = (currentVersion: string) =>
|
||||
useQuery({
|
||||
queryKey: APP_QUERY_KEYS.latestFrontendVersion(currentVersion),
|
||||
queryFn: () => versionApi.getLatestFrontendVersion(currentVersion),
|
||||
// refetch every 10 minutes
|
||||
refetchInterval: 10 * 60 * 1000,
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
16
frontend/src/app/api/dtos/Board/BoardDto.ts
Normal file
16
frontend/src/app/api/dtos/Board/BoardDto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { type Nullable } from '@/shared';
|
||||
import { type BoardType } from '../../../../shared/lib/models/Board/BoardType';
|
||||
import { type UserRights } from '../../../../shared/lib/models/Permission/UserRights';
|
||||
|
||||
export interface BoardDto {
|
||||
id: number;
|
||||
name: string;
|
||||
type: BoardType;
|
||||
recordId: Nullable<number>;
|
||||
isSystem: boolean;
|
||||
ownerId: Nullable<number>;
|
||||
participantIds: Nullable<number[]>;
|
||||
sortOrder: number;
|
||||
taskBoardId: Nullable<number>;
|
||||
userRights: UserRights;
|
||||
}
|
||||
38
frontend/src/app/api/dtos/Board/CreateBoardDto.ts
Normal file
38
frontend/src/app/api/dtos/Board/CreateBoardDto.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
import { BoardType } from '../../../../shared';
|
||||
|
||||
export class CreateBoardDto {
|
||||
name: string;
|
||||
// if not provided, will be calculated on backend
|
||||
sortOrder?: number;
|
||||
type: BoardType;
|
||||
recordId: Nullable<number>;
|
||||
|
||||
constructor({ name, sortOrder, type, recordId }: CreateBoardDto) {
|
||||
this.name = name;
|
||||
this.sortOrder = sortOrder;
|
||||
this.type = type;
|
||||
this.recordId = recordId;
|
||||
}
|
||||
|
||||
static forEntityType({
|
||||
name,
|
||||
sortOrder,
|
||||
entityTypeId,
|
||||
}: {
|
||||
name: string;
|
||||
sortOrder?: number;
|
||||
entityTypeId: number;
|
||||
}): CreateBoardDto {
|
||||
return new CreateBoardDto({
|
||||
name,
|
||||
sortOrder,
|
||||
type: BoardType.ENTITY_TYPE,
|
||||
recordId: entityTypeId,
|
||||
});
|
||||
}
|
||||
|
||||
static forTasks({ name, sortOrder }: { name: string; sortOrder: number }): CreateBoardDto {
|
||||
return new CreateBoardDto({ name, sortOrder, type: BoardType.TASK, recordId: null });
|
||||
}
|
||||
}
|
||||
9
frontend/src/app/api/dtos/Board/UpdateBoardDto.ts
Normal file
9
frontend/src/app/api/dtos/Board/UpdateBoardDto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class UpdateBoardDto {
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
participantIds: number[];
|
||||
|
||||
constructor(data: Partial<UpdateBoardDto>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
15
frontend/src/app/api/dtos/Builder/FeatureDto.ts
Normal file
15
frontend/src/app/api/dtos/Builder/FeatureDto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type FeatureCode } from '../../../../shared/lib/models/Feature/FeatureCode';
|
||||
|
||||
export class FeatureDto {
|
||||
id: number;
|
||||
name: string;
|
||||
code: FeatureCode;
|
||||
isEnabled: boolean;
|
||||
|
||||
constructor({ id, name, code, isEnabled }: FeatureDto) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.code = code;
|
||||
this.isEnabled = isEnabled;
|
||||
}
|
||||
}
|
||||
13
frontend/src/app/api/dtos/CreateContactAndLeadDto.ts
Normal file
13
frontend/src/app/api/dtos/CreateContactAndLeadDto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
|
||||
export class CreateContactAndLeadDto {
|
||||
contactTypeId: number;
|
||||
leadTypeId: Nullable<number>;
|
||||
leadBoardId: Nullable<number>;
|
||||
|
||||
constructor({ contactTypeId, leadTypeId, leadBoardId }: CreateContactAndLeadDto) {
|
||||
this.contactTypeId = contactTypeId;
|
||||
this.leadTypeId = leadTypeId;
|
||||
this.leadBoardId = leadBoardId;
|
||||
}
|
||||
}
|
||||
25
frontend/src/app/api/dtos/Entity/EntityDto.ts
Normal file
25
frontend/src/app/api/dtos/Entity/EntityDto.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { EntityLinkDto } from '@/app';
|
||||
import type { FieldValueDto } from '@/modules/fields';
|
||||
import type { ChatDto } from '@/modules/multichat';
|
||||
import type { ExternalEntity, Nullable, UserRights } from '@/shared';
|
||||
|
||||
export interface EntityDto {
|
||||
id: number;
|
||||
name: string;
|
||||
createdBy: number;
|
||||
createdAt: string;
|
||||
entityTypeId: number;
|
||||
userRights: UserRights;
|
||||
boardId: Nullable<number>;
|
||||
responsibleUserId: number;
|
||||
stageId: Nullable<number>;
|
||||
entityLinks: EntityLinkDto[];
|
||||
copiedFrom: Nullable<number>;
|
||||
copiedCount: Nullable<number>;
|
||||
externalEntities: ExternalEntity[];
|
||||
fieldValues: FieldValueDto<unknown>[];
|
||||
lastShipmentDate?: string;
|
||||
closedAt?: string;
|
||||
chats?: ChatDto[];
|
||||
focused?: boolean;
|
||||
}
|
||||
8
frontend/src/app/api/dtos/EntityEvent/ContactInfoDto.ts
Normal file
8
frontend/src/app/api/dtos/EntityEvent/ContactInfoDto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { type Nullable } from '@/shared';
|
||||
|
||||
export interface ContactInfoDto {
|
||||
id: number;
|
||||
name: string;
|
||||
phone: Nullable<string[]>;
|
||||
email: Nullable<string>;
|
||||
}
|
||||
20
frontend/src/app/api/dtos/EntityEvent/EntityEventItemDto.ts
Normal file
20
frontend/src/app/api/dtos/EntityEvent/EntityEventItemDto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type CallDirection, type CallStatus } from '@/modules/telephony';
|
||||
import { type Nullable } from '@/shared';
|
||||
import { type EntityInfoDto } from './EntityInfoDto';
|
||||
|
||||
export interface EntityEventItemDto {
|
||||
id: number;
|
||||
sessionId: string;
|
||||
callId: string;
|
||||
userId: number;
|
||||
entityId: number;
|
||||
direction: CallDirection;
|
||||
phoneNumber: string;
|
||||
duration: Nullable<number>;
|
||||
status: CallStatus;
|
||||
failureReason: Nullable<string>;
|
||||
recordUrl: Nullable<string>;
|
||||
createdAt: string;
|
||||
entityInfo: EntityInfoDto;
|
||||
comment?: string;
|
||||
}
|
||||
10
frontend/src/app/api/dtos/EntityEvent/EntityInfoDto.ts
Normal file
10
frontend/src/app/api/dtos/EntityEvent/EntityInfoDto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { type Nullable } from '@/shared';
|
||||
import { type ContactInfoDto } from './ContactInfoDto';
|
||||
|
||||
export interface EntityInfoDto {
|
||||
id: number;
|
||||
name: string;
|
||||
entityTypeId: number;
|
||||
hasAccess: boolean;
|
||||
contact: Nullable<ContactInfoDto>;
|
||||
}
|
||||
15
frontend/src/app/api/dtos/EntityLink/EntityLinkDto.ts
Normal file
15
frontend/src/app/api/dtos/EntityLink/EntityLinkDto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type ObjectState } from '@/shared';
|
||||
|
||||
export class EntityLinkDto {
|
||||
sourceId: number;
|
||||
targetId: number;
|
||||
sortOrder: number;
|
||||
state: ObjectState;
|
||||
|
||||
constructor({ sourceId, targetId, sortOrder, state }: EntityLinkDto) {
|
||||
this.sourceId = sourceId;
|
||||
this.targetId = targetId;
|
||||
this.sortOrder = sortOrder;
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
17
frontend/src/app/api/dtos/EntityType/EntityTypeDto.ts
Normal file
17
frontend/src/app/api/dtos/EntityType/EntityTypeDto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { FieldDto, FieldGroupDto } from '@/modules/fields';
|
||||
import type { EntityCategory, EntityTypeLink, FeatureCode, Section } from '@/shared';
|
||||
|
||||
export interface EntityTypeDto {
|
||||
id: number;
|
||||
name: string;
|
||||
section: Section;
|
||||
createdAt: string;
|
||||
sortOrder: number;
|
||||
fields: FieldDto[];
|
||||
featureCodes: FeatureCode[];
|
||||
fieldGroups: FieldGroupDto[];
|
||||
linkedSchedulerIds: number[];
|
||||
entityCategory: EntityCategory;
|
||||
linkedProductsSectionIds: number[];
|
||||
linkedEntityTypes: EntityTypeLink[];
|
||||
}
|
||||
49
frontend/src/app/api/dtos/EntityType/UpdateEntityTypeDto.ts
Normal file
49
frontend/src/app/api/dtos/EntityType/UpdateEntityTypeDto.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { type FieldDto, type FieldGroupDto, type ProjectFieldsSettings } from '@/modules/fields';
|
||||
import { type TaskFieldCode } from '@/modules/tasks';
|
||||
import { type EntityCategory, type EntityTypeLink, type FeatureCode, type Section } from '@/shared';
|
||||
|
||||
export class UpdateEntityTypeDto {
|
||||
id: number;
|
||||
name: string;
|
||||
entityCategory: EntityCategory;
|
||||
section: Section;
|
||||
fieldGroups: FieldGroupDto[];
|
||||
fields: FieldDto[];
|
||||
linkedEntityTypes: EntityTypeLink[];
|
||||
featureCodes: FeatureCode[];
|
||||
taskSettingsActiveFields?: TaskFieldCode[];
|
||||
linkedProductsSectionIds?: number[];
|
||||
linkedSchedulerIds?: number[];
|
||||
fieldsSettings?: ProjectFieldsSettings;
|
||||
sortOrder?: number;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
name,
|
||||
entityCategory,
|
||||
section,
|
||||
fieldGroups,
|
||||
fields,
|
||||
linkedEntityTypes,
|
||||
featureCodes,
|
||||
taskSettingsActiveFields,
|
||||
linkedProductsSectionIds,
|
||||
linkedSchedulerIds,
|
||||
fieldsSettings,
|
||||
sortOrder,
|
||||
}: UpdateEntityTypeDto) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.entityCategory = entityCategory;
|
||||
this.section = section;
|
||||
this.fieldGroups = fieldGroups;
|
||||
this.fields = fields;
|
||||
this.linkedEntityTypes = linkedEntityTypes;
|
||||
this.featureCodes = featureCodes;
|
||||
this.taskSettingsActiveFields = taskSettingsActiveFields;
|
||||
this.linkedProductsSectionIds = linkedProductsSectionIds;
|
||||
this.linkedSchedulerIds = linkedSchedulerIds;
|
||||
this.fieldsSettings = fieldsSettings;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type Field } from '../../../../modules/fields/shared/lib/models/Field/Field';
|
||||
import { type FieldGroup } from '../../../../modules/fields/shared/lib/models/FieldGroup/FieldGroup';
|
||||
|
||||
export class UpdateEntityTypeFieldsModel {
|
||||
entityTypeId: number;
|
||||
fieldGroups: FieldGroup[];
|
||||
fields: Field[];
|
||||
|
||||
constructor({ entityTypeId, fieldGroups, fields }: UpdateEntityTypeFieldsModel) {
|
||||
this.entityTypeId = entityTypeId;
|
||||
this.fieldGroups = fieldGroups;
|
||||
this.fields = fields;
|
||||
}
|
||||
}
|
||||
8
frontend/src/app/api/dtos/FeedItem/FeedItemDto.ts
Normal file
8
frontend/src/app/api/dtos/FeedItem/FeedItemDto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { type FeedItemType } from '../../../../modules/card/shared/lib/models/FeedItemType';
|
||||
|
||||
export interface FeedItemDto {
|
||||
id: number;
|
||||
type: FeedItemType;
|
||||
data: object;
|
||||
createdAt: string;
|
||||
}
|
||||
4
frontend/src/app/api/dtos/Feedback/FeedbackType.ts
Normal file
4
frontend/src/app/api/dtos/Feedback/FeedbackType.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum FeedbackType {
|
||||
TRIAL_EXPIRED = 'trial_expired',
|
||||
USER_LIMIT = 'user_limit',
|
||||
}
|
||||
15
frontend/src/app/api/dtos/Feedback/SendFeedbackDto.ts
Normal file
15
frontend/src/app/api/dtos/Feedback/SendFeedbackDto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type FeedbackType } from './FeedbackType';
|
||||
import { type TrialExpiredFeedback } from './TrialExpiredFeedback';
|
||||
import { type UserLimitFeedback } from './UserLimitFeedback';
|
||||
|
||||
export type FeedbackPayload = TrialExpiredFeedback | UserLimitFeedback;
|
||||
|
||||
export class SendFeedbackDto {
|
||||
type: FeedbackType;
|
||||
payload: FeedbackPayload;
|
||||
|
||||
constructor({ type, payload }: SendFeedbackDto) {
|
||||
this.type = type;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
17
frontend/src/app/api/dtos/Feedback/TrialExpiredFeedback.ts
Normal file
17
frontend/src/app/api/dtos/Feedback/TrialExpiredFeedback.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export class TrialExpiredFeedback {
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
userNumber: string;
|
||||
subscribe: string;
|
||||
plan: string;
|
||||
|
||||
constructor({ name, phone, email, userNumber, subscribe, plan }: TrialExpiredFeedback) {
|
||||
this.name = name;
|
||||
this.phone = phone;
|
||||
this.email = email;
|
||||
this.userNumber = userNumber;
|
||||
this.subscribe = subscribe;
|
||||
this.plan = plan;
|
||||
}
|
||||
}
|
||||
13
frontend/src/app/api/dtos/Feedback/UserLimitFeedback.ts
Normal file
13
frontend/src/app/api/dtos/Feedback/UserLimitFeedback.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export class UserLimitFeedback {
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
userNumber: string;
|
||||
|
||||
constructor({ name, phone, email, userNumber }: UserLimitFeedback) {
|
||||
this.name = name;
|
||||
this.phone = phone;
|
||||
this.email = email;
|
||||
this.userNumber = userNumber;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
|
||||
export interface FileInfoResultDto {
|
||||
id: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
mimeType: string;
|
||||
downloadUrl: Nullable<string>;
|
||||
previewUrl: Nullable<string>;
|
||||
createdAt: string;
|
||||
}
|
||||
13
frontend/src/app/api/dtos/FileLink/FileLinkDto.ts
Normal file
13
frontend/src/app/api/dtos/FileLink/FileLinkDto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { type Nullable } from '@/shared';
|
||||
|
||||
export interface FileLinkDto {
|
||||
id: number;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
fileType: string;
|
||||
downloadUrl: string;
|
||||
previewUrl: Nullable<string>;
|
||||
createdAt: string;
|
||||
createdBy: number;
|
||||
}
|
||||
17
frontend/src/app/api/dtos/Form/SendContactUsFormDto.ts
Normal file
17
frontend/src/app/api/dtos/Form/SendContactUsFormDto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
|
||||
export class SendContactUsFormDto {
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
comment: Nullable<string>;
|
||||
ref: Nullable<string>;
|
||||
|
||||
constructor({ name, phone, email, comment, ref }: SendContactUsFormDto) {
|
||||
this.name = name;
|
||||
this.phone = phone;
|
||||
this.email = email;
|
||||
this.comment = comment;
|
||||
this.ref = ref;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class CreateFrontendObjectDto<T extends unknown = unknown> {
|
||||
key: string;
|
||||
value: T;
|
||||
|
||||
constructor({ key, value }: CreateFrontendObjectDto<T>) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface FrontendObjectDto<T extends unknown = unknown> {
|
||||
key: string;
|
||||
value: T;
|
||||
createdAt: string;
|
||||
}
|
||||
9
frontend/src/app/api/dtos/GeneralSettings/AccountDto.ts
Normal file
9
frontend/src/app/api/dtos/GeneralSettings/AccountDto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
|
||||
export interface AccountDto {
|
||||
id: number;
|
||||
subdomain: string;
|
||||
createdAt: string;
|
||||
companyName: string;
|
||||
logoUrl: Nullable<string>;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Currency, DateFormat, Language, Nullable, PhoneFormat, WeekDays } from '@/shared';
|
||||
|
||||
export interface AccountSettingsDto {
|
||||
language: Language;
|
||||
isBpmnEnable: boolean;
|
||||
phoneFormat: PhoneFormat;
|
||||
allowDuplicates: boolean;
|
||||
timeZone: Nullable<string>;
|
||||
currency: Nullable<Currency>;
|
||||
numberFormat: Nullable<string>;
|
||||
startOfWeek: Nullable<WeekDays>;
|
||||
dateFormat: Nullable<DateFormat>;
|
||||
workingDays: Nullable<WeekDays[]>;
|
||||
workingTimeTo: Nullable<string>;
|
||||
workingTimeFrom: Nullable<string>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Currency, DateFormat, Language, Nullable, PhoneFormat, WeekDays } from '@/shared';
|
||||
|
||||
export interface UpdateAccountSettingsDto {
|
||||
language: Language;
|
||||
phoneFormat: PhoneFormat;
|
||||
allowDuplicates: boolean;
|
||||
timeZone: Nullable<string>;
|
||||
currency: Nullable<Currency>;
|
||||
numberFormat: Nullable<string>;
|
||||
startOfWeek: Nullable<WeekDays>;
|
||||
dateFormat: Nullable<DateFormat>;
|
||||
workingDays: Nullable<WeekDays[]>;
|
||||
workingTimeTo: Nullable<string>;
|
||||
workingTimeFrom: Nullable<string>;
|
||||
}
|
||||
70
frontend/src/app/api/dtos/Permission/ObjectPermissionDto.ts
Normal file
70
frontend/src/app/api/dtos/Permission/ObjectPermissionDto.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { type Nullable, PermissionLevel, type PermissionObjectType } from '@/shared';
|
||||
|
||||
export class ObjectPermissionDto {
|
||||
objectType: PermissionObjectType;
|
||||
objectId: Nullable<number>;
|
||||
createPermission: PermissionLevel;
|
||||
viewPermission: PermissionLevel;
|
||||
editPermission: PermissionLevel;
|
||||
deletePermission: PermissionLevel;
|
||||
reportPermission: PermissionLevel;
|
||||
dashboardPermission: PermissionLevel;
|
||||
|
||||
constructor({
|
||||
objectType,
|
||||
objectId,
|
||||
createPermission,
|
||||
viewPermission,
|
||||
editPermission,
|
||||
deletePermission,
|
||||
reportPermission,
|
||||
dashboardPermission,
|
||||
}: ObjectPermissionDto) {
|
||||
this.objectType = objectType;
|
||||
this.objectId = objectId;
|
||||
this.createPermission = createPermission;
|
||||
this.viewPermission = viewPermission;
|
||||
this.editPermission = editPermission;
|
||||
this.deletePermission = deletePermission;
|
||||
this.reportPermission = reportPermission;
|
||||
this.dashboardPermission = dashboardPermission;
|
||||
}
|
||||
|
||||
static createAllAllowed({
|
||||
objectId,
|
||||
objectType,
|
||||
}: {
|
||||
objectId: Nullable<number>;
|
||||
objectType: PermissionObjectType;
|
||||
}): ObjectPermissionDto {
|
||||
return new ObjectPermissionDto({
|
||||
objectId,
|
||||
objectType,
|
||||
viewPermission: PermissionLevel.ALLOWED,
|
||||
editPermission: PermissionLevel.ALLOWED,
|
||||
createPermission: PermissionLevel.ALLOWED,
|
||||
deletePermission: PermissionLevel.ALLOWED,
|
||||
reportPermission: PermissionLevel.ALLOWED,
|
||||
dashboardPermission: PermissionLevel.ALLOWED,
|
||||
});
|
||||
}
|
||||
|
||||
static createAllDenied({
|
||||
objectId,
|
||||
objectType,
|
||||
}: {
|
||||
objectId: Nullable<number>;
|
||||
objectType: PermissionObjectType;
|
||||
}): ObjectPermissionDto {
|
||||
return new ObjectPermissionDto({
|
||||
objectId,
|
||||
objectType,
|
||||
viewPermission: PermissionLevel.DENIED,
|
||||
editPermission: PermissionLevel.DENIED,
|
||||
createPermission: PermissionLevel.DENIED,
|
||||
deletePermission: PermissionLevel.DENIED,
|
||||
reportPermission: PermissionLevel.DENIED,
|
||||
dashboardPermission: PermissionLevel.DENIED,
|
||||
});
|
||||
}
|
||||
}
|
||||
15
frontend/src/app/api/dtos/Profile/UpdateUserProfileDto.ts
Normal file
15
frontend/src/app/api/dtos/Profile/UpdateUserProfileDto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
|
||||
export class UpdateUserProfileDto {
|
||||
birthDate?: Nullable<string>;
|
||||
employmentDate?: Nullable<string>;
|
||||
workingTimeFrom?: Nullable<string>;
|
||||
workingTimeTo?: Nullable<string>;
|
||||
|
||||
constructor({ birthDate, employmentDate, workingTimeFrom, workingTimeTo }: UpdateUserProfileDto) {
|
||||
this.birthDate = birthDate;
|
||||
this.employmentDate = employmentDate;
|
||||
this.workingTimeFrom = workingTimeFrom;
|
||||
this.workingTimeTo = workingTimeTo;
|
||||
}
|
||||
}
|
||||
23
frontend/src/app/api/dtos/Profile/UserProfileDto.ts
Normal file
23
frontend/src/app/api/dtos/Profile/UserProfileDto.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
|
||||
export class UserProfileDto {
|
||||
userId: number;
|
||||
birthDate: Nullable<string>;
|
||||
employmentDate: Nullable<string>;
|
||||
workingTimeFrom: Nullable<string>;
|
||||
workingTimeTo: Nullable<string>;
|
||||
|
||||
constructor({
|
||||
userId,
|
||||
birthDate,
|
||||
employmentDate,
|
||||
workingTimeFrom,
|
||||
workingTimeTo,
|
||||
}: UserProfileDto) {
|
||||
this.userId = userId;
|
||||
this.birthDate = birthDate;
|
||||
this.employmentDate = employmentDate;
|
||||
this.workingTimeFrom = workingTimeFrom;
|
||||
this.workingTimeTo = workingTimeTo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
|
||||
export class SiteFormAnalyticDataDto {
|
||||
code: string;
|
||||
value: Nullable<unknown>;
|
||||
|
||||
constructor({ code, value }: SiteFormAnalyticDataDto) {
|
||||
this.code = code;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
13
frontend/src/app/api/dtos/SiteForm/SiteFormDataDto.ts
Normal file
13
frontend/src/app/api/dtos/SiteForm/SiteFormDataDto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
import type { SiteFormAnalyticDataDto } from './SiteFormAnalyticDataDto';
|
||||
import type { SiteFormFieldDataDto } from './SiteFormFieldDataDto';
|
||||
|
||||
export class SiteFormDataDto {
|
||||
fields?: Nullable<SiteFormFieldDataDto[]>;
|
||||
analytics?: Nullable<SiteFormAnalyticDataDto[]>;
|
||||
|
||||
constructor({ fields, analytics }: SiteFormDataDto) {
|
||||
this.fields = fields;
|
||||
this.analytics = analytics;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class SiteFormFieldDataDto {
|
||||
id: number;
|
||||
value: unknown;
|
||||
|
||||
constructor({ id, value }: SiteFormFieldDataDto) {
|
||||
this.id = id;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
11
frontend/src/app/api/dtos/SiteForm/SiteFormResultDto.ts
Normal file
11
frontend/src/app/api/dtos/SiteForm/SiteFormResultDto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
|
||||
export class SiteFormResultDto {
|
||||
result: boolean;
|
||||
message: Nullable<string>;
|
||||
|
||||
constructor({ result, message }: SiteFormResultDto) {
|
||||
this.result = result;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
17
frontend/src/app/api/dtos/Stage/CreateStageDto.ts
Normal file
17
frontend/src/app/api/dtos/Stage/CreateStageDto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Nullable, StageCode } from '@/shared';
|
||||
|
||||
export class CreateStageDto {
|
||||
name: string;
|
||||
color: string;
|
||||
code: Nullable<StageCode>;
|
||||
sortOrder?: number;
|
||||
isSystem?: boolean;
|
||||
|
||||
constructor({ name, color, code, sortOrder, isSystem }: CreateStageDto) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.code = code;
|
||||
this.sortOrder = sortOrder;
|
||||
this.isSystem = isSystem;
|
||||
}
|
||||
}
|
||||
23
frontend/src/app/api/dtos/Stage/StageDto.ts
Normal file
23
frontend/src/app/api/dtos/Stage/StageDto.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Nullable, ObjectState, StageCode } from '@/shared';
|
||||
|
||||
export class StageDto {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
boardId: number;
|
||||
isSystem: boolean;
|
||||
sortOrder: number;
|
||||
state: ObjectState;
|
||||
code: Nullable<StageCode>;
|
||||
|
||||
constructor({ id, name, color, code, isSystem, sortOrder, boardId, state }: StageDto) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.code = code;
|
||||
this.color = color;
|
||||
this.state = state;
|
||||
this.boardId = boardId;
|
||||
this.isSystem = isSystem;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
}
|
||||
19
frontend/src/app/api/dtos/Stage/UpdateStageDto.ts
Normal file
19
frontend/src/app/api/dtos/Stage/UpdateStageDto.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Nullable, StageCode } from '@/shared';
|
||||
|
||||
export class UpdateStageDto {
|
||||
id: number;
|
||||
name?: string;
|
||||
color?: string;
|
||||
code?: Nullable<StageCode>;
|
||||
isSystem?: boolean;
|
||||
sortOrder?: number;
|
||||
|
||||
constructor({ id, name, color, code, isSystem, sortOrder }: UpdateStageDto) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.code = code;
|
||||
this.isSystem = isSystem;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
}
|
||||
12
frontend/src/app/api/dtos/Subscription/SubscriptionDto.ts
Normal file
12
frontend/src/app/api/dtos/Subscription/SubscriptionDto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { AppSumoTiers, Nullable } from '@/shared';
|
||||
|
||||
export interface SubscriptionDto {
|
||||
isTrial: boolean;
|
||||
isValid: boolean;
|
||||
createdAt: string;
|
||||
userLimit: number;
|
||||
isExternal: boolean;
|
||||
expiredAt: Nullable<string>;
|
||||
planName: AppSumoTiers | string;
|
||||
firstVisit?: Nullable<string>;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class SubscriptionFeatureDto {
|
||||
name: string;
|
||||
available: boolean;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Nullable } from '@/shared';
|
||||
import type { SubscriptionFeatureDto } from './SubscriptionFeatureDto';
|
||||
import type { SubscriptionPriceDto } from './SubscriptionPriceDto';
|
||||
|
||||
export class SubscriptionPlanDto {
|
||||
id: string;
|
||||
name: string;
|
||||
order: number;
|
||||
code?: string;
|
||||
isDefault: boolean;
|
||||
userLimit: Nullable<number>;
|
||||
description: Nullable<string>;
|
||||
prices: SubscriptionPriceDto[];
|
||||
features: SubscriptionFeatureDto[];
|
||||
defaultPriceId: Nullable<string> = null;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { type BillingInterval } from '../../../../shared/lib/models/Subscription/BillingInterval';
|
||||
|
||||
export class SubscriptionPriceDto {
|
||||
id: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
interval: BillingInterval;
|
||||
|
||||
constructor({ id, amount, currency, interval }: SubscriptionPriceDto) {
|
||||
this.id = id;
|
||||
this.amount = amount;
|
||||
this.currency = currency;
|
||||
this.interval = interval;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface UpdateSubscriptionDto {
|
||||
isTrial?: boolean;
|
||||
periodStart?: string;
|
||||
periodEnd?: string;
|
||||
userLimit?: number;
|
||||
planName?: string;
|
||||
externalCustomerId?: string | null;
|
||||
firstVisit?: string;
|
||||
}
|
||||
9
frontend/src/app/api/dtos/User/ChangeUserPasswordDto.ts
Normal file
9
frontend/src/app/api/dtos/User/ChangeUserPasswordDto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class ChangeUserPasswordDto {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
|
||||
constructor({ currentPassword, newPassword }: ChangeUserPasswordDto) {
|
||||
this.currentPassword = currentPassword;
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
}
|
||||
39
frontend/src/app/api/dtos/User/CreateUserDto.ts
Normal file
39
frontend/src/app/api/dtos/User/CreateUserDto.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Nullable, UserRole } from '@/shared';
|
||||
import type { ObjectPermissionDto } from '../Permission/ObjectPermissionDto';
|
||||
|
||||
export class CreateUserDto {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: Nullable<string>;
|
||||
password: string;
|
||||
role: UserRole;
|
||||
departmentId: Nullable<number>;
|
||||
position: Nullable<string>;
|
||||
objectPermissions: ObjectPermissionDto[];
|
||||
accessibleUserIds?: Nullable<number[]>;
|
||||
|
||||
constructor({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
phone,
|
||||
password,
|
||||
role,
|
||||
departmentId,
|
||||
position,
|
||||
objectPermissions,
|
||||
accessibleUserIds,
|
||||
}: CreateUserDto) {
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.email = email;
|
||||
this.phone = phone;
|
||||
this.password = password;
|
||||
this.role = role;
|
||||
this.departmentId = departmentId;
|
||||
this.position = position;
|
||||
this.objectPermissions = objectPermissions;
|
||||
this.accessibleUserIds = accessibleUserIds;
|
||||
}
|
||||
}
|
||||
58
frontend/src/app/api/dtos/User/UpdateUserDto.ts
Normal file
58
frontend/src/app/api/dtos/User/UpdateUserDto.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Nullable, User, UserRole } from '@/shared';
|
||||
import type { ObjectPermissionDto } from '../Permission/ObjectPermissionDto';
|
||||
|
||||
export class UpdateUserDto {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: Nullable<string>;
|
||||
password: Nullable<string>;
|
||||
role: UserRole;
|
||||
avatarUrl: Nullable<string>;
|
||||
departmentId: Nullable<number>;
|
||||
position: Nullable<string>;
|
||||
objectPermissions: ObjectPermissionDto[];
|
||||
accessibleUserIds?: Nullable<number[]>;
|
||||
|
||||
constructor({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
phone,
|
||||
password,
|
||||
role,
|
||||
avatarUrl,
|
||||
departmentId,
|
||||
position,
|
||||
objectPermissions,
|
||||
accessibleUserIds,
|
||||
}: UpdateUserDto) {
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.email = email;
|
||||
this.phone = phone;
|
||||
this.password = password;
|
||||
this.role = role;
|
||||
this.avatarUrl = avatarUrl;
|
||||
this.departmentId = departmentId;
|
||||
this.position = position;
|
||||
this.objectPermissions = objectPermissions;
|
||||
this.accessibleUserIds = accessibleUserIds;
|
||||
}
|
||||
|
||||
static fromExistingUser(user: User): UpdateUserDto {
|
||||
return new UpdateUserDto({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName || '',
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
password: null,
|
||||
role: user.role,
|
||||
avatarUrl: user.avatarUrl,
|
||||
departmentId: user.departmentId,
|
||||
position: user.position,
|
||||
objectPermissions: user.objectPermissions,
|
||||
accessibleUserIds: user.accessibleUserIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
19
frontend/src/app/api/dtos/User/UserDto.ts
Normal file
19
frontend/src/app/api/dtos/User/UserDto.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Nullable, UserRole } from '@/shared';
|
||||
import type { ObjectPermissionDto } from '../Permission/ObjectPermissionDto';
|
||||
|
||||
export interface UserDto {
|
||||
id: number;
|
||||
firstName: string;
|
||||
lastName: Nullable<string>;
|
||||
email: string;
|
||||
phone: Nullable<string>;
|
||||
role: UserRole;
|
||||
isActive: boolean;
|
||||
departmentId: Nullable<number>;
|
||||
avatarUrl: Nullable<string>;
|
||||
position: Nullable<string>;
|
||||
objectPermissions?: ObjectPermissionDto[];
|
||||
analyticsId: string;
|
||||
accessibleUserIds: number[];
|
||||
isPlatformAdmin?: boolean;
|
||||
}
|
||||
5
frontend/src/app/api/dtos/Version/VersionDto.ts
Normal file
5
frontend/src/app/api/dtos/Version/VersionDto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface VersionDto {
|
||||
id: number;
|
||||
version: string;
|
||||
date: string;
|
||||
}
|
||||
46
frontend/src/app/api/dtos/index.tsx
Normal file
46
frontend/src/app/api/dtos/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
export { SequenceName } from '../IdentityApi/IdentityApi';
|
||||
export type { BoardDto } from './Board/BoardDto';
|
||||
export { CreateBoardDto } from './Board/CreateBoardDto';
|
||||
export { UpdateBoardDto } from './Board/UpdateBoardDto';
|
||||
export { FeatureDto } from './Builder/FeatureDto';
|
||||
export { CreateContactAndLeadDto } from './CreateContactAndLeadDto';
|
||||
export type { EntityDto } from './Entity/EntityDto';
|
||||
export type { ContactInfoDto } from './EntityEvent/ContactInfoDto';
|
||||
export type { EntityEventItemDto } from './EntityEvent/EntityEventItemDto';
|
||||
export type { EntityInfoDto } from './EntityEvent/EntityInfoDto';
|
||||
export { EntityLinkDto } from './EntityLink/EntityLinkDto';
|
||||
export type { EntityTypeDto } from './EntityType/EntityTypeDto';
|
||||
export { UpdateEntityTypeDto } from './EntityType/UpdateEntityTypeDto';
|
||||
export { UpdateEntityTypeFieldsModel } from './EntityType/UpdateEntityTypeFieldsModel';
|
||||
export { FeedbackType } from './Feedback/FeedbackType';
|
||||
export { SendFeedbackDto, type FeedbackPayload } from './Feedback/SendFeedbackDto';
|
||||
export { TrialExpiredFeedback } from './Feedback/TrialExpiredFeedback';
|
||||
export { UserLimitFeedback } from './Feedback/UserLimitFeedback';
|
||||
export type { FeedItemDto } from './FeedItem/FeedItemDto';
|
||||
export type { FileInfoResultDto } from './FileInfoResult/FileInfoResultDto';
|
||||
export type { FileLinkDto } from './FileLink/FileLinkDto';
|
||||
export { SendContactUsFormDto } from './Form/SendContactUsFormDto';
|
||||
export { CreateFrontendObjectDto } from './FrontendObject/CreateFrontendObjectDto';
|
||||
export type { FrontendObjectDto } from './FrontendObject/FrontendObjectDto';
|
||||
export type { AccountDto } from './GeneralSettings/AccountDto';
|
||||
export type { AccountSettingsDto } from './GeneralSettings/AccountSettingsDto';
|
||||
export type { UpdateAccountSettingsDto } from './GeneralSettings/UpdateAccountSettingsDto';
|
||||
export { ObjectPermissionDto } from './Permission/ObjectPermissionDto';
|
||||
export { UpdateUserProfileDto } from './Profile/UpdateUserProfileDto';
|
||||
export { UserProfileDto } from './Profile/UserProfileDto';
|
||||
export { SiteFormDataDto } from './SiteForm/SiteFormDataDto';
|
||||
export { SiteFormFieldDataDto } from './SiteForm/SiteFormFieldDataDto';
|
||||
export { SiteFormResultDto } from './SiteForm/SiteFormResultDto';
|
||||
export { CreateStageDto } from './Stage/CreateStageDto';
|
||||
export { StageDto } from './Stage/StageDto';
|
||||
export { UpdateStageDto } from './Stage/UpdateStageDto';
|
||||
export type { SubscriptionDto } from './Subscription/SubscriptionDto';
|
||||
export type { SubscriptionFeatureDto } from './Subscription/SubscriptionFeatureDto';
|
||||
export type { SubscriptionPlanDto } from './Subscription/SubscriptionPlanDto';
|
||||
export type { SubscriptionPriceDto } from './Subscription/SubscriptionPriceDto';
|
||||
export type { UpdateSubscriptionDto } from './Subscription/UpdateSubscriptionDto';
|
||||
export { ChangeUserPasswordDto } from './User/ChangeUserPasswordDto';
|
||||
export { CreateUserDto } from './User/CreateUserDto';
|
||||
export { UpdateUserDto } from './User/UpdateUserDto';
|
||||
export type { UserDto } from './User/UserDto';
|
||||
export type { VersionDto } from './Version/VersionDto';
|
||||
27
frontend/src/app/api/index.tsx
Normal file
27
frontend/src/app/api/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
export { ApiRoutes } from './ApiRoutes';
|
||||
export { baseApi, determineApiHost } from './BaseApi/BaseApi';
|
||||
export { boardApi } from './BoardApi/BoardApi';
|
||||
export { boardApiUtil } from './BoardApi/BoardApiUtil';
|
||||
export { dadataApi } from './DadataApi/DadataApi';
|
||||
export * from './dtos';
|
||||
export { entityTypeApi } from './EntityTypeApi/EntityTypeApi';
|
||||
export { featureApi } from './FeatureApi/FeatureApi';
|
||||
export { feedbackApi } from './FeedbackApi/FeedbackApi';
|
||||
export { fileApi, type FileUploadResult } from './FileApi/FileApi';
|
||||
export { useGetFileInfos } from './FileApi/queries/useGetFileInfos';
|
||||
export { formApi } from './FormApi/FormApi';
|
||||
export { frontendObjectsApi } from './FrontendObjectsApi/FrontendObjectsApi';
|
||||
export { generalSettingsApi } from './GeneralSettingsApi/GeneralSettingsApi';
|
||||
export { identityApi } from './IdentityApi/IdentityApi';
|
||||
export type { IdentityPool } from './IdentityApi/IdentityApi';
|
||||
export { stageApi } from './StageApi/StageApi';
|
||||
export { stageApiUtil } from './StageApi/StageApiUtil';
|
||||
export { useGetSubscriptionForAccount } from './SubscriptionApi/queries/useGetSubscriptionForAccount';
|
||||
export { useUpdateSubscription } from './SubscriptionApi/queries/useUpdateSubscription';
|
||||
export { subscriptionApi } from './SubscriptionApi/SubscriptionApi';
|
||||
export { userApi } from './UserApi/UserApi';
|
||||
export { invalidateUserProfilesInCache } from './UserProfileApi/helpers/invalidateUserProfilesInCache';
|
||||
export { useGetUserProfiles } from './UserProfileApi/queries/useGetUserProfiles';
|
||||
export { userProfileApi } from './UserProfileApi/UserProfileApi';
|
||||
export { utilityApi } from './UtilityApi/UtilityApi';
|
||||
export { useGetLatestFrontendVersionPolling } from './VersionApi/queries/useGetLatestFrontendVersionPolling';
|
||||
56
frontend/src/app/config/i18n/i18n.ts
Normal file
56
frontend/src/app/config/i18n/i18n.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import i18n from 'i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend, { type HttpBackendOptions } from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
// to load namespaces in parallel on application start
|
||||
const ns: string[] = [
|
||||
'common',
|
||||
'module.automation',
|
||||
'module.bpmn',
|
||||
'module.builder',
|
||||
'module.fields',
|
||||
'module.mailing',
|
||||
'module.notifications',
|
||||
'module.multichat',
|
||||
'module.notes',
|
||||
'module.products',
|
||||
'module.reporting',
|
||||
'module.scheduler',
|
||||
'module.telephony',
|
||||
'module.tutorial',
|
||||
'page.board-settings',
|
||||
'page.login',
|
||||
'page.settings',
|
||||
'page.system',
|
||||
'page.tasks',
|
||||
'store.field-groups-store',
|
||||
'store.fields-store',
|
||||
'component.card',
|
||||
'component.entity-board',
|
||||
'component.section',
|
||||
];
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init<HttpBackendOptions>({
|
||||
debug: false,
|
||||
fallbackLng: 'en',
|
||||
defaultNS: 'common',
|
||||
load: 'languageOnly',
|
||||
maxParallelReads: 32,
|
||||
|
||||
ns,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
});
|
||||
|
||||
export { i18n };
|
||||
49
frontend/src/app/config/knip/knip.ts
Normal file
49
frontend/src/app/config/knip/knip.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { KnipConfig } from 'knip';
|
||||
|
||||
// https://knip.dev/overview/configuration
|
||||
const config: KnipConfig = {
|
||||
entry: ['src/index.tsx'],
|
||||
project: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
ignore: [
|
||||
'src/app/config',
|
||||
'.storybook',
|
||||
'src/shared/assets/docs-images/index.tsx',
|
||||
'src/shared/lib/hooks/useTracePropsUpdate.ts',
|
||||
'src/shared/lib/helpers/capitalizeFirstLetter.ts',
|
||||
'src/modules/scheduler/api/ScheduleAppointmentApi/queries/useGetSchedulerTotalVisits.ts',
|
||||
'src/modules/scheduler/api/ScheduleApi/helpers/invalidateScheduleInCache.ts',
|
||||
'src/modules/multichat/api/ChatProviderApi/queries/useGetExternalChatProviders.ts',
|
||||
],
|
||||
exclude: [
|
||||
// https://github.com/webpro/knip#reading-the-report
|
||||
'enumMembers',
|
||||
'nsExports',
|
||||
'nsTypes',
|
||||
'classMembers',
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
includeEntryExports: true,
|
||||
ignoreDependencies: [
|
||||
// deps
|
||||
'file-saver',
|
||||
'@babel/plugin-proposal-decorators',
|
||||
'@babel/plugin-transform-class-properties',
|
||||
'@babel/plugin-transform-typescript',
|
||||
'@babel/preset-typescript',
|
||||
'@emotion/react',
|
||||
'@emotion/utils',
|
||||
'@tabler/icons',
|
||||
'normalize.css',
|
||||
'@tiptap/extension-text-style',
|
||||
// dev deps
|
||||
'@storybook/blocks',
|
||||
'babel-plugin-styled-components',
|
||||
'@react-pdf-viewer/default-layout',
|
||||
// unlisted deps
|
||||
'@tanstack/table-core',
|
||||
'@tanstack/table-core',
|
||||
'@tiptap/core',
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
37
frontend/src/app/config/newrelic/newrelic.ts
Normal file
37
frontend/src/app/config/newrelic/newrelic.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { envUtil } from '@/shared';
|
||||
|
||||
const config = {
|
||||
init: {
|
||||
session_replay: {
|
||||
enabled: true,
|
||||
block_selector: '',
|
||||
mask_text_selector: '*',
|
||||
sampling_rate: 10.0,
|
||||
error_sampling_rate: 100.0,
|
||||
mask_all_inputs: true,
|
||||
collect_fonts: true,
|
||||
inline_images: false,
|
||||
inline_stylesheet: true,
|
||||
mask_input_options: {},
|
||||
},
|
||||
distributed_tracing: { enabled: true },
|
||||
privacy: { cookies_enabled: true },
|
||||
ajax: { deny_list: ['bam.eu01.nr-data.net'] },
|
||||
},
|
||||
info: {
|
||||
beacon: 'bam.eu01.nr-data.net',
|
||||
errorBeacon: 'bam.eu01.nr-data.net',
|
||||
licenseKey: envUtil.newRelicLicenseKey,
|
||||
applicationID: envUtil.newRelicApplicationId,
|
||||
sa: 1,
|
||||
},
|
||||
loader_config: {
|
||||
accountID: envUtil.newRelicAccountId,
|
||||
trustKey: envUtil.newRelicAccountId,
|
||||
agentID: envUtil.newRelicApplicationId,
|
||||
licenseKey: envUtil.newRelicLicenseKey,
|
||||
applicationID: envUtil.newRelicApplicationId,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
20
frontend/src/app/config/storybook/I18nDecorator.tsx
Normal file
20
frontend/src/app/config/storybook/I18nDecorator.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Decorator } from '@storybook/react';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { i18n } from '../i18n/i18n';
|
||||
|
||||
export const I18nDecorator: Decorator = (StoryComponent, context) => {
|
||||
const { locale } = context.globals;
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(locale);
|
||||
}, [locale]);
|
||||
|
||||
return (
|
||||
<Suspense fallback="Some translations are loading...">
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<StoryComponent />
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
8
frontend/src/app/config/storybook/RouterDecorator.tsx
Normal file
8
frontend/src/app/config/storybook/RouterDecorator.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Decorator } from '@storybook/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
export const RouterDecorator: Decorator = StoryComponent => (
|
||||
<BrowserRouter>
|
||||
<StoryComponent />
|
||||
</BrowserRouter>
|
||||
);
|
||||
4
frontend/src/app/config/storybook/StyleDecorator.tsx
Normal file
4
frontend/src/app/config/storybook/StyleDecorator.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { Decorator } from '@storybook/react';
|
||||
import '../../styles/main.css';
|
||||
|
||||
export const StyleDecorator: Decorator = StoryComponent => <StoryComponent />;
|
||||
4
frontend/src/app/index.tsx
Normal file
4
frontend/src/app/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './api';
|
||||
export { App } from './App';
|
||||
export * from './routes';
|
||||
export * from './store';
|
||||
42
frontend/src/app/pages/HomePage/HomePage.tsx
Normal file
42
frontend/src/app/pages/HomePage/HomePage.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { authStore } from '@/modules/auth';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import { SectionView, WholePageLoaderWithLogo } from '../../../shared';
|
||||
import { routes } from '../../routes';
|
||||
import { appStore, entityTypeStore } from '../../store';
|
||||
|
||||
export const HomePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const redirect = useCallback((): void => {
|
||||
const firstEt = entityTypeStore.getAvailableEntityTypes()[0];
|
||||
|
||||
if (!firstEt) {
|
||||
navigate(routes.timeBoard());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstEt.section.view === SectionView.BOARD) {
|
||||
navigate(routes.boardSection({ entityTypeId: firstEt.id }), {
|
||||
replace: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(routes.listSection(firstEt.id), { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (appStore.isLoaded) redirect();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appStore.isLoaded]);
|
||||
|
||||
if (authStore.user && authStore.user.isPartner())
|
||||
return <Navigate to={routes.partnerInfo(authStore.user.id)} />;
|
||||
|
||||
return <WholePageLoaderWithLogo />;
|
||||
});
|
||||
1
frontend/src/app/pages/index.tsx
Normal file
1
frontend/src/app/pages/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { HomePage } from './HomePage/HomePage';
|
||||
30
frontend/src/app/routes/AppBoundary.tsx
Normal file
30
frontend/src/app/routes/AppBoundary.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { appStore } from '@/app';
|
||||
import { MultichatModal } from '@/modules/multichat';
|
||||
import { NotesModal } from '@/modules/notes';
|
||||
import { TelephonyModal } from '@/modules/telephony';
|
||||
import { ErrorBoundary, PageTracker, ReloadModal, ToastContainer, VersionModal } from '@/shared';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const AppBoundary = observer(() => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<PageTracker />
|
||||
<Outlet />
|
||||
|
||||
{appStore.isLoaded && (
|
||||
<>
|
||||
<ToastContainer />
|
||||
<MultichatModal />
|
||||
<NotesModal />
|
||||
<TelephonyModal />
|
||||
<VersionModal />
|
||||
<ReloadModal />
|
||||
</>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
AppBoundary.displayName = 'AppBoundary';
|
||||
export { AppBoundary };
|
||||
379
frontend/src/app/routes/createRouter.tsx
Normal file
379
frontend/src/app/routes/createRouter.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
import { AppBoundary } from '@/app';
|
||||
import { LoginLinkPage, LoginPage } from '@/modules/auth';
|
||||
import { AutomationProcessesPage, ListSectionAutomationProcessesPage } from '@/modules/bpmn';
|
||||
import {
|
||||
BuilderHomePage,
|
||||
EtSectionBuilderPage,
|
||||
HeadlessSiteFormBuilderPage,
|
||||
OnlineBookingSiteFormBuilderPage,
|
||||
ProductsSectionBuilderPage,
|
||||
SchedulerBuilderPage,
|
||||
SiteFormBuilderPage,
|
||||
} from '@/modules/builder';
|
||||
import { AddCardPage, CardPage } from '@/modules/card';
|
||||
import { MailingPage, MailingSettingsPage } from '@/modules/mailing';
|
||||
import { MultichatPage } from '@/modules/multichat';
|
||||
import { NotesPage } from '@/modules/notes';
|
||||
import { PartnerInfoPage } from '@/modules/partner';
|
||||
import { ProductPage, ProductsPage, ShipmentPage } from '@/modules/products';
|
||||
import { GoalSettingsPage } from '@/modules/reporting';
|
||||
import { SchedulerPage } from '@/modules/scheduler';
|
||||
import {
|
||||
EntitiesBoardSettingsPage,
|
||||
EntitiesListAutomationPage,
|
||||
EntitiesListPage,
|
||||
EntitiesPage,
|
||||
EverythingPage,
|
||||
TasksBoardSettingsPage,
|
||||
} from '@/modules/section';
|
||||
import {
|
||||
AccountApiAccessPage,
|
||||
CommonBillingPage,
|
||||
DocumentCreationFieldsPage,
|
||||
DocumentTemplatesPage,
|
||||
EditDepartmentsPage,
|
||||
EditUserPage,
|
||||
GeneralSettingsPage,
|
||||
GoogleCalendarRedirectPage,
|
||||
IntegrationsPage,
|
||||
MyworkRequestInvoiceBillingPage,
|
||||
StripeBillingPage,
|
||||
SuperadminPage,
|
||||
UsersSettingsPage,
|
||||
} from '@/modules/settings';
|
||||
import { ActivitiesPage, TasksPage, TimeBoardPage } from '@/modules/tasks';
|
||||
import {
|
||||
CallsConfiguringScenariosPage,
|
||||
CallsSettingsAccountPage,
|
||||
CallsSettingsUsersPage,
|
||||
CallsSipRegistrationsPage,
|
||||
} from '@/modules/telephony';
|
||||
import {
|
||||
BrowserNotSupportedPage,
|
||||
EntitiesBoardSettingsTab,
|
||||
ForbiddenPage,
|
||||
NotFoundPage,
|
||||
WithAdminRole,
|
||||
WithAuth,
|
||||
WithPartnerRole,
|
||||
WithSuperadminRole,
|
||||
WithViewEntityReportPermission,
|
||||
WithViewSchedulerReportPermission,
|
||||
withPage,
|
||||
} from '@/shared';
|
||||
import { HttpStatusCode } from 'axios';
|
||||
import { Navigate, createBrowserRouter } from 'react-router-dom';
|
||||
import { HomePage } from '../pages';
|
||||
|
||||
// https://reactrouter.com/en/6.21.1/routers/create-browser-router
|
||||
export const createRouter = ({
|
||||
isBrowserSupported,
|
||||
}: {
|
||||
isBrowserSupported: boolean;
|
||||
}): ReturnType<typeof createBrowserRouter> => {
|
||||
if (!isBrowserSupported)
|
||||
return createBrowserRouter([
|
||||
{
|
||||
element: <AppBoundary />,
|
||||
children: [
|
||||
{
|
||||
path: '/browser-not-supported',
|
||||
element: <BrowserNotSupportedPage />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/browser-not-supported" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
return createBrowserRouter([
|
||||
{
|
||||
// https://reactrouter.com/en/main/components/outlet
|
||||
element: <AppBoundary />,
|
||||
children: [
|
||||
// Home
|
||||
{
|
||||
path: '/',
|
||||
element: WithAuth(HomePage),
|
||||
},
|
||||
|
||||
// Login
|
||||
{
|
||||
path: '/login',
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: '/login-link',
|
||||
element: <LoginLinkPage />,
|
||||
},
|
||||
|
||||
// Mail
|
||||
{
|
||||
path: '/mail',
|
||||
element: WithAuth(MailingPage),
|
||||
},
|
||||
|
||||
// Multichat
|
||||
{
|
||||
path: '/chat',
|
||||
element: WithAuth(MultichatPage),
|
||||
},
|
||||
{
|
||||
path: '/chat/:chatId',
|
||||
element: WithAuth(MultichatPage),
|
||||
},
|
||||
{
|
||||
path: '/chat/:chatId/message/:messageId',
|
||||
element: WithAuth(MultichatPage),
|
||||
},
|
||||
|
||||
// Notes
|
||||
{
|
||||
path: '/notes',
|
||||
element: WithAuth(NotesPage),
|
||||
},
|
||||
|
||||
// Tasks and Activities
|
||||
{
|
||||
path: '/activities/:tab/:view?/:year?/:month?/:day?',
|
||||
element: WithAuth(withPage(ActivitiesPage)),
|
||||
},
|
||||
{
|
||||
path: '/tasks/deadline/:tab/:view?/:year?/:month?/:day?',
|
||||
element: WithAuth(withPage(TimeBoardPage)),
|
||||
},
|
||||
{
|
||||
path: '/tasks/b/:boardId/:tab/:view?/:year?/:month?/:day?',
|
||||
element: WithAuth(withPage(TasksPage)),
|
||||
},
|
||||
{
|
||||
path: '/tasks/b/:boardId/board-settings',
|
||||
element: WithAuth(TasksBoardSettingsPage),
|
||||
},
|
||||
|
||||
// Entities list and board
|
||||
{
|
||||
path: '/et/:entityTypeId/list',
|
||||
element: WithAuth(withPage(EntitiesListPage)),
|
||||
},
|
||||
{
|
||||
path: `/et/:entityTypeId/list/${EntitiesBoardSettingsTab.AUTOMATION}`,
|
||||
element: WithAdminRole(withPage(EntitiesListAutomationPage)),
|
||||
},
|
||||
{
|
||||
path: `/et/:entityTypeId/list/${EntitiesBoardSettingsTab.AUTOMATION_BPMN}`,
|
||||
element: WithAdminRole(withPage(ListSectionAutomationProcessesPage)),
|
||||
},
|
||||
{
|
||||
path: '/et/:entityTypeId/b/:boardId/:tab/:view?',
|
||||
element: WithViewEntityReportPermission(withPage(EntitiesPage)),
|
||||
},
|
||||
{
|
||||
path: '/et/:entityTypeId/everything',
|
||||
element: WithAuth(withPage(EverythingPage)),
|
||||
},
|
||||
{
|
||||
path: '/et/:entityTypeId/b/:boardId/board-settings/:tab',
|
||||
element: WithAdminRole(withPage(EntitiesBoardSettingsPage)),
|
||||
},
|
||||
{
|
||||
path: `/et/:entityTypeId/b/:boardId/board-settings/${EntitiesBoardSettingsTab.AUTOMATION_BPMN}`,
|
||||
element: WithAdminRole(withPage(AutomationProcessesPage)),
|
||||
},
|
||||
|
||||
// Set sales goals page
|
||||
{
|
||||
path: '/et/:entityTypeId/goal-settings',
|
||||
element: WithAdminRole(withPage(GoalSettingsPage)),
|
||||
},
|
||||
|
||||
// Entity card
|
||||
{
|
||||
path: '/et/:entityTypeId/card/:entityId/:tab/:view?/:year?/:month?/:day?',
|
||||
element: WithAuth(CardPage),
|
||||
},
|
||||
{
|
||||
path: '/et/:entityTypeId/card/add',
|
||||
element: WithAuth(AddCardPage),
|
||||
},
|
||||
|
||||
// Users settings
|
||||
{
|
||||
path: '/settings/users/list',
|
||||
element: WithAdminRole(UsersSettingsPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/users/list/new',
|
||||
element: WithAdminRole(EditUserPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/users/groups',
|
||||
element: WithAdminRole(EditDepartmentsPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/users/list/:id',
|
||||
element: WithAdminRole(EditUserPage),
|
||||
},
|
||||
|
||||
// Billing
|
||||
{
|
||||
path: '/settings/billing/common',
|
||||
element: WithAdminRole(CommonBillingPage, false),
|
||||
},
|
||||
{
|
||||
path: '/settings/billing/stripe',
|
||||
element: WithAdminRole(StripeBillingPage, false),
|
||||
},
|
||||
{
|
||||
path: '/settings/billing/invoice',
|
||||
element: WithAdminRole(MyworkRequestInvoiceBillingPage, false),
|
||||
},
|
||||
|
||||
{
|
||||
path: '/settings/general',
|
||||
element: WithAdminRole(GeneralSettingsPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/api',
|
||||
element: WithAdminRole(AccountApiAccessPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/superadmin',
|
||||
element: WithSuperadminRole(SuperadminPage),
|
||||
},
|
||||
{
|
||||
// Change carefully, should be synced with backend
|
||||
path: '/settings/integrations',
|
||||
element: WithAuth(IntegrationsPage),
|
||||
},
|
||||
// Wazzup wauth – do not change the path
|
||||
// https://wazzup24.com/help/api-en/wauth/
|
||||
{
|
||||
path: '/settings/integrations/wazzup/wauth',
|
||||
element: WithAuth(IntegrationsPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/mailing',
|
||||
element: WithAuth(MailingSettingsPage),
|
||||
},
|
||||
{
|
||||
// Google calendar backlink leads here – do not change the path
|
||||
path: '/settings/integrations/google-calendar',
|
||||
element: WithAuth(GoogleCalendarRedirectPage),
|
||||
},
|
||||
|
||||
// Documents creation settings
|
||||
{
|
||||
path: '/settings/documents/templates',
|
||||
element: WithAdminRole(DocumentTemplatesPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/documents/fields',
|
||||
element: WithAdminRole(DocumentCreationFieldsPage),
|
||||
},
|
||||
|
||||
// Calls settings
|
||||
{
|
||||
path: '/settings/calls/account',
|
||||
element: WithAdminRole(CallsSettingsAccountPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/calls/users',
|
||||
element: WithAdminRole(CallsSettingsUsersPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/calls/scenarios',
|
||||
element: WithAdminRole(CallsConfiguringScenariosPage),
|
||||
},
|
||||
{
|
||||
path: '/settings/calls/sip-registrations',
|
||||
element: WithAdminRole(CallsSipRegistrationsPage),
|
||||
},
|
||||
|
||||
// Builder
|
||||
{
|
||||
path: '/builder/:tab',
|
||||
element: WithAdminRole(BuilderHomePage),
|
||||
},
|
||||
|
||||
// Builder – Entity type
|
||||
{
|
||||
path: '/builder/et/:moduleId?',
|
||||
element: WithAdminRole(EtSectionBuilderPage),
|
||||
},
|
||||
|
||||
// Builder – Products
|
||||
{
|
||||
path: '/builder/products/:moduleId?',
|
||||
element: WithAdminRole(ProductsSectionBuilderPage),
|
||||
},
|
||||
|
||||
// Builder – Scheduler
|
||||
{
|
||||
path: '/builder/scheduler/:moduleId?',
|
||||
element: WithAdminRole(SchedulerBuilderPage),
|
||||
},
|
||||
|
||||
// Builder – Forms
|
||||
{
|
||||
path: '/builder/site-forms/:moduleId?',
|
||||
element: WithAdminRole(SiteFormBuilderPage),
|
||||
},
|
||||
{
|
||||
path: '/builder/site-forms/headless/:moduleId?',
|
||||
element: WithAdminRole(HeadlessSiteFormBuilderPage),
|
||||
},
|
||||
{
|
||||
path: '/builder/site-forms/online-booking/:moduleId?',
|
||||
element: WithAdminRole(OnlineBookingSiteFormBuilderPage),
|
||||
},
|
||||
|
||||
// Partners page
|
||||
{
|
||||
path: '/partners/:partnerId',
|
||||
element: WithPartnerRole(PartnerInfoPage),
|
||||
},
|
||||
|
||||
// Products
|
||||
{
|
||||
path: '/p/:sectionType/:sectionId/product/:productId',
|
||||
element: WithAuth(withPage(ProductPage)),
|
||||
},
|
||||
{
|
||||
path: '/p/:sectionType/:sectionId/:tab',
|
||||
element: WithAuth(ProductsPage),
|
||||
},
|
||||
|
||||
// Shipments
|
||||
{
|
||||
path: '/p/:sectionType/:sectionId/shipments/:shipmentId',
|
||||
element: WithAuth(ShipmentPage),
|
||||
},
|
||||
|
||||
// Scheduler
|
||||
{
|
||||
path: '/scheduler/:scheduleType/:scheduleId/:tab',
|
||||
element: WithViewSchedulerReportPermission(withPage(SchedulerPage)),
|
||||
},
|
||||
|
||||
// System pages
|
||||
{
|
||||
path: `/${HttpStatusCode.Forbidden}`,
|
||||
element: <ForbiddenPage />,
|
||||
},
|
||||
{
|
||||
path: `/${HttpStatusCode.NotFound}`,
|
||||
element: <NotFoundPage />,
|
||||
},
|
||||
|
||||
// Wildcard
|
||||
{
|
||||
path: '*',
|
||||
element: <NotFoundPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
};
|
||||
3
frontend/src/app/routes/index.tsx
Normal file
3
frontend/src/app/routes/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { AppBoundary } from './AppBoundary';
|
||||
export { createRouter } from './createRouter';
|
||||
export { PBX_PROVIDER_TYPE_QUERY_PARAM, routes } from './routes';
|
||||
645
frontend/src/app/routes/routes.ts
Normal file
645
frontend/src/app/routes/routes.ts
Normal file
@@ -0,0 +1,645 @@
|
||||
import type { BuilderTabs, ModuleCategory } from '@/modules/builder';
|
||||
import {
|
||||
CardTab,
|
||||
ORDER_ID_QUERY_PARAM,
|
||||
generateProductsSectionOrderTabValue,
|
||||
} from '@/modules/card';
|
||||
import type { GanttView } from '@/modules/gantt';
|
||||
import { MAILBOX_ID_QUERY_PARAM } from '@/modules/mailing';
|
||||
import { ProductsPageTabs, type ProductsSectionType } from '@/modules/products';
|
||||
import type { ReportsSection } from '@/modules/reporting';
|
||||
import { generateSchedulerLinkedEntityTypeTab, type ScheduleType } from '@/modules/scheduler';
|
||||
import {
|
||||
ALBATO_INFO_MODAL_QUERY_PARAM,
|
||||
APIX_DRIVE_INFO_MODAL_QUERY_PARAM,
|
||||
FB_FIRST_INFO_MODAL_QUERY_PARAM,
|
||||
GOOGLE_CALENDAR_CONNECT_MODAL_QUERY_PARAM,
|
||||
GOOGLE_CALENDAR_MANAGE_MODAL_QUERY_PARAM,
|
||||
MAKE_INFO_MODAL_QUERY_PARAM,
|
||||
ONE_C_INFO_MODAL_QUERY_PARAM,
|
||||
PBX_GROUP_ID,
|
||||
SALESFORCE_FIRST_INFO_MODAL_QUERY_PARAM,
|
||||
TILDA_INFO_MODAL_QUERY_PARAM,
|
||||
TWILIO_FIRST_INFO_MODAL_QUERY_PARAM,
|
||||
WAZZUP_FIRST_INFO_MODAL_QUERY_PARAM,
|
||||
WORDPRESS_INFO_MODAL_QUERY_PARAM,
|
||||
} from '@/modules/settings';
|
||||
import { TasksTab } from '@/modules/tasks';
|
||||
import {
|
||||
CommonQueryParams,
|
||||
EntitiesBoardSettingsTab,
|
||||
SectionView,
|
||||
type CalendarView,
|
||||
type EntityType,
|
||||
type Nullable,
|
||||
type Optional,
|
||||
} from '@/shared';
|
||||
import { HttpStatusCode } from 'axios';
|
||||
|
||||
export const PBX_PROVIDER_TYPE_QUERY_PARAM = 'pbxProviderType';
|
||||
|
||||
export const routes = {
|
||||
root: '/',
|
||||
login: '/login',
|
||||
sectionBase(entityTypeId: number) {
|
||||
return `/et/${entityTypeId}`;
|
||||
},
|
||||
everything(entityTypeId: number) {
|
||||
return `${this.sectionBase(entityTypeId)}/everything`;
|
||||
},
|
||||
listSectionBase(entityTypeId: number) {
|
||||
return `${this.sectionBase(entityTypeId)}/list`;
|
||||
},
|
||||
listSectionAutomation(entityTypeId: number) {
|
||||
return `${this.sectionBase(entityTypeId)}/list/${EntitiesBoardSettingsTab.AUTOMATION}`;
|
||||
},
|
||||
listSectionBpmn(entityTypeId: number) {
|
||||
return `${this.sectionBase(entityTypeId)}/list/${EntitiesBoardSettingsTab.AUTOMATION_BPMN}`;
|
||||
},
|
||||
listSection(entityTypeId: number) {
|
||||
return `${this.listSectionBase(entityTypeId)}?${CommonQueryParams.PAGE}=1`;
|
||||
},
|
||||
listSectionWithBoard({ entityTypeId, boardId }: { entityTypeId: number; boardId: number }) {
|
||||
return `${this.sectionBase(entityTypeId)}/b/${boardId}/${SectionView.LIST}?${CommonQueryParams.PAGE}=1`;
|
||||
},
|
||||
entitiesSectionBase({ entityTypeId, boardId }: { entityTypeId: number; boardId?: number }) {
|
||||
return `${this.sectionBase(entityTypeId)}/b/${boardId ?? -1}`;
|
||||
},
|
||||
entitiesSection({
|
||||
entityTypeId,
|
||||
boardId,
|
||||
tab,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
boardId?: number;
|
||||
tab: SectionView;
|
||||
}) {
|
||||
return `${this.entitiesSectionBase({ entityTypeId, boardId })}/${tab}${tab === SectionView.LIST ? `?${CommonQueryParams.PAGE}=1` : ''}`;
|
||||
},
|
||||
entitiesSectionTimeline({
|
||||
entityTypeId,
|
||||
boardId,
|
||||
view,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
boardId: number;
|
||||
view: GanttView;
|
||||
}) {
|
||||
return `${this.entitiesSectionBase({ entityTypeId, boardId })}/${SectionView.TIMELINE}/${view}`;
|
||||
},
|
||||
boardSectionBase(entityTypeId: number) {
|
||||
return `${this.sectionBase(entityTypeId)}/b`;
|
||||
},
|
||||
// if boardId is not provided, user will be navigated to page with boardId = -1
|
||||
// Then, page will request available boards and renavigate user to actual board
|
||||
boardSection({ entityTypeId, boardId }: { entityTypeId: number; boardId?: Nullable<number> }) {
|
||||
return `${this.boardSectionBase(entityTypeId)}/${boardId ?? -1}/${SectionView.BOARD}`;
|
||||
},
|
||||
boardSectionReports({
|
||||
boardId,
|
||||
entityTypeId,
|
||||
reportsSection,
|
||||
}: {
|
||||
boardId: number;
|
||||
entityTypeId: number;
|
||||
reportsSection: ReportsSection | string;
|
||||
}) {
|
||||
return `${this.boardSectionBase(entityTypeId)}/${boardId}/${SectionView.REPORTS}${reportsSection ? `?${CommonQueryParams.SECTION}=${reportsSection}` : ''}`;
|
||||
},
|
||||
boardSectionDashboard({ entityTypeId, boardId }: { entityTypeId: number; boardId: number }) {
|
||||
return `${this.boardSectionBase(entityTypeId)}/${boardId}/${SectionView.DASHBOARD}`;
|
||||
},
|
||||
goalSettings(entityTypeId: number) {
|
||||
return `${this.sectionBase(entityTypeId)}/goal-settings`;
|
||||
},
|
||||
section({
|
||||
entityType,
|
||||
firstBoardId,
|
||||
}: {
|
||||
entityType: EntityType;
|
||||
firstBoardId: Optional<Nullable<number>>;
|
||||
}) {
|
||||
if (entityType.section.view === SectionView.BOARD)
|
||||
return this.boardSection({ entityTypeId: entityType.id, boardId: firstBoardId });
|
||||
|
||||
return this.listSection(entityType.id);
|
||||
},
|
||||
cardBase(entityTypeId: number) {
|
||||
return `${this.sectionBase(entityTypeId)}/card`;
|
||||
},
|
||||
card({
|
||||
entityTypeId,
|
||||
entityId,
|
||||
from,
|
||||
tab = CardTab.OVERVIEW,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
entityId: number;
|
||||
from?: string;
|
||||
tab?: CardTab;
|
||||
}) {
|
||||
return `${this.cardBase(entityTypeId)}/${entityId}/${tab}${from ? `?${CommonQueryParams.FROM}=${from}` : ''}`;
|
||||
},
|
||||
cardProductsOrder({
|
||||
entityTypeId,
|
||||
entityId,
|
||||
sectionId,
|
||||
sectionType,
|
||||
orderId,
|
||||
from,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
entityId: number;
|
||||
sectionId: number;
|
||||
sectionType: ProductsSectionType;
|
||||
orderId?: number;
|
||||
from?: string;
|
||||
}) {
|
||||
return (
|
||||
this.cardBase(entityTypeId) +
|
||||
`/${entityId}/${generateProductsSectionOrderTabValue({
|
||||
sectionId,
|
||||
sectionType,
|
||||
})}${from ? `?${CommonQueryParams.FROM}=${from}` : ''}${orderId ? `${from ? '&' : '?'}${ORDER_ID_QUERY_PARAM}=${orderId}` : ''}`
|
||||
);
|
||||
},
|
||||
cardAfterAdd({
|
||||
entityTypeId,
|
||||
entityId,
|
||||
from,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
entityId: number;
|
||||
from?: string;
|
||||
}) {
|
||||
return `${this.cardBase(entityTypeId)}/${entityId}/${CardTab.OVERVIEW}?${CardTab.AFTER_ADD}=true${
|
||||
from ? `&${CommonQueryParams.FROM}=${from}` : ''
|
||||
}`;
|
||||
},
|
||||
addCard({
|
||||
entityTypeId,
|
||||
boardId,
|
||||
from,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
boardId?: number;
|
||||
from?: string;
|
||||
}) {
|
||||
return `${this.cardBase(entityTypeId)}/add${boardId ? `?boardId=${boardId}` : ''}${from ? `${boardId ? '&' : '?'}${CommonQueryParams.FROM}=${from}` : ''}`;
|
||||
},
|
||||
productsBase({
|
||||
sectionType,
|
||||
sectionId,
|
||||
}: {
|
||||
sectionType: ProductsSectionType;
|
||||
sectionId: number;
|
||||
}) {
|
||||
return `/p/${sectionType}/${sectionId}`;
|
||||
},
|
||||
products({
|
||||
sectionId,
|
||||
sectionType,
|
||||
addProduct,
|
||||
sku,
|
||||
tab = ProductsPageTabs.PRODUCTS,
|
||||
}: {
|
||||
sectionId: number;
|
||||
sectionType: ProductsSectionType;
|
||||
addProduct?: boolean;
|
||||
sku?: string;
|
||||
tab?: ProductsPageTabs;
|
||||
}) {
|
||||
let path;
|
||||
|
||||
if (addProduct && sku) {
|
||||
path = `${this.productsBase({ sectionType, sectionId })}/${tab}?add=true&sku=${sku}`;
|
||||
} else if (addProduct) {
|
||||
path = `${this.productsBase({ sectionType, sectionId })}/${tab}?add=true`;
|
||||
} else {
|
||||
path = `${this.productsBase({ sectionType, sectionId })}/${tab}`;
|
||||
}
|
||||
|
||||
return `${path}${tab === ProductsPageTabs.PRODUCTS ? `?${CommonQueryParams.PAGE}=1` : ''}`;
|
||||
},
|
||||
productsReports({
|
||||
sectionId,
|
||||
sectionType,
|
||||
reportsSection,
|
||||
}: {
|
||||
sectionId: number;
|
||||
sectionType: ProductsSectionType;
|
||||
reportsSection: string;
|
||||
}) {
|
||||
return `${this.productsBase({ sectionType, sectionId })}/${ProductsPageTabs.REPORTS}?${CommonQueryParams.SECTION}=${reportsSection}`;
|
||||
},
|
||||
product({
|
||||
sectionId,
|
||||
sectionType,
|
||||
productId,
|
||||
from,
|
||||
}: {
|
||||
sectionId: number;
|
||||
sectionType: ProductsSectionType;
|
||||
productId: number;
|
||||
from?: string;
|
||||
}) {
|
||||
if (from)
|
||||
return `${this.productsBase({ sectionType, sectionId })}/product/${productId}?${CommonQueryParams.FROM}=${from}`;
|
||||
|
||||
return `${this.productsBase({ sectionType, sectionId })}/product/${productId}`;
|
||||
},
|
||||
shipments({ sectionId, sectionType }: { sectionId: number; sectionType: ProductsSectionType }) {
|
||||
return `${this.productsBase({ sectionType, sectionId })}/shipments?${CommonQueryParams.PAGE}=1`;
|
||||
},
|
||||
shipment({
|
||||
sectionId,
|
||||
sectionType,
|
||||
shipmentId,
|
||||
}: {
|
||||
sectionId: number;
|
||||
sectionType: ProductsSectionType;
|
||||
shipmentId: number;
|
||||
}) {
|
||||
return `${this.productsBase({ sectionType, sectionId })}/shipments/${shipmentId}`;
|
||||
},
|
||||
settingsBase: '/settings',
|
||||
settingsUsersBase() {
|
||||
return `${this.settingsBase}/users`;
|
||||
},
|
||||
settingsUsers() {
|
||||
return `${this.settingsUsersBase()}/list`;
|
||||
},
|
||||
settingsDepartments() {
|
||||
return `${this.settingsUsersBase()}/groups`;
|
||||
},
|
||||
settingsUsersAdd() {
|
||||
return `${this.settingsUsersBase()}/list/new`;
|
||||
},
|
||||
settingsUsersUpdate(userId: number) {
|
||||
return `${this.settingsUsersBase()}/list/${userId}`;
|
||||
},
|
||||
settingsGeneral() {
|
||||
return `${this.settingsBase}/general`;
|
||||
},
|
||||
settingsApiKeys() {
|
||||
return `${this.settingsBase}/api`;
|
||||
},
|
||||
settingsBillingBase() {
|
||||
return `${this.settingsBase}/billing`;
|
||||
},
|
||||
settingsBillingCommon() {
|
||||
return `${this.settingsBillingBase()}/common`;
|
||||
},
|
||||
settingsBillingStripe() {
|
||||
return `${this.settingsBillingBase()}/stripe`;
|
||||
},
|
||||
settingsBillingMyworkRequestInvoiceBase() {
|
||||
return `${this.settingsBillingBase()}/invoice`;
|
||||
},
|
||||
settingsBillingMyworkRequestInvoice({ plan, users }: { plan: string; users: number }) {
|
||||
return `${this.settingsBillingMyworkRequestInvoiceBase()}?plan=${plan}&users=${users}`;
|
||||
},
|
||||
settingsIntegrations() {
|
||||
return `${this.settingsBase}/integrations`;
|
||||
},
|
||||
settingsIntegrationsGoogleCalendarConnect({ code, state }: { code: string; state?: string }) {
|
||||
return `${this.settingsBase}/integrations?${GOOGLE_CALENDAR_CONNECT_MODAL_QUERY_PARAM}=true&code=${code}${state ? `&state=${state}` : ''}`;
|
||||
},
|
||||
settingsIntegrationsGoogleCalendarManage() {
|
||||
return `${this.settingsIntegrations()}?${GOOGLE_CALENDAR_MANAGE_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsWazzupInfo() {
|
||||
return `${this.settingsIntegrations()}?${WAZZUP_FIRST_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsPbxGroup() {
|
||||
return `${this.settingsIntegrations()}#${PBX_GROUP_ID}`;
|
||||
},
|
||||
settingsIntegrationsTildaInfo() {
|
||||
return `${this.settingsIntegrations()}?${TILDA_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsWordpressInfo() {
|
||||
return `${this.settingsIntegrations()}?${WORDPRESS_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsTwilioInfo() {
|
||||
return `${this.settingsIntegrations()}?${TWILIO_FIRST_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsFbMessengerInfo() {
|
||||
return `${this.settingsIntegrations()}?${FB_FIRST_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsSalesforceInfo() {
|
||||
return `${this.settingsIntegrations()}?${SALESFORCE_FIRST_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsMakeInfo() {
|
||||
return `${this.settingsIntegrations()}?${MAKE_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsApixDriveInfo() {
|
||||
return `${this.settingsIntegrations()}?${APIX_DRIVE_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsAlbatoInfo() {
|
||||
return `${this.settingsIntegrations()}?${ALBATO_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsIntegrationsOneCInfo() {
|
||||
return `${this.settingsIntegrations()}?${ONE_C_INFO_MODAL_QUERY_PARAM}=true`;
|
||||
},
|
||||
settingsMailing() {
|
||||
return `${this.settingsBase}/mailing`;
|
||||
},
|
||||
settingsMailingEditMailbox(mailboxId: number) {
|
||||
return `${this.settingsMailing()}?${MAILBOX_ID_QUERY_PARAM}=${mailboxId}`;
|
||||
},
|
||||
settingsDocumentsBase() {
|
||||
return `${this.settingsBase}/documents`;
|
||||
},
|
||||
settingsDocumentTemplates() {
|
||||
return `${this.settingsDocumentsBase()}/templates`;
|
||||
},
|
||||
settingsDocumentCreationFields() {
|
||||
return `${this.settingsDocumentsBase()}/fields`;
|
||||
},
|
||||
settingsMailingAddMailbox() {
|
||||
return `${this.settingsBase}/mailing?add=true`;
|
||||
},
|
||||
settingsCallsBase() {
|
||||
return `${this.settingsBase}/calls`;
|
||||
},
|
||||
settingsCallsAccount() {
|
||||
return `${this.settingsCallsBase()}/account`;
|
||||
},
|
||||
settingsCallsUsers() {
|
||||
return `${this.settingsCallsBase()}/users`;
|
||||
},
|
||||
settingsCallsScenarios() {
|
||||
return `${this.settingsCallsBase()}/scenarios`;
|
||||
},
|
||||
settingsCallsSchemas() {
|
||||
return `${this.settingsCallsBase()}/schemas`;
|
||||
},
|
||||
settingsSuperadmin() {
|
||||
return `${this.settingsBase}/superadmin`;
|
||||
},
|
||||
settingsCallsSipRegistrations({
|
||||
add,
|
||||
pbxProviderType,
|
||||
}: {
|
||||
add?: boolean;
|
||||
pbxProviderType?: string;
|
||||
}) {
|
||||
return `${this.settingsCallsBase()}/sip-registrations${add ? `?${CommonQueryParams.ADD}=true` : ''}${
|
||||
pbxProviderType ? `${add ? '&' : '?'}${PBX_PROVIDER_TYPE_QUERY_PARAM}=${pbxProviderType}` : ''
|
||||
}`;
|
||||
},
|
||||
entityTypeBoardSettings({
|
||||
boardId,
|
||||
entityTypeId,
|
||||
tab = EntitiesBoardSettingsTab.AUTOMATION,
|
||||
from,
|
||||
}: {
|
||||
boardId: number;
|
||||
entityTypeId: number;
|
||||
tab?: EntitiesBoardSettingsTab;
|
||||
from?: string;
|
||||
}) {
|
||||
return `${this.sectionBase(entityTypeId)}/b/${boardId}/board-settings/${tab}${from ? `?${CommonQueryParams.FROM}=${from}` : ''}`;
|
||||
},
|
||||
activitiesBase: '/activities',
|
||||
activities: `/activities/${TasksTab.BOARD}`,
|
||||
activitiesCalendar({
|
||||
view,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
}: {
|
||||
view: CalendarView;
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
}) {
|
||||
return `/activities/${TasksTab.CALENDAR}/${view}/${year}/${month}/${day}`;
|
||||
},
|
||||
tasksBase: '/tasks',
|
||||
timeBoardBase() {
|
||||
return `${this.tasksBase}/deadline`;
|
||||
},
|
||||
timeBoard() {
|
||||
return `${this.tasksBase}/deadline/${TasksTab.BOARD}`;
|
||||
},
|
||||
timeBoardCalendar({
|
||||
view,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
}: {
|
||||
view: CalendarView;
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
}) {
|
||||
return `${this.tasksBase}/deadline/${TasksTab.CALENDAR}/${view}/${year}/${month}/${day}`;
|
||||
},
|
||||
tasksBoardBase(boardId: number) {
|
||||
return `${this.tasksBase}/b/${boardId}`;
|
||||
},
|
||||
tasksBoard(boardId: number) {
|
||||
return `${this.tasksBoardBase(boardId)}/${TasksTab.BOARD}`;
|
||||
},
|
||||
taskBoardSettings({
|
||||
boardId,
|
||||
from,
|
||||
entityId,
|
||||
entityTypeId,
|
||||
}: {
|
||||
boardId: number;
|
||||
from?: string;
|
||||
entityId?: number;
|
||||
entityTypeId?: number;
|
||||
}) {
|
||||
return `${this.tasksBoardBase(boardId)}/board-settings${from ? `?${CommonQueryParams.FROM}=${from}` : ''}${
|
||||
entityId && entityTypeId
|
||||
? `${from ? '&' : '?'}entityTypeId=${entityTypeId}&entityId=${entityId}`
|
||||
: ''
|
||||
}`;
|
||||
},
|
||||
tasksList(boardId: number) {
|
||||
return `${this.tasksBoardBase(boardId)}/${TasksTab.LIST}`;
|
||||
},
|
||||
tasksTimeline({ boardId, view }: { boardId: number; view: GanttView }) {
|
||||
return `${this.tasksBoardBase(boardId)}/${TasksTab.TIMELINE}/${view}`;
|
||||
},
|
||||
projectTasksBoard({
|
||||
entityTypeId,
|
||||
entityId,
|
||||
from,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
entityId: number;
|
||||
from?: string;
|
||||
}) {
|
||||
return (
|
||||
this.cardBase(entityTypeId) +
|
||||
`/${entityId}/${CardTab.BOARD}${from ? `?${CommonQueryParams.FROM}=${from}` : ''}`
|
||||
);
|
||||
},
|
||||
tasksCalendar({
|
||||
boardId,
|
||||
view,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
}: {
|
||||
boardId: number;
|
||||
view: CalendarView;
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
}) {
|
||||
return `${this.tasksBoardBase(boardId)}/${TasksTab.CALENDAR}/${view}/${year}/${month}/${day}`;
|
||||
},
|
||||
projectTasksCalendar({
|
||||
entityTypeId,
|
||||
entityId,
|
||||
view,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
from,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
entityId: number;
|
||||
view: CalendarView;
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
from?: string;
|
||||
}) {
|
||||
return (
|
||||
this.cardBase(entityTypeId) +
|
||||
`/${entityId}/${CardTab.CALENDAR}/${view}/${year}/${month}/${day}${from ? `?${CommonQueryParams.FROM}=${from}` : ''}`
|
||||
);
|
||||
},
|
||||
projectTasksList({
|
||||
entityTypeId,
|
||||
entityId,
|
||||
from,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
entityId: number;
|
||||
from?: string;
|
||||
}) {
|
||||
return (
|
||||
this.cardBase(entityTypeId) +
|
||||
`/${entityId}/${CardTab.LIST}${from ? `?${CommonQueryParams.FROM}=${from}` : ''}`
|
||||
);
|
||||
},
|
||||
projectTasksTimeline({
|
||||
entityTypeId,
|
||||
entityId,
|
||||
from,
|
||||
view,
|
||||
}: {
|
||||
entityTypeId: number;
|
||||
entityId: number;
|
||||
view: GanttView;
|
||||
from?: string;
|
||||
}) {
|
||||
return `${this.cardBase(entityTypeId)}/${entityId}/${CardTab.TIMELINE}/${view}${from ? `?${CommonQueryParams.FROM}=${from}` : ''}`;
|
||||
},
|
||||
partnerInfo(partnerId: number) {
|
||||
return `/partners/${partnerId}`;
|
||||
},
|
||||
mail: '/mail',
|
||||
forbiddenPage: `/${HttpStatusCode.Forbidden}`,
|
||||
multichatBase: '/chat',
|
||||
multichat(from?: string) {
|
||||
return `${this.multichatBase}${from ? `?${CommonQueryParams.FROM}=${from}` : ''}`;
|
||||
},
|
||||
multichatChat(chatId: number) {
|
||||
return `${this.multichatBase}/${chatId}`;
|
||||
},
|
||||
multichatChatMessage({ chatId, messageId }: { chatId: number; messageId: number }) {
|
||||
return `${this.multichatBase}/${chatId}/message/${messageId}`;
|
||||
},
|
||||
builderBase: '/builder',
|
||||
builder(tab: BuilderTabs) {
|
||||
return `${this.builderBase}/${tab}`;
|
||||
},
|
||||
builderCreateSiteForm() {
|
||||
return `${this.builderBase}/site-forms`;
|
||||
},
|
||||
builderUpdateSiteForm(siteFormId: number) {
|
||||
return `${this.builderCreateSiteForm()}/${siteFormId}`;
|
||||
},
|
||||
builderCreateHeadlessSiteForm() {
|
||||
return `${this.builderBase}/site-forms/headless`;
|
||||
},
|
||||
builderUpdateHeadlessSiteForm(siteFormId: number) {
|
||||
return `${this.builderCreateHeadlessSiteForm()}/${siteFormId}`;
|
||||
},
|
||||
builderCreateOnlineBookingSiteForm() {
|
||||
return `${this.builderBase}/site-forms/online-booking`;
|
||||
},
|
||||
builderUpdateOnlineBookingSiteForm(siteFormId: number) {
|
||||
return `${this.builderCreateOnlineBookingSiteForm()}/${siteFormId}`;
|
||||
},
|
||||
builderCreateProductsSection(type: ProductsSectionType) {
|
||||
return `${this.builderBase}/products?type=${type}`;
|
||||
},
|
||||
builderUpdateProductsSection({
|
||||
moduleId,
|
||||
moduleType,
|
||||
}: {
|
||||
moduleId: number;
|
||||
moduleType: ProductsSectionType;
|
||||
}) {
|
||||
return `${this.builderBase}/products/${moduleId}?type=${moduleType}`;
|
||||
},
|
||||
builderCreateEt(module: ModuleCategory) {
|
||||
return `${this.builderBase}/et?${CommonQueryParams.CATEGORY}=${module}`;
|
||||
},
|
||||
builderUpdateEt(moduleId: number) {
|
||||
return `${this.builderBase}/et/${moduleId}`;
|
||||
},
|
||||
builderCreateScheduler() {
|
||||
return `${this.builderBase}/scheduler`;
|
||||
},
|
||||
builderUpdateScheduler(moduleId: number) {
|
||||
return `${this.builderBase}/scheduler/${moduleId}`;
|
||||
},
|
||||
schedulerBase({ scheduleType, scheduleId }: { scheduleType: ScheduleType; scheduleId: number }) {
|
||||
return `/scheduler/${scheduleType}/${scheduleId}`;
|
||||
},
|
||||
scheduler({
|
||||
scheduleId,
|
||||
scheduleType,
|
||||
tab,
|
||||
}: {
|
||||
scheduleId: number;
|
||||
scheduleType: ScheduleType;
|
||||
tab: SectionView;
|
||||
}) {
|
||||
return `${this.schedulerBase({ scheduleId, scheduleType })}/${tab}`;
|
||||
},
|
||||
schedulerReports({
|
||||
scheduleId,
|
||||
scheduleType,
|
||||
reportsSection,
|
||||
}: {
|
||||
scheduleId: number;
|
||||
scheduleType: ScheduleType;
|
||||
reportsSection: ReportsSection;
|
||||
}) {
|
||||
return `${this.schedulerBase({ scheduleId, scheduleType })}/${SectionView.REPORTS}?${CommonQueryParams.SECTION}=${reportsSection}`;
|
||||
},
|
||||
schedulerClients({
|
||||
scheduleId,
|
||||
scheduleType,
|
||||
entityTypeId,
|
||||
}: {
|
||||
scheduleId: number;
|
||||
scheduleType: ScheduleType;
|
||||
entityTypeId: number;
|
||||
}) {
|
||||
return `${this.schedulerBase({ scheduleId, scheduleType })}/${generateSchedulerLinkedEntityTypeTab(entityTypeId)}`;
|
||||
},
|
||||
notes: '/notes',
|
||||
notFoundPage: `/${HttpStatusCode.NotFound}`,
|
||||
};
|
||||
89
frontend/src/app/store/AppStore.ts
Normal file
89
frontend/src/app/store/AppStore.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { watchdogStore } from '@/app';
|
||||
import { authStore } from '@/modules/auth';
|
||||
import { mailboxSettingsStore } from '@/modules/mailing';
|
||||
import { notificationsStore, toastNotificationsStore } from '@/modules/notifications';
|
||||
import { productsModuleStore } from '@/modules/products';
|
||||
import { schedulerEventHandlerStore } from '@/modules/scheduler';
|
||||
import { departmentsSettingsStore } from '@/modules/settings';
|
||||
import { activityTypeStore, taskSettingsStore } from '@/modules/tasks';
|
||||
import { voximplantConnectorStore } from '@/modules/telephony';
|
||||
import type { DataStore, SubscriberStore } from '@/shared';
|
||||
import { flow, makeAutoObservable, runInAction, when } from 'mobx';
|
||||
import { entityTypeStore } from './EntityTypeStore';
|
||||
import { featureStore } from './FeatureStore';
|
||||
import { generalSettingsStore } from './GeneralSettingsStore';
|
||||
import { identityStore } from './IdentityStore';
|
||||
import { subscriptionStore } from './SubscriptionStore';
|
||||
import { userStore } from './UserStore';
|
||||
|
||||
class AppStore {
|
||||
isLoaded = false;
|
||||
|
||||
private _dataStores: DataStore[] = [
|
||||
userStore,
|
||||
identityStore,
|
||||
featureStore,
|
||||
entityTypeStore,
|
||||
subscriptionStore,
|
||||
activityTypeStore,
|
||||
taskSettingsStore,
|
||||
productsModuleStore,
|
||||
generalSettingsStore,
|
||||
departmentsSettingsStore,
|
||||
notificationsStore,
|
||||
mailboxSettingsStore,
|
||||
];
|
||||
|
||||
private _subscriberStores: SubscriberStore[] = [
|
||||
notificationsStore,
|
||||
toastNotificationsStore,
|
||||
schedulerEventHandlerStore,
|
||||
];
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
load = flow(function* (this: AppStore) {
|
||||
try {
|
||||
// wait for auth
|
||||
yield when(() => authStore.isAuthenticated);
|
||||
|
||||
// we assume that this data is important but not mandatory for app functionality,
|
||||
// so some promises could possibly reject
|
||||
Promise.allSettled([voximplantConnectorStore.loadData()]);
|
||||
|
||||
// load global stores, we assume that this data is essential for app functionality
|
||||
yield Promise.all(this._dataStores.map(ds => ds.loadData()));
|
||||
|
||||
// subscribe to global events
|
||||
yield Promise.all(this._subscriberStores.map(ss => ss.subscribe()));
|
||||
|
||||
this._dataStores.forEach(ds => watchdogStore.watch(ds));
|
||||
} catch (e) {
|
||||
throw new Error(`Error loading app: ${e}`);
|
||||
} finally {
|
||||
this.isLoaded = true;
|
||||
}
|
||||
});
|
||||
|
||||
reset = (): void => {
|
||||
// clear all global stores
|
||||
runInAction(() => {
|
||||
voximplantConnectorStore.reset();
|
||||
|
||||
this._dataStores.forEach(ds => ds.reset());
|
||||
});
|
||||
|
||||
// unsubscribe from global events
|
||||
runInAction(() => {
|
||||
this._subscriberStores.forEach(ss => ss.unsubscribe());
|
||||
});
|
||||
|
||||
watchdogStore.reset();
|
||||
|
||||
this.isLoaded = false;
|
||||
};
|
||||
}
|
||||
|
||||
export const appStore = new AppStore();
|
||||
185
frontend/src/app/store/EntityTypeStore.ts
Normal file
185
frontend/src/app/store/EntityTypeStore.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { authStore } from '@/modules/auth';
|
||||
import {
|
||||
EntityCategory,
|
||||
PermissionLevel,
|
||||
PermissionObjectType,
|
||||
type DataStore,
|
||||
type EntityType,
|
||||
type Nullable,
|
||||
type Option,
|
||||
} from '@/shared';
|
||||
import { computed, makeAutoObservable, toJS } from 'mobx';
|
||||
import { entityTypeApi, type UpdateEntityTypeDto, type UpdateEntityTypeFieldsModel } from '../api';
|
||||
|
||||
export class EntityTypeStore implements DataStore {
|
||||
entityTypes: EntityType[] = [];
|
||||
|
||||
isLoaded = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get entityTypesOptions(): Option<number>[] {
|
||||
return this.sortedEntityTypes.map<Option<number>>(et => ({
|
||||
value: et.id,
|
||||
label: et.section.name,
|
||||
}));
|
||||
}
|
||||
|
||||
get contacts(): EntityType[] {
|
||||
return this.getByCategory(EntityCategory.CONTACT);
|
||||
}
|
||||
|
||||
get contactsOptions(): Option<number>[] {
|
||||
return this.contacts.map<Option<number>>(et => ({ value: et.id, label: et.name }));
|
||||
}
|
||||
|
||||
get companies(): EntityType[] {
|
||||
return this.getByCategory(EntityCategory.COMPANY);
|
||||
}
|
||||
|
||||
get companiesOptions(): Option<number>[] {
|
||||
return this.companies.map<Option<number>>(et => ({ value: et.id, label: et.name }));
|
||||
}
|
||||
|
||||
get contactsAndCompaniesOptions(): Option<number>[] {
|
||||
return [...this.contactsOptions, ...this.companiesOptions];
|
||||
}
|
||||
|
||||
get firstDealEntityTypeId(): Nullable<number> {
|
||||
return this.sortedEntityTypes.find(et => et.entityCategory === EntityCategory.DEAL)?.id ?? null;
|
||||
}
|
||||
|
||||
get entityTypesExceptContactAndCompanies(): EntityType[] {
|
||||
return this.sortedEntityTypes.filter(
|
||||
et => ![EntityCategory.CONTACT, EntityCategory.COMPANY].includes(et.entityCategory)
|
||||
);
|
||||
}
|
||||
|
||||
get entityTypesExceptContactAndCompaniesOptions(): Option<number>[] {
|
||||
return this.entityTypesExceptContactAndCompanies.map<Option<number>>(et => ({
|
||||
value: et.id,
|
||||
label: et.name,
|
||||
}));
|
||||
}
|
||||
|
||||
get firstEntityTypeId(): number {
|
||||
const firstEntityTypeId = this.sortedEntityTypes[0]?.id;
|
||||
|
||||
if (!firstEntityTypeId)
|
||||
throw new Error('First entity type was not found, no entity types exist');
|
||||
|
||||
return firstEntityTypeId;
|
||||
}
|
||||
|
||||
@computed.struct
|
||||
get sortedEntityTypes(): EntityType[] {
|
||||
return toJS(this.entityTypes).sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
}
|
||||
|
||||
deleteEntityType = async (entityTypeId: number): Promise<void> => {
|
||||
await entityTypeApi.deleteEntityType(entityTypeId);
|
||||
|
||||
this.entityTypes = this.entityTypes.filter(et => et.id !== entityTypeId);
|
||||
};
|
||||
|
||||
updateEntityType = async (entityType: UpdateEntityTypeDto): Promise<EntityType> => {
|
||||
const updatedEntityType = await entityTypeApi.updateEntityType(entityType);
|
||||
|
||||
this.entityTypes = this.entityTypes.map(et =>
|
||||
et.id === entityType.id ? updatedEntityType : et
|
||||
);
|
||||
|
||||
return updatedEntityType;
|
||||
};
|
||||
|
||||
updateEntityTypeFields = async (model: UpdateEntityTypeFieldsModel): Promise<EntityType> => {
|
||||
const updated = await entityTypeApi.updateEntityTypeFields(model);
|
||||
const idx = this.entityTypes.findIndex(et => et.id === model.entityTypeId);
|
||||
|
||||
this.entityTypes.splice(idx, 1, updated);
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
getById = (id: number): EntityType => {
|
||||
const entityType = this.entityTypes.find(et => et.id === id);
|
||||
|
||||
if (!entityType) throw new Error(`Entity type with id ${id} was not found`);
|
||||
|
||||
return entityType;
|
||||
};
|
||||
|
||||
getByIdAsync = async (id: number): Promise<EntityType> => {
|
||||
try {
|
||||
const entityType = await entityTypeApi.getEntityType(id);
|
||||
|
||||
// invalidate cache
|
||||
this.entityTypes = this.entityTypes.map(et => (et.id === id ? entityType : et));
|
||||
|
||||
return entityType;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to get entity type ${id}: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
loadData = async (): Promise<void> => {
|
||||
try {
|
||||
this.entityTypes = await entityTypeApi.getEntityTypes();
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to load entity types: ${e}`);
|
||||
} finally {
|
||||
this.isLoaded = true;
|
||||
}
|
||||
};
|
||||
|
||||
invalidateEntityTypesInCache = async (): Promise<void> => {
|
||||
try {
|
||||
this.entityTypes = await entityTypeApi.getEntityTypes();
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to invalidate entity types cache: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
getByCategory = (entityCategory: EntityCategory): EntityType[] => {
|
||||
return this.sortedEntityTypes.filter(et => et.entityCategory === entityCategory);
|
||||
};
|
||||
|
||||
getAvailableEntityTypes = (): EntityType[] => {
|
||||
const { user: currentUser } = authStore;
|
||||
|
||||
if (!currentUser) throw new Error('User is not logged in');
|
||||
|
||||
const entityTypes = this.sortedEntityTypes;
|
||||
const objectPermissions = currentUser.objectPermissions;
|
||||
|
||||
return authStore.user?.isAdmin()
|
||||
? entityTypes
|
||||
: entityTypes.filter(et => {
|
||||
const permission = objectPermissions.find(
|
||||
op => op.objectType === PermissionObjectType.ENTITY_TYPE && op.objectId === et.id
|
||||
);
|
||||
|
||||
return permission ? permission.viewPermission !== PermissionLevel.DENIED : true;
|
||||
});
|
||||
};
|
||||
|
||||
getLinkedDealsOptions = (etId: number): Option<number>[] => {
|
||||
return this.getById(etId).linkedEntityTypes.flatMap<Option<number>>(l => {
|
||||
const linkedEt = entityTypeStore.getById(l.targetId);
|
||||
|
||||
return linkedEt.entityCategory === EntityCategory.DEAL
|
||||
? [{ label: linkedEt.name, value: linkedEt.id }]
|
||||
: [];
|
||||
});
|
||||
};
|
||||
|
||||
reset = (): void => {
|
||||
this.entityTypes = [];
|
||||
|
||||
this.isLoaded = false;
|
||||
};
|
||||
}
|
||||
|
||||
export const entityTypeStore = new EntityTypeStore();
|
||||
25
frontend/src/app/store/FeatureStore.ts
Normal file
25
frontend/src/app/store/FeatureStore.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { DataStore } from '@/shared';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import { featureApi, type FeatureDto } from '../api';
|
||||
|
||||
class FeatureStore implements DataStore {
|
||||
features: FeatureDto[] = [];
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
loadData = async (): Promise<void> => {
|
||||
try {
|
||||
this.features = await featureApi.getFeatures();
|
||||
} catch (e) {
|
||||
throw new Error(`Error while loading features: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
reset = (): void => {
|
||||
this.features = [];
|
||||
};
|
||||
}
|
||||
|
||||
export const featureStore = new FeatureStore();
|
||||
46
frontend/src/app/store/GeneralSettingsFormData.ts
Normal file
46
frontend/src/app/store/GeneralSettingsFormData.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Account, AccountSettings } from '@/modules/settings';
|
||||
import { BooleanModel, InputModel, MultiselectModel, SelectModel, WeekDays } from '@/shared';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
export class GeneralSettingsFormData {
|
||||
companyName: InputModel;
|
||||
subdomain: InputModel;
|
||||
language: SelectModel;
|
||||
workingDays: MultiselectModel<WeekDays>;
|
||||
startOfWeek: SelectModel;
|
||||
workingTimeFrom: InputModel;
|
||||
workingTimeTo: InputModel;
|
||||
timeZone: SelectModel;
|
||||
currency: SelectModel;
|
||||
numberFormat: SelectModel;
|
||||
phoneFormat: SelectModel;
|
||||
dateFormat: SelectModel;
|
||||
allowDuplicates: BooleanModel;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
initializeFormData = ({
|
||||
account,
|
||||
accountSettings,
|
||||
}: {
|
||||
account: Account;
|
||||
accountSettings: AccountSettings;
|
||||
}) => {
|
||||
this.companyName = InputModel.create(account.companyName);
|
||||
this.subdomain = InputModel.create(account.subdomain);
|
||||
|
||||
this.language = SelectModel.create(accountSettings.language).required();
|
||||
this.workingDays = MultiselectModel.createFromNullable<WeekDays>(accountSettings.workingDays);
|
||||
this.startOfWeek = SelectModel.create(accountSettings.startOfWeek ?? WeekDays.MONDAY);
|
||||
this.workingTimeFrom = InputModel.create(accountSettings.workingTimeFrom);
|
||||
this.workingTimeTo = InputModel.create(accountSettings.workingTimeTo);
|
||||
this.timeZone = SelectModel.create(accountSettings.timeZone);
|
||||
this.currency = SelectModel.create(accountSettings.currency).required();
|
||||
this.numberFormat = SelectModel.create(accountSettings.numberFormat);
|
||||
this.phoneFormat = SelectModel.create(accountSettings.phoneFormat).required();
|
||||
this.dateFormat = SelectModel.create(accountSettings.dateFormat ?? null);
|
||||
this.allowDuplicates = BooleanModel.create(accountSettings.allowDuplicates);
|
||||
};
|
||||
}
|
||||
185
frontend/src/app/store/GeneralSettingsStore.ts
Normal file
185
frontend/src/app/store/GeneralSettingsStore.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import type { Account, AccountSettings } from '@/modules/settings';
|
||||
import {
|
||||
type DataStore,
|
||||
DayNumberMap,
|
||||
type Nullable,
|
||||
type Optional,
|
||||
UtcDate,
|
||||
type WeekDays,
|
||||
} from '@/shared';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import { generalSettingsApi, type UpdateAccountSettingsDto } from '../api';
|
||||
import { GeneralSettingsFormData } from './GeneralSettingsFormData';
|
||||
|
||||
class GeneralSettingsStore implements DataStore {
|
||||
account: Nullable<Account> = null;
|
||||
accountSettings: Nullable<AccountSettings> = null;
|
||||
generalSettingsForm: GeneralSettingsFormData = new GeneralSettingsFormData();
|
||||
|
||||
isLoaded = false;
|
||||
hasDemoData = false;
|
||||
isDemoDeleting = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get updateAccountSettingsDto(): UpdateAccountSettingsDto {
|
||||
return {
|
||||
language: this.generalSettingsForm.language.value,
|
||||
workingDays:
|
||||
this.generalSettingsForm.workingDays.values.length > 0
|
||||
? this.generalSettingsForm.workingDays.values
|
||||
: null,
|
||||
startOfWeek: this.generalSettingsForm.startOfWeek.value,
|
||||
workingTimeFrom: this.generalSettingsForm.workingTimeFrom.value,
|
||||
workingTimeTo: this.generalSettingsForm.workingTimeTo.value,
|
||||
timeZone: this.generalSettingsForm.timeZone.value,
|
||||
currency: this.generalSettingsForm.currency.value,
|
||||
numberFormat: this.generalSettingsForm.numberFormat.value,
|
||||
phoneFormat: this.generalSettingsForm.phoneFormat.value,
|
||||
dateFormat: this.generalSettingsForm.dateFormat.value,
|
||||
allowDuplicates: this.generalSettingsForm.allowDuplicates.value,
|
||||
};
|
||||
}
|
||||
|
||||
get startOfWeekAsNumber(): Optional<number> {
|
||||
if (!this.accountSettings?.startOfWeek) return undefined;
|
||||
|
||||
return DayNumberMap[this.accountSettings.startOfWeek];
|
||||
}
|
||||
|
||||
get workingDaysAsNumberArray(): Nullable<number[]> {
|
||||
if (!this.accountSettings?.workingDays) return null;
|
||||
|
||||
return this.accountSettings.workingDays.map(day => DayNumberMap[day as WeekDays]);
|
||||
}
|
||||
|
||||
get nonWorkingDaysAsNumberArray(): Nullable<number[]> {
|
||||
const accountWorkingDays = this.workingDaysAsNumberArray;
|
||||
|
||||
if (!accountWorkingDays) return null;
|
||||
|
||||
const wholeWeekAsNumberArray = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
return wholeWeekAsNumberArray.filter(d => !accountWorkingDays.includes(d));
|
||||
}
|
||||
|
||||
get accountCreationYear(): Nullable<number> {
|
||||
if (!this.account) return null;
|
||||
|
||||
return this.account.createdAt.year;
|
||||
}
|
||||
|
||||
loadAccount = async (): Promise<void> => {
|
||||
try {
|
||||
this.account = await generalSettingsApi.getAccount();
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to load account: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
loadAccountSettings = async (): Promise<void> => {
|
||||
try {
|
||||
this.accountSettings = await generalSettingsApi.getAccountSettings();
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to load account settings: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
loadDemoDataExists = async (): Promise<void> => {
|
||||
try {
|
||||
this.hasDemoData = await generalSettingsApi.getDemoDataExists();
|
||||
} catch (e) {
|
||||
this.hasDemoData = false;
|
||||
}
|
||||
};
|
||||
|
||||
loadData = async (): Promise<void> => {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadAccount(),
|
||||
this.loadAccountSettings(),
|
||||
this.loadDemoDataExists(),
|
||||
]);
|
||||
|
||||
if (this.account && this.accountSettings) {
|
||||
this.generalSettingsForm.initializeFormData({
|
||||
account: this.account,
|
||||
accountSettings: this.accountSettings,
|
||||
});
|
||||
|
||||
UtcDate.setLocale(this.accountSettings.language);
|
||||
UtcDate.setFormat(this.accountSettings.dateFormat);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to load general settings store data: ${e}`);
|
||||
} finally {
|
||||
this.isLoaded = true;
|
||||
}
|
||||
};
|
||||
|
||||
updateAccountSettings = async (): Promise<void> => {
|
||||
this.accountSettings = await generalSettingsApi.updateAccountSettings(
|
||||
this.updateAccountSettingsDto
|
||||
);
|
||||
};
|
||||
|
||||
uploadAccountLogo = async (blob: Blob): Promise<void> => {
|
||||
const formData = new FormData();
|
||||
formData.append('logo', blob, 'logo.jpg');
|
||||
|
||||
try {
|
||||
this.account = await generalSettingsApi.uploadAccountLogo(formData);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to upload account logo: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
removeAccountLogo = async (): Promise<void> => {
|
||||
try {
|
||||
this.account = await generalSettingsApi.removeAccountLogo();
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to remove account logo: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
deleteDemoData = async (): Promise<void> => {
|
||||
try {
|
||||
this.isDemoDeleting = true;
|
||||
|
||||
await generalSettingsApi.deleteDemoData();
|
||||
|
||||
this.hasDemoData = false;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to delete demo data: ${e}`);
|
||||
} finally {
|
||||
this.isDemoDeleting = false;
|
||||
}
|
||||
};
|
||||
|
||||
clearApplicationCache = async (): Promise<void> => {
|
||||
const doNotClearKeys: string[] = [
|
||||
'accountId',
|
||||
'id_pools',
|
||||
'i18nextLng',
|
||||
'gaUserId',
|
||||
'token',
|
||||
'userId',
|
||||
];
|
||||
|
||||
for (const key in localStorage) {
|
||||
if (!doNotClearKeys.includes(key)) localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
reset = (): void => {
|
||||
this.isLoaded = false;
|
||||
this.account = null;
|
||||
this.accountSettings = null;
|
||||
};
|
||||
}
|
||||
|
||||
export const generalSettingsStore = new GeneralSettingsStore();
|
||||
412
frontend/src/app/store/IconStore.tsx
Normal file
412
frontend/src/app/store/IconStore.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
import {
|
||||
BoxIcon,
|
||||
BriefcaseIcon,
|
||||
Building1Icon,
|
||||
Building2Icon,
|
||||
Building3Icon,
|
||||
BulbIcon,
|
||||
Calendar1Icon,
|
||||
Calendar2Icon,
|
||||
Calendar3Icon,
|
||||
Calendar4Icon,
|
||||
CallIcon,
|
||||
Chart1Icon,
|
||||
Chart2Icon,
|
||||
Clock1Icon,
|
||||
Clock2Icon,
|
||||
CrownIcon,
|
||||
EqualizerIcon,
|
||||
HandIcon,
|
||||
InOutIcon,
|
||||
LightningIcon,
|
||||
MailIcon,
|
||||
Product1Icon,
|
||||
Product2Icon,
|
||||
Rocket1Icon,
|
||||
Rocket2Icon,
|
||||
Rocket3Icon,
|
||||
Settings1Icon,
|
||||
Settings2Icon,
|
||||
Settings3Icon,
|
||||
Settings4Icon,
|
||||
Shapes1Icon,
|
||||
Shapes2Icon,
|
||||
Shapes3Icon,
|
||||
Shapes4Icon,
|
||||
SiteFormIcon,
|
||||
SoundIcon,
|
||||
Star1Icon,
|
||||
Star2Icon,
|
||||
Star3Icon,
|
||||
Target1Icon,
|
||||
Target2Icon,
|
||||
Tick1Icon,
|
||||
Tick2Icon,
|
||||
Tie1Icon,
|
||||
Tie2Icon,
|
||||
User1Icon,
|
||||
User2Icon,
|
||||
User3Icon,
|
||||
User4Icon,
|
||||
type Icon,
|
||||
} from '@/shared';
|
||||
import { EntityCategory } from '@/shared/lib/models/EntityType/EntityCategory';
|
||||
import { IconName } from '@/shared/lib/models/Icon/IconName';
|
||||
|
||||
interface IconWithColor {
|
||||
icon: Icon;
|
||||
color: string;
|
||||
}
|
||||
class IconStore {
|
||||
icons: Icon[] = [
|
||||
{
|
||||
name: IconName.ROCKET_1,
|
||||
icon: <Rocket1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.ROCKET_2,
|
||||
icon: <Rocket2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.ROCKET_3,
|
||||
icon: <Rocket3Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CROWN,
|
||||
icon: <CrownIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.LIGHTNING,
|
||||
icon: <LightningIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.TICK_1,
|
||||
icon: <Tick1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.TICK_2,
|
||||
icon: <Tick2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.BULB,
|
||||
icon: <BulbIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.TARGET_1,
|
||||
icon: <Target1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.TARGET_2,
|
||||
icon: <Target2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.STAR_1,
|
||||
icon: <Star1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.STAR_2,
|
||||
icon: <Star2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.STAR_3,
|
||||
icon: <Star3Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CHART_1,
|
||||
icon: <Chart1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CHART_2,
|
||||
icon: <Chart2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CLOCK_1,
|
||||
icon: <Clock1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CLOCK_2,
|
||||
icon: <Clock2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SHAPES_1,
|
||||
icon: <Shapes1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SHAPES_2,
|
||||
icon: <Shapes2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SHAPES_3,
|
||||
icon: <Shapes3Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SHAPES_4,
|
||||
icon: <Shapes4Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.USER_1,
|
||||
icon: <User1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.USER_2,
|
||||
icon: <User2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.USER_3,
|
||||
icon: <User3Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.USER_4,
|
||||
icon: <User4Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.HAND,
|
||||
icon: <HandIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.BUILDING_1,
|
||||
icon: <Building1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.BUILDING_2,
|
||||
icon: <Building2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.BUILDING_3,
|
||||
icon: <Building3Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.TIE_1,
|
||||
icon: <Tie1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.TIE_2,
|
||||
icon: <Tie2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.IN_OUT,
|
||||
icon: <InOutIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.PRODUCT_1,
|
||||
icon: <Product1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.PRODUCT_2,
|
||||
icon: <Product2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.BOX,
|
||||
icon: <BoxIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.BRIEFCASE,
|
||||
icon: <BriefcaseIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CALENDAR_1,
|
||||
icon: <Calendar1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CALENDAR_2,
|
||||
icon: <Calendar2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CALENDAR_3,
|
||||
icon: <Calendar3Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CALENDAR_4,
|
||||
icon: <Calendar4Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.EQUALIZER,
|
||||
icon: <EqualizerIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SOUND,
|
||||
icon: <SoundIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.MAIL,
|
||||
icon: <MailIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SETTINGS_1,
|
||||
icon: <Settings1Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SETTINGS_2,
|
||||
icon: <Settings2Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SETTINGS_3,
|
||||
icon: <Settings3Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SETTINGS_4,
|
||||
icon: <Settings4Icon />,
|
||||
},
|
||||
{
|
||||
name: IconName.CALL,
|
||||
icon: <CallIcon />,
|
||||
},
|
||||
{
|
||||
name: IconName.SITE_FORM,
|
||||
icon: <SiteFormIcon />,
|
||||
},
|
||||
];
|
||||
|
||||
defaultEntityIconWithColor: Map<EntityCategory, IconWithColor> = new Map([
|
||||
[
|
||||
EntityCategory.COMPANY,
|
||||
{
|
||||
icon: {
|
||||
name: IconName.BUILDING_2,
|
||||
icon: <Building2Icon />,
|
||||
},
|
||||
color: 'var(--primary-statuses-orange-440)',
|
||||
},
|
||||
],
|
||||
[
|
||||
EntityCategory.CONTACT,
|
||||
{
|
||||
icon: {
|
||||
name: IconName.USER_2,
|
||||
icon: <User2Icon />,
|
||||
},
|
||||
color: 'var(--primary-statuses-noun-440)',
|
||||
},
|
||||
],
|
||||
[
|
||||
EntityCategory.CONTRACTOR,
|
||||
{
|
||||
icon: {
|
||||
name: IconName.TIE_2,
|
||||
icon: <Tie2Icon />,
|
||||
},
|
||||
color: 'var(--primary-statuses-amethyst-360)',
|
||||
},
|
||||
],
|
||||
[
|
||||
EntityCategory.DEAL,
|
||||
{
|
||||
icon: {
|
||||
name: IconName.CROWN,
|
||||
icon: <CrownIcon />,
|
||||
},
|
||||
color: 'var(--primary-statuses-pink-360)',
|
||||
},
|
||||
],
|
||||
[
|
||||
EntityCategory.HR,
|
||||
{
|
||||
icon: {
|
||||
name: IconName.STAR_2,
|
||||
icon: <Star2Icon />,
|
||||
},
|
||||
color: 'var(--primary-statuses-purple-360)',
|
||||
},
|
||||
],
|
||||
[
|
||||
EntityCategory.PARTNER,
|
||||
{
|
||||
icon: {
|
||||
name: IconName.SHAPES_3,
|
||||
icon: <Shapes3Icon />,
|
||||
},
|
||||
color: 'var(--primary-statuses-amethyst-360)',
|
||||
},
|
||||
],
|
||||
[
|
||||
EntityCategory.PROJECT,
|
||||
{
|
||||
icon: {
|
||||
name: IconName.BULB,
|
||||
icon: <BulbIcon />,
|
||||
},
|
||||
color: 'var(--primary-statuses-blue-360)',
|
||||
},
|
||||
],
|
||||
[
|
||||
EntityCategory.SUPPLIER,
|
||||
{
|
||||
icon: {
|
||||
name: IconName.PRODUCT_1,
|
||||
icon: <Product1Icon />,
|
||||
},
|
||||
color: 'var(--primary-statuses-amethyst-360)',
|
||||
},
|
||||
],
|
||||
[
|
||||
EntityCategory.UNIVERSAL,
|
||||
{
|
||||
icon: {
|
||||
name: IconName.STAR_1,
|
||||
icon: <Star1Icon />,
|
||||
},
|
||||
color: 'var(--primary-statuses-malachite-480)',
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
get firstIcon(): Icon {
|
||||
const firstIcon = this.icons[0];
|
||||
|
||||
if (!firstIcon) throw new Error('IconStore has no icons, failed to get the first icon');
|
||||
|
||||
return firstIcon;
|
||||
}
|
||||
|
||||
getByName = (name: IconName): Icon => {
|
||||
const icon = this.icons.find(i => i.name === name);
|
||||
|
||||
if (!icon) {
|
||||
console.error(`Icon with name ${name} was not found`);
|
||||
|
||||
return this.firstIcon;
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
get defaultModuleColor(): string {
|
||||
return 'var(--primary-statuses-crimson-360)';
|
||||
}
|
||||
|
||||
get defaultSchedulerIcon(): Icon {
|
||||
const icon = this.icons.find(i => i.name === IconName.CALENDAR_4);
|
||||
|
||||
return icon ?? this.firstIcon;
|
||||
}
|
||||
|
||||
get defaultProductsIcon(): Icon {
|
||||
const icon = this.icons.find(i => i.name === IconName.BOX);
|
||||
|
||||
return icon ?? this.firstIcon;
|
||||
}
|
||||
|
||||
get schedulerColor(): string {
|
||||
return 'var(--primary-statuses-crimson-360)';
|
||||
}
|
||||
|
||||
get productsColor(): string {
|
||||
return 'var(--primary-statuses-salad-480)';
|
||||
}
|
||||
|
||||
get systemModuleColor(): string {
|
||||
return 'var(--primary-statuses-green-520)';
|
||||
}
|
||||
|
||||
getDefaultIconByEntityCategory = (entityCategory: EntityCategory): Icon => {
|
||||
const icon = this.defaultEntityIconWithColor.get(entityCategory)?.icon;
|
||||
|
||||
return icon ?? this.firstIcon;
|
||||
};
|
||||
|
||||
getEntityColorByEntityCategory = (entityCategory: EntityCategory): string => {
|
||||
return this.defaultEntityIconWithColor.get(entityCategory)?.color ?? this.defaultModuleColor;
|
||||
};
|
||||
}
|
||||
|
||||
export const iconStore = new IconStore();
|
||||
129
frontend/src/app/store/IdentityStore.ts
Normal file
129
frontend/src/app/store/IdentityStore.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { DataStore } from '@/shared';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import { SequenceName, identityApi, type IdentityPool } from '../api';
|
||||
|
||||
const ID_POOLS_LS_KEY = 'id_pools';
|
||||
|
||||
class IdentityStore implements DataStore {
|
||||
isFetchingMore = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
loadData = async (): Promise<void> => {
|
||||
const poolsInStorage = localStorage.getItem(ID_POOLS_LS_KEY);
|
||||
|
||||
if (poolsInStorage) return;
|
||||
|
||||
try {
|
||||
const pools = await identityApi.getAllIdPools();
|
||||
|
||||
localStorage.setItem(ID_POOLS_LS_KEY, JSON.stringify(pools));
|
||||
} catch (e) {
|
||||
throw new Error(`Error while loading id pools: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
getFieldId = (): number => {
|
||||
return this.getIdFromPool(SequenceName.FIELD);
|
||||
};
|
||||
|
||||
getFieldGroupId = (): number => {
|
||||
return this.getIdFromPool(SequenceName.FIELD_GROUP);
|
||||
};
|
||||
|
||||
getFieldOptionId = (): number => {
|
||||
return this.getIdFromPool(SequenceName.FIELD_OPTION);
|
||||
};
|
||||
|
||||
getPoolFromStorage = (seqName: SequenceName): { pool: IdentityPool; idx: number } => {
|
||||
const parsedPools = this.getParsedPoolsFromStorage();
|
||||
|
||||
const idx = parsedPools.findIndex(p => p.name === seqName);
|
||||
const parsedPool = idx === -1 ? null : parsedPools[idx];
|
||||
|
||||
if (!parsedPool) throw new Error(`Can not get id pool for sequence ${seqName}`);
|
||||
|
||||
return { pool: parsedPool, idx };
|
||||
};
|
||||
|
||||
getParsedPoolsFromStorage = (): IdentityPool[] => {
|
||||
const pools = localStorage.getItem(ID_POOLS_LS_KEY);
|
||||
|
||||
if (!pools) throw new Error('Id pools are not initialized');
|
||||
|
||||
return JSON.parse(pools) as IdentityPool[];
|
||||
};
|
||||
|
||||
addIdsToPool = ({ seqName, ids }: { seqName: SequenceName; ids: number[] }): void => {
|
||||
const { pool, idx } = this.getPoolFromStorage(seqName);
|
||||
|
||||
const parsedPools = this.getParsedPoolsFromStorage();
|
||||
const parsedPool = parsedPools[idx];
|
||||
|
||||
if (!parsedPool) throw new Error(`Can not addIdsToPool, parsed pool was not found, ${seqName}`);
|
||||
|
||||
parsedPool.values = [...pool.values, ...ids];
|
||||
|
||||
localStorage.setItem(ID_POOLS_LS_KEY, JSON.stringify(parsedPools));
|
||||
};
|
||||
|
||||
updateIdsInPool = ({ seqName, ids }: { seqName: SequenceName; ids: number[] }): void => {
|
||||
const parsedPools = this.getParsedPoolsFromStorage();
|
||||
const { idx } = this.getPoolFromStorage(seqName);
|
||||
|
||||
const parsedPool = parsedPools[idx];
|
||||
|
||||
if (!parsedPool)
|
||||
throw new Error(`Can not updateIdsInPool, parsed pool was not found, ${seqName}`);
|
||||
|
||||
parsedPool.values = ids;
|
||||
|
||||
localStorage.setItem(ID_POOLS_LS_KEY, JSON.stringify(parsedPools));
|
||||
};
|
||||
|
||||
getIdFromPool = (seqName: SequenceName): number => {
|
||||
const { pool } = this.getPoolFromStorage(seqName);
|
||||
|
||||
let poolValues = pool.values;
|
||||
|
||||
if (poolValues.length <= 10 && !this.isFetchingMore) {
|
||||
let additionalIds: number[] = [];
|
||||
|
||||
this.isFetchingMore = true;
|
||||
|
||||
identityApi
|
||||
.getIdPoolValues(seqName)
|
||||
.then(res => {
|
||||
additionalIds = res;
|
||||
|
||||
this.addIdsToPool({ seqName, ids: additionalIds });
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error while getting more ids for pool', err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isFetchingMore = false;
|
||||
});
|
||||
}
|
||||
|
||||
const id = poolValues.shift();
|
||||
|
||||
if (id) {
|
||||
this.updateIdsInPool({ seqName, ids: poolValues });
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
throw new Error(`Can not get id from pool ${seqName}`);
|
||||
};
|
||||
|
||||
reset = (): void => {
|
||||
localStorage.removeItem(ID_POOLS_LS_KEY);
|
||||
|
||||
this.isFetchingMore = false;
|
||||
};
|
||||
}
|
||||
|
||||
export const identityStore = new IdentityStore();
|
||||
22
frontend/src/app/store/RecordSingularityStore.ts
Normal file
22
frontend/src/app/store/RecordSingularityStore.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { type Nullable } from '@/shared';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
// we want to only play one record at a time (e.g. in Player component),
|
||||
// so we need to store the current playing record url
|
||||
class RecordSingularityStore {
|
||||
currentRecordUrl: Nullable<string> = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
setCurrentRecordUrl = (url: Nullable<string>): void => {
|
||||
this.currentRecordUrl = url;
|
||||
};
|
||||
|
||||
clear = (): void => {
|
||||
this.currentRecordUrl = null;
|
||||
};
|
||||
}
|
||||
|
||||
export const recordSingularityStore = new RecordSingularityStore();
|
||||
44
frontend/src/app/store/RecordStateStore.ts
Normal file
44
frontend/src/app/store/RecordStateStore.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { type RecordState } from '@/shared';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
export class RecordStateStore {
|
||||
recordState: RecordState;
|
||||
|
||||
constructor(recordState: RecordState) {
|
||||
this.recordState = recordState;
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
setPlaybackRate = (rate: RecordState['playbackRate']): void => {
|
||||
this.recordState.playbackRate = rate;
|
||||
};
|
||||
|
||||
setPlaying = (playing: RecordState['playing']): void => {
|
||||
this.recordState.playing = playing;
|
||||
};
|
||||
|
||||
togglePlaying = (): void => {
|
||||
this.recordState.playing = !this.recordState.playing;
|
||||
};
|
||||
|
||||
setPlayed = (played: RecordState['played']): void => {
|
||||
this.recordState.played = played;
|
||||
};
|
||||
|
||||
setSeekingStatus = (status: RecordState['seekingStatus']): void => {
|
||||
this.recordState.seekingStatus = status;
|
||||
};
|
||||
|
||||
setDuration = (duration: RecordState['duration']): void => {
|
||||
this.recordState.duration = duration;
|
||||
};
|
||||
|
||||
setExpanded = (expanded: RecordState['expanded']): void => {
|
||||
this.recordState.expanded = expanded;
|
||||
};
|
||||
|
||||
toggleExpanded = (): void => {
|
||||
this.recordState.expanded = !this.recordState.expanded;
|
||||
};
|
||||
}
|
||||
22
frontend/src/app/store/SettingsStore.ts
Normal file
22
frontend/src/app/store/SettingsStore.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { makeAutoObservable, observe } from 'mobx';
|
||||
|
||||
export class SettingsStore<T extends Record<string, any>> {
|
||||
settings: T;
|
||||
|
||||
private constructor(name: string) {
|
||||
const stored = localStorage.getItem(name);
|
||||
this.settings = stored ? (JSON.parse(stored) as T) : ({} as T);
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
static getSettingsStore<T extends Record<string, any>>(name: string): SettingsStore<T> {
|
||||
const store = new SettingsStore<T>(name);
|
||||
|
||||
observe(store.settings, () => {
|
||||
localStorage.setItem(name, JSON.stringify(store.settings));
|
||||
});
|
||||
|
||||
return store;
|
||||
}
|
||||
}
|
||||
100
frontend/src/app/store/SubscriptionStore.ts
Normal file
100
frontend/src/app/store/SubscriptionStore.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { DataStore, Nullable, Subscription, SubscriptionPlan } from '@/shared';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import { subscriptionApi } from '../api';
|
||||
|
||||
class SubscriptionStore implements DataStore {
|
||||
subscription: Nullable<Subscription> = null;
|
||||
|
||||
isGettingPortalUrl = false;
|
||||
isSubscriptionLoaded = false;
|
||||
areSubscriptionPlansLoaded = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get isTrial(): boolean {
|
||||
if (!this.subscription) return false;
|
||||
|
||||
return this.subscription.isTrial;
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
if (!this.subscription) return false;
|
||||
|
||||
return this.subscription.isValid;
|
||||
}
|
||||
|
||||
loadData = async (): Promise<void> => {
|
||||
try {
|
||||
this.subscription = await subscriptionApi.getSubscription();
|
||||
} catch (e) {
|
||||
throw new Error(`Error while loading subscription: ${e}`);
|
||||
} finally {
|
||||
this.isSubscriptionLoaded = true;
|
||||
}
|
||||
};
|
||||
|
||||
getSubscriptionPlans = async (): Promise<SubscriptionPlan[]> => {
|
||||
try {
|
||||
this.areSubscriptionPlansLoaded = false;
|
||||
|
||||
return await subscriptionApi.getSubscriptionPlans();
|
||||
} catch (e) {
|
||||
throw new Error(`Error while loading subscription plans: ${e}`);
|
||||
} finally {
|
||||
this.areSubscriptionPlansLoaded = true;
|
||||
}
|
||||
};
|
||||
|
||||
getCheckoutUrl = async ({
|
||||
amount,
|
||||
priceId,
|
||||
productId,
|
||||
couponId,
|
||||
numberOfUsers,
|
||||
}: {
|
||||
numberOfUsers: number;
|
||||
amount?: number;
|
||||
priceId?: string;
|
||||
couponId?: string;
|
||||
productId?: string;
|
||||
}): Promise<string> => {
|
||||
try {
|
||||
this.isGettingPortalUrl = true;
|
||||
|
||||
return await subscriptionApi.getCheckoutUrl({
|
||||
productId,
|
||||
amount,
|
||||
priceId,
|
||||
couponId,
|
||||
numberOfUsers,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Error while loading subscription plans: ${e}`);
|
||||
} finally {
|
||||
this.isGettingPortalUrl = false;
|
||||
}
|
||||
};
|
||||
|
||||
getPortalUrl = async (): Promise<string> => {
|
||||
try {
|
||||
this.isGettingPortalUrl = true;
|
||||
|
||||
return await subscriptionApi.getPortalUrl();
|
||||
} catch (e) {
|
||||
throw new Error(`Error while loading subscription plans: ${e}`);
|
||||
} finally {
|
||||
this.isGettingPortalUrl = false;
|
||||
}
|
||||
};
|
||||
|
||||
reset = (): void => {
|
||||
this.subscription = null;
|
||||
|
||||
this.areSubscriptionPlansLoaded = false;
|
||||
this.isGettingPortalUrl = false;
|
||||
};
|
||||
}
|
||||
|
||||
export const subscriptionStore = new SubscriptionStore();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user