This commit is contained in:
Viktoria Polyakova
2026-01-25 08:57:38 +00:00
commit 4fb101c5db
7657 changed files with 497012 additions and 0 deletions

143
frontend/src/app/App.tsx Normal file
View 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)

View 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',
}

View 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);

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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),
}),
});

View 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();

View File

@@ -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();

View File

@@ -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();

View 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();

View 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();

View 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();

View 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();

View File

@@ -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),
});

View File

@@ -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);
},
});
};

View 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();

View 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();

View File

@@ -0,0 +1,5 @@
import { queryClient } from '@/index';
import { APP_QUERY_KEYS } from '../../AppQueryKeys';
export const invalidateUserProfilesInCache = () =>
queryClient.invalidateQueries({ queryKey: APP_QUERY_KEYS.userProfiles });

View File

@@ -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),
}),
});
};

View 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();

View 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();

View File

@@ -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,
});

View 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;
}

View 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 });
}
}

View File

@@ -0,0 +1,9 @@
export class UpdateBoardDto {
name: string;
sortOrder: number;
participantIds: number[];
constructor(data: Partial<UpdateBoardDto>) {
Object.assign(this, data);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View File

@@ -0,0 +1,8 @@
import { type Nullable } from '@/shared';
export interface ContactInfoDto {
id: number;
name: string;
phone: Nullable<string[]>;
email: Nullable<string>;
}

View 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;
}

View 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>;
}

View 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;
}
}

View 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[];
}

View 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;
}
}

View File

@@ -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;
}
}

View 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;
}

View File

@@ -0,0 +1,4 @@
export enum FeedbackType {
TRIAL_EXPIRED = 'trial_expired',
USER_LIMIT = 'user_limit',
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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;
}

View 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;
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,5 @@
export interface FrontendObjectDto<T extends unknown = unknown> {
key: string;
value: T;
createdAt: string;
}

View File

@@ -0,0 +1,9 @@
import type { Nullable } from '@/shared';
export interface AccountDto {
id: number;
subdomain: string;
createdAt: string;
companyName: string;
logoUrl: Nullable<string>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View 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,
});
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,9 @@
export class SiteFormFieldDataDto {
id: number;
value: unknown;
constructor({ id, value }: SiteFormFieldDataDto) {
this.id = id;
this.value = value;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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>;
}

View File

@@ -0,0 +1,4 @@
export class SubscriptionFeatureDto {
name: string;
available: boolean;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
export interface UpdateSubscriptionDto {
isTrial?: boolean;
periodStart?: string;
periodEnd?: string;
userLimit?: number;
planName?: string;
externalCustomerId?: string | null;
firstVisit?: string;
}

View File

@@ -0,0 +1,9 @@
export class ChangeUserPasswordDto {
currentPassword: string;
newPassword: string;
constructor({ currentPassword, newPassword }: ChangeUserPasswordDto) {
this.currentPassword = currentPassword;
this.newPassword = newPassword;
}
}

View 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;
}
}

View 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,
});
}
}

View 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;
}

View File

@@ -0,0 +1,5 @@
export interface VersionDto {
id: number;
version: string;
date: string;
}

View 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';

View 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';

View 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 };

View 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;

View 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;

View 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>
);
};

View 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>
);

View File

@@ -0,0 +1,4 @@
import type { Decorator } from '@storybook/react';
import '../../styles/main.css';
export const StyleDecorator: Decorator = StoryComponent => <StoryComponent />;

View File

@@ -0,0 +1,4 @@
export * from './api';
export { App } from './App';
export * from './routes';
export * from './store';

View 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 />;
});

View File

@@ -0,0 +1 @@
export { HomePage } from './HomePage/HomePage';

View 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 };

View 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 />,
},
],
},
]);
};

View File

@@ -0,0 +1,3 @@
export { AppBoundary } from './AppBoundary';
export { createRouter } from './createRouter';
export { PBX_PROVIDER_TYPE_QUERY_PARAM, routes } from './routes';

View 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}`,
};

View 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();

View 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();

View 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();

View 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);
};
}

View 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();

View 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();

View 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();

View 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();

View 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;
};
}

View 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;
}
}

View 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