feat: a lot of a lot

This commit is contained in:
2024-09-01 01:05:20 +03:00
parent b93bfe39d5
commit 45d80b7c86
38 changed files with 1000 additions and 6 deletions

View File

@@ -44,6 +44,8 @@ export type { CreateBarcodeTemplateAttributeRequest } from './models/CreateBarco
export type { CreateBarcodeTemplateAttributeResponse } from './models/CreateBarcodeTemplateAttributeResponse';
export type { CreateDealBillRequest } from './models/CreateDealBillRequest';
export type { CreateDealBillResponse } from './models/CreateDealBillResponse';
export type { CreateMarketplaceRequest } from './models/CreateMarketplaceRequest';
export type { CreateMarketplaceResponse } from './models/CreateMarketplaceResponse';
export type { CreatePaymentRecordRequest } from './models/CreatePaymentRecordRequest';
export type { CreatePaymentRecordResponse } from './models/CreatePaymentRecordResponse';
export type { CreatePayRateRequest } from './models/CreatePayRateRequest';
@@ -55,6 +57,7 @@ export type { CreateServicesKitRequest } from './models/CreateServicesKitRequest
export type { CreateServicesKitResponse } from './models/CreateServicesKitResponse';
export type { CreateShippingWarehouseRequest } from './models/CreateShippingWarehouseRequest';
export type { CreateShippingWarehouseResponse } from './models/CreateShippingWarehouseResponse';
export type { CreateTaskResponse } from './models/CreateTaskResponse';
export type { CreateUserRequest } from './models/CreateUserRequest';
export type { CreateUserResponse } from './models/CreateUserResponse';
export type { DealAddKitRequest } from './models/DealAddKitRequest';
@@ -107,6 +110,8 @@ export type { DealUpdateServiceQuantityRequest } from './models/DealUpdateServic
export type { DealUpdateServiceQuantityResponse } from './models/DealUpdateServiceQuantityResponse';
export type { DealUpdateServiceRequest } from './models/DealUpdateServiceRequest';
export type { DealUpdateServiceResponse } from './models/DealUpdateServiceResponse';
export type { DeleteMarketplaceRequest } from './models/DeleteMarketplaceRequest';
export type { DeleteMarketplaceResponse } from './models/DeleteMarketplaceResponse';
export type { DeletePaymentRecordRequest } from './models/DeletePaymentRecordRequest';
export type { DeletePaymentRecordResponse } from './models/DeletePaymentRecordResponse';
export type { DeletePayRateRequest } from './models/DeletePayRateRequest';
@@ -128,6 +133,8 @@ export type { GetAllShippingWarehousesResponse } from './models/GetAllShippingWa
export type { GetAllUsersResponse } from './models/GetAllUsersResponse';
export type { GetBarcodeTemplateByIdRequest } from './models/GetBarcodeTemplateByIdRequest';
export type { GetBarcodeTemplateByIdResponse } from './models/GetBarcodeTemplateByIdResponse';
export type { GetClientMarketplacesRequest } from './models/GetClientMarketplacesRequest';
export type { GetClientMarketplacesResponse } from './models/GetClientMarketplacesResponse';
export type { GetDealBillById } from './models/GetDealBillById';
export type { GetPaymentRecordsResponse } from './models/GetPaymentRecordsResponse';
export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfRequest';
@@ -138,6 +145,8 @@ export type { GetServiceKitSchema } from './models/GetServiceKitSchema';
export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest';
export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema';
export type { MarketplaceSchema } from './models/MarketplaceSchema';
export type { NotificationChannel } from './models/NotificationChannel';
export type { PaginationInfoSchema } from './models/PaginationInfoSchema';
export type { PaymentRecordCreateSchema } from './models/PaymentRecordCreateSchema';
@@ -177,8 +186,12 @@ export type { ServiceSchema } from './models/ServiceSchema';
export type { ServiceUpdateRequest } from './models/ServiceUpdateRequest';
export type { ServiceUpdateResponse } from './models/ServiceUpdateResponse';
export type { ShippingWarehouseSchema } from './models/ShippingWarehouseSchema';
export type { SynchronizeMarketplaceRequest } from './models/SynchronizeMarketplaceRequest';
export type { TaskInfoResponse } from './models/TaskInfoResponse';
export type { TimeTrackingData } from './models/TimeTrackingData';
export type { TimeTrackingRecord } from './models/TimeTrackingRecord';
export type { UpdateMarketplaceRequest } from './models/UpdateMarketplaceRequest';
export type { UpdateMarketplaceResponse } from './models/UpdateMarketplaceResponse';
export type { UpdatePayRateRequest } from './models/UpdatePayRateRequest';
export type { UpdatePayRateResponse } from './models/UpdatePayRateResponse';
export type { UpdateServiceKitSchema } from './models/UpdateServiceKitSchema';
@@ -207,5 +220,6 @@ export { ProductService } from './services/ProductService';
export { RoleService } from './services/RoleService';
export { ServiceService } from './services/ServiceService';
export { ShippingWarehouseService } from './services/ShippingWarehouseService';
export { TaskService } from './services/TaskService';
export { TimeTrackingService } from './services/TimeTrackingService';
export { UserService } from './services/UserService';

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { MarketplaceCreateSchema } from './MarketplaceCreateSchema';
export type CreateMarketplaceRequest = {
marketplace: MarketplaceCreateSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateMarketplaceResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateTaskResponse = {
taskId: string;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DeleteMarketplaceRequest = {
marketplaceId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DeleteMarketplaceResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GetClientMarketplacesRequest = {
clientId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { MarketplaceSchema } from './MarketplaceSchema';
export type GetClientMarketplacesResponse = {
marketplaces: Array<MarketplaceSchema>;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type MarketplaceCreateSchema = {
name: string;
clientId: number;
baseMarketplaceKey: string;
authData: Record<string, any>;
};

View File

@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseMarketplaceSchema } from './BaseMarketplaceSchema';
import type { ClientSchema } from './ClientSchema';
export type MarketplaceSchema = {
name: string;
baseMarketplace: BaseMarketplaceSchema;
client: ClientSchema;
authData: Record<string, any>;
id: number;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type SynchronizeMarketplaceRequest = {
marketplaceId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TaskInfoResponse = {
taskId: string;
status: string;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { MarketplaceSchema } from './MarketplaceSchema';
export type UpdateMarketplaceRequest = {
marketplace: MarketplaceSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateMarketplaceResponse = {
ok: boolean;
message: string;
};

View File

@@ -28,4 +28,15 @@ export class AuthService {
},
});
}
/**
* Test
* @returns any Successful Response
* @throws ApiError
*/
public static testAuthTestPost(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/auth/test',
});
}
}

View File

@@ -71,7 +71,7 @@ export class ClientService {
* @returns ClientCreateResponse Successful Response
* @throws ApiError
*/
public static createClient({
public static createClientApi({
requestBody,
}: {
requestBody: ClientCreateRequest,

View File

@@ -2,7 +2,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CreateMarketplaceRequest } from '../models/CreateMarketplaceRequest';
import type { CreateMarketplaceResponse } from '../models/CreateMarketplaceResponse';
import type { DeleteMarketplaceRequest } from '../models/DeleteMarketplaceRequest';
import type { DeleteMarketplaceResponse } from '../models/DeleteMarketplaceResponse';
import type { GetAllBaseMarketplacesResponse } from '../models/GetAllBaseMarketplacesResponse';
import type { GetClientMarketplacesRequest } from '../models/GetClientMarketplacesRequest';
import type { GetClientMarketplacesResponse } from '../models/GetClientMarketplacesResponse';
import type { UpdateMarketplaceRequest } from '../models/UpdateMarketplaceRequest';
import type { UpdateMarketplaceResponse } from '../models/UpdateMarketplaceResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
@@ -18,4 +26,84 @@ export class MarketplaceService {
url: '/marketplace/base/get-all',
});
}
/**
* Get
* @returns GetClientMarketplacesResponse Successful Response
* @throws ApiError
*/
public static getClientMarketplaces({
requestBody,
}: {
requestBody: GetClientMarketplacesRequest,
}): CancelablePromise<GetClientMarketplacesResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/marketplace/get',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Create
* @returns CreateMarketplaceResponse Successful Response
* @throws ApiError
*/
public static createMarketplace({
requestBody,
}: {
requestBody: CreateMarketplaceRequest,
}): CancelablePromise<CreateMarketplaceResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/marketplace/create',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete
* @returns DeleteMarketplaceResponse Successful Response
* @throws ApiError
*/
public static deleteMarketplace({
requestBody,
}: {
requestBody: DeleteMarketplaceRequest,
}): CancelablePromise<DeleteMarketplaceResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/marketplace/delete',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Update
* @returns UpdateMarketplaceResponse Successful Response
* @throws ApiError
*/
public static updateMarketplace({
requestBody,
}: {
requestBody: UpdateMarketplaceRequest,
}): CancelablePromise<UpdateMarketplaceResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/marketplace/update',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -0,0 +1,53 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CreateTaskResponse } from '../models/CreateTaskResponse';
import type { SynchronizeMarketplaceRequest } from '../models/SynchronizeMarketplaceRequest';
import type { TaskInfoResponse } from '../models/TaskInfoResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class TaskService {
/**
* Synchronize Marketplace
* @returns CreateTaskResponse Successful Response
* @throws ApiError
*/
public static createSynchronizeMarketplaceTask({
requestBody,
}: {
requestBody: SynchronizeMarketplaceRequest,
}): CancelablePromise<CreateTaskResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/task/synchronize-marketplace',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Task Info
* @returns TaskInfoResponse Successful Response
* @throws ApiError
*/
public static getTaskInfo({
taskId,
}: {
taskId: string,
}): CancelablePromise<TaskInfoResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/task/info/{task_id}',
path: {
'task_id': taskId,
},
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -8,7 +8,7 @@ import {
IconHome2,
IconLogout,
IconMan,
IconMoon,
IconMoon, IconShoppingCart,
IconSun,
} from '@tabler/icons-react';
import classes from './Navbar.module.css';
@@ -76,6 +76,11 @@ const mockdata = [
icon: IconBuildingWarehouse,
label: 'Склады отгрузки',
href: '/shipping_warehouses'
},
{
icon:IconShoppingCart,
label: 'Маркетплейсы',
href: '/marketplaces'
}
];

View File

@@ -9,6 +9,7 @@ const ClientSelectNew: FC<Props> = (props) => {
const {clients} = useClientsList();
return (
<ObjectSelect
searchable
data={clients}
{...props}
/>

101
src/features/tasksSlice.tsx Normal file
View File

@@ -0,0 +1,101 @@
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
import {notifications} from "../shared/lib/notifications.ts";
import {IconCheck, IconX} from "@tabler/icons-react";
import {rem} from "@mantine/core";
export type TaskData = {
title: string;
message: string;
};
export type TaskConfig = {
onSuccessData: TaskData;
onErrorData: TaskData;
onLoadingData: TaskData;
};
export type Task = {
id: string;
config: TaskConfig;
info: Record<string, unknown>;
};
interface TasksState {
tasks: Task[];
notificationTaskMap: { [key: string]: string };
}
const initialState: TasksState = {
tasks: [],
notificationTaskMap: {},
};
const tasksSlice = createSlice({
name: "tasks",
initialState,
reducers: {
addTask: (state, action: PayloadAction<Task>) => {
const task = action.payload;
state.notificationTaskMap[task.id] = notifications.show({
loading: true,
title: task.config.onLoadingData.title,
message: task.config.onLoadingData.message,
autoClose: false,
withCloseButton: false,
withBorder: true,
radius: "sm"
});
state.tasks.push(task);
localStorage.setItem("tasks", JSON.stringify(state.tasks));
},
removeTask: (state, action: PayloadAction<string>) => {
state.tasks = state.tasks.filter((task) => task.id !== action.payload);
localStorage.setItem("tasks", JSON.stringify(state.tasks));
},
failTask: (state, action: PayloadAction<Task>) => {
const task = action.payload;
const notificationId = state.notificationTaskMap[task.id];
if (!notificationId) return;
notifications.update({
id: notificationId,
color: 'red',
title: task.config.onErrorData.title,
message: task.config.onErrorData.message,
icon: <IconX style={{width: rem(18), height: rem(18)}}/>,
loading: false,
autoClose: 2000,
})
state.tasks = state.tasks.filter((task) => task.id !== action.payload.id);
state.notificationTaskMap = Object.fromEntries(
Object.entries(state.notificationTaskMap).filter(([taskId]) => taskId !== task.id)
);
localStorage.setItem("tasks", JSON.stringify(state.tasks));
},
successTask: (state, action: PayloadAction<Task>) => {
const task = action.payload;
const notificationId = state.notificationTaskMap[task.id];
if (!notificationId) return;
notifications.update({
id: notificationId,
color: 'teal',
title: task.config.onSuccessData.title,
message: task.config.onSuccessData.message,
icon: <IconCheck style={{width: rem(18), height: rem(18)}}/>,
loading: false,
autoClose: 2000,
})
state.tasks = state.tasks.filter((task) => task.id !== action.payload.id);
state.notificationTaskMap = Object.fromEntries(
Object.entries(state.notificationTaskMap).filter(([taskId]) => taskId !== task.id)
);
localStorage.setItem("tasks", JSON.stringify(state.tasks));
}
},
});
export const {addTask, removeTask, successTask, failTask} = tasksSlice.actions;
export default tasksSlice.reducer;

View File

@@ -0,0 +1,54 @@
import {useEffect, useRef, DependencyList} from 'react';
type UsePollingEffectOptions = {
interval?: number;
isActive?: boolean;
onCleanUp?: () => void;
};
function usePollingEffect(
asyncCallback: () => Promise<void>,
dependencies: DependencyList = [],
options: UsePollingEffectOptions = {}
): void {
const {
interval = 3000,
isActive = true,
onCleanUp = () => {
}
} = options;
const timeoutIdRef = useRef<number | null>(null);
useEffect(() => {
if (!isActive) { // If not active, don't do anything
return;
}
let stopped = false;
const pollingCallback = async () => {
try {
await asyncCallback();
} finally {
if (!stopped) {
timeoutIdRef.current = setTimeout(pollingCallback, interval);
}
}
};
// Immediately invoke the polling callback when the effect runs
pollingCallback();
// Clean up function to clear the timeout if the component unmounts or dependencies change
return () => {
stopped = true; // This will prevent new timeouts from being scheduled
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
onCleanUp();
};
}, [...dependencies, interval, isActive]); // dependencies array spread with interval
}
export default usePollingEffect;

View File

@@ -19,6 +19,7 @@ import {ModalsProvider} from "@mantine/modals";
import {OpenAPI} from "./client";
import {DatesProvider} from "@mantine/dates";
import {modals} from "./modals/modals.ts";
import TasksProvider from "./providers/TasksProvider/TasksProvider.tsx";
// Configuring router
const router = createRouter({routeTree})
@@ -46,8 +47,12 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<MantineProvider defaultColorScheme={"dark"}>
<ModalsProvider modals={modals}>
<DatesProvider settings={{locale: 'ru'}}>
<RouterProvider router={router}/>
<Notifications/>
<TasksProvider>
<RouterProvider router={router}/>
<Notifications/>
</TasksProvider>
</DatesProvider>
</ModalsProvider>
</MantineProvider>

View File

@@ -20,6 +20,7 @@ import ServiceKitModalForm from "../pages/ServicesPage/modals/ServicesKitModalFo
import ServicesKitSelectModal from "./ServicesKitSelectModal/ServicesKitSelectModal.tsx";
import SelectDealProductsModal from "../pages/LeadsPage/modals/SelectDealProductsModal.tsx";
import ShippingWarehouseForm from "../pages/ShippingWarehousesPage/modals/ShippingWarehouseForm.tsx";
import MarketplaceFormModal from "../pages/MarketplacesPage/modals/MarketplaceFormModal/MarketplaceFormModal.tsx";
export const modals = {
enterDeadline: EnterDeadlineModal,
@@ -42,5 +43,6 @@ export const modals = {
serviceKitModalForm: ServiceKitModalForm,
servicesKitSelectModal: ServicesKitSelectModal,
selectDealProductsModal: SelectDealProductsModal,
shippingWarehouseForm: ShippingWarehouseForm
shippingWarehouseForm: ShippingWarehouseForm,
marketplaceFormModal: MarketplaceFormModal
}

View File

@@ -0,0 +1,100 @@
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {ClientSchema, MarketplaceSchema} from "../../../../client";
import {FC} from "react";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import useMarketplacesTableColumns from "./columns.tsx";
import {MRT_TableOptions} from "mantine-react-table";
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
import {modals} from "@mantine/modals";
import {IconEdit, IconRefresh, IconTrash} from "@tabler/icons-react";
type RestProps = {
client?: ClientSchema;
onSynchronize?: (marketplace: MarketplaceSchema) => void;
}
type Props = CRUDTableProps<MarketplaceSchema> & RestProps;
const MarketplacesTable: FC<Props> = ({onDelete, onChange, onCreate, items, client, onSynchronize}) => {
const columns = useMarketplacesTableColumns();
const onDeleteClick = (marketplace: MarketplaceSchema) => {
if (!onDelete) return;
modals.openConfirmModal({
title: 'Удаление маркетплейса',
children: (
<Text size="sm">
Вы уверены что хотите удалить маркетплейс {marketplace.name}
</Text>
),
labels: {confirm: 'Да', cancel: "Нет"},
confirmProps: {color: 'red'},
onConfirm: () => onDelete(marketplace)
});
}
const onEditClick = (marketplace: MarketplaceSchema) => {
if (!onChange) return;
modals.openContextModal({
modal: "marketplaceFormModal",
withCloseButton: false,
innerProps: {
onChange: (event) => onChange(event),
element: marketplace
}
})
}
const onCreateClick = () => {
if (!onCreate || !client) return;
modals.openContextModal({
modal: "marketplaceFormModal",
withCloseButton: false,
innerProps: {
onCreate: (event) => onCreate({...event, client: client})
}
})
}
return (
<BaseTable
data={items}
columns={columns}
restProps={{
enableSorting: false,
enableColumnActions: false,
enableTopToolbar: true,
renderTopToolbar: (
<Flex p={rem(10)} justify="end">
<Button
disabled={!client}
variant={"default"}
onClick={() => onCreateClick()}>
Добавить
</Button>
</Flex>
),
enableRowActions: true,
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onEditClick(row.original)}
variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label="Удалить">
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
<Tooltip label="Синхронизировать">
<ActionIcon onClick={() => onSynchronize && onSynchronize(row.original)}
variant={"default"}>
<IconRefresh/>
</ActionIcon>
</Tooltip>
</Flex>
)
} as MRT_TableOptions<MarketplaceSchema>}
/>
)
}
export default MarketplacesTable;

View File

@@ -0,0 +1,35 @@
import {MarketplaceSchema} from "../../../../client";
import {MRT_ColumnDef} from "mantine-react-table";
import {useMemo} from "react";
import {ActionIcon, Image} from "@mantine/core";
const useMarketplacesTableColumns = () => {
return useMemo<MRT_ColumnDef<MarketplaceSchema>[]>(() => [
{
header: "Маркетплейс",
size: 10,
Cell: ({row}) => (
<ActionIcon variant={"transparent"}>
<Image src={row.original.baseMarketplace?.iconUrl || ""}/>
</ActionIcon>
)
},
{
accessorKey: "name",
header: "Название",
enableSorting: false,
},
{
accessorKey: "client.name",
header: "Клиент",
enableSorting: false,
},
// {
// accessorKey: "authData",
// header: "Данные авторизации",
// enableSorting: false,
// },
], []);
}
export default useMarketplacesTableColumns;

View File

@@ -0,0 +1,115 @@
import {useEffect, useState} from "react";
import {ClientSchema, MarketplaceSchema, MarketplaceService, TaskService} from "../../../client";
import {notifications} from "../../../shared/lib/notifications.ts";
import {RootState, useAppDispatch} from "../../../redux/store.ts";
import {addTask} from "../../../features/tasksSlice.tsx";
import {useSelector} from "react-redux";
const useMarketplacesPageState = () => {
const dispatch = useAppDispatch();
const tasks = useSelector((state: RootState) => state.tasks.tasks);
const [client, setClient] = useState<ClientSchema | undefined>();
const [items, setItems] = useState<MarketplaceSchema[]>([]);
const fetchMarketplaces = async () => {
if (!client) return;
MarketplaceService.getClientMarketplaces({
requestBody: {
clientId: client.id
}
}).then((response) => {
setItems(response.marketplaces);
})
}
const onCreate = (marketplace: MarketplaceSchema) => {
MarketplaceService.createMarketplace({
requestBody: {
marketplace: {
...marketplace,
clientId: marketplace.client.id,
baseMarketplaceKey: marketplace.baseMarketplace.key
}
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
if (!ok) return;
await fetchMarketplaces();
})
}
const onDelete = (marketplace: MarketplaceSchema) => {
MarketplaceService.deleteMarketplace({
requestBody: {
marketplaceId: marketplace.id
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
if (!ok) return;
await fetchMarketplaces();
})
}
const onChange = (marketplace: MarketplaceSchema) => {
MarketplaceService.updateMarketplace({
requestBody: {
marketplace: marketplace
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
if (!ok) return;
await fetchMarketplaces();
})
}
const onSynchronize = (marketplace: MarketplaceSchema) => {
// If there is already synchronization task for this marketplace show notifications.error()
const task = tasks.find(task => task.info.marketplaceId === marketplace.id);
if (task) {
notifications.error({
title: 'Ошибка',
message: `Синхронизация маркетплейса ${marketplace.name} уже запущена`
});
return;
}
TaskService.createSynchronizeMarketplaceTask({
requestBody: {
marketplaceId:
marketplace.id
}
}).then(({taskId}) => {
dispatch(addTask({
id: taskId,
config: {
onErrorData: {
title: 'Ошибка',
message: `Ошибка синхронизации маркетплейса: ${marketplace.name}`
},
onLoadingData: {
title: 'Синхронизация',
message: `Синхронизация маркетплейса: ${marketplace.name}`
},
onSuccessData: {
title: 'Успех',
message: `Маркетплейс ${marketplace.name} успешно синхронизирован`
}
},
info: {
marketplaceId: marketplace.id
}
}));
})
}
useEffect(() => {
fetchMarketplaces();
}, [client]);
return {
client,
setClient,
items,
onDelete,
onChange,
onCreate,
onSynchronize
}
}
export default useMarketplacesPageState;

View File

@@ -0,0 +1 @@
export {MarketplacesPage} from './ui/MarketplacesPage.tsx';

View File

@@ -0,0 +1,71 @@
import {TextInput} from "@mantine/core";
import {BaseFormInputProps} from "../../../../types/utils.ts";
import {FC} from "react";
import {BaseMarketplaceSchema} from "../../../../client";
import {BaseMarketplaceType} from "../../../../shared/enums/BaseMarketplaceType.ts";
type RestProps = {
baseMarketplace: BaseMarketplaceSchema;
}
type Props = BaseFormInputProps<Record<string, string>> & RestProps;
const MarketplaceAuthDataInput: FC<Props> = (props: Props) => {
console.log(props.baseMarketplace);
const getWildberriesInputs = () => {
// return input that sets record "Authorization" to value
return <TextInput
{...props}
label={"Ключ авторизации"}
placeholder={"Введите ключ авторизации"}
value={props.value["Authorization"] || ""}
onChange={(value) => props.onChange({...props.value, Authorization: value.target.value})}
/>
}
const getOzonInputs = () => {
// return input that sets record "Client-Id" and "Api-Key" to value
return (
<>
<TextInput
{...props}
label={"Client-Id"}
placeholder={"Введите Client-Id"}
value={props.value["Client-Id"] || ""}
onChange={(value) => props.onChange({...props.value, "Client-Id": value.target.value})}
/>
<TextInput
{...props}
label={"Api-Key"}
placeholder={"Введите Api-Key"}
value={props.value["Api-Key"] || ""}
onChange={(value) => props.onChange({...props.value, "Api-Key": value.target.value})}
/>
</>
)
}
const getYandexMarketInputs = () => {
}
const getInputs = () => {
if (props.baseMarketplace.key === BaseMarketplaceType.WILDBERRIES) {
return getWildberriesInputs();
}
if (props.baseMarketplace.key === BaseMarketplaceType.OZON) {
return getOzonInputs();
}
if (props.baseMarketplace.key === BaseMarketplaceType.YANDEX_MARKET) {
return getYandexMarketInputs();
}
return <></>
}
return (
<>
{getInputs()}
</>
)
}
export default MarketplaceAuthDataInput;

View File

@@ -0,0 +1,70 @@
import {ContextModalProps} from "@mantine/modals";
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import {MarketplaceSchema} from "../../../../client";
import {useForm} from "@mantine/form";
import {Fieldset, Flex, rem, TextInput} from "@mantine/core";
import BaseMarketplaceSelect from "../../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import MarketplaceAuthDataInput from "./MarketplaceAuthDataInput.tsx";
type Props = CreateEditFormProps<MarketplaceSchema>
const MarketplaceFormModal = ({
context,
id,
innerProps
}: ContextModalProps<Props>) => {
const isEditing = 'element' in innerProps;
const initialValue: Partial<MarketplaceSchema> = isEditing ? innerProps.element : {
authData: {
Authorization: '',
"Client-Id": '',
"Api-Key": ''
},
};
const form = useForm<Partial<MarketplaceSchema>>({
initialValues: initialValue,
validate: {
baseMarketplace: (baseMarketplace) => !baseMarketplace && "Необходимо указать базовый маркетплейс",
name: (name) => !name && "Необходимо указать название маркетплейса",
authData: (authData) => !authData && "Необходимо указать данные авторизации"
}
});
return (
<BaseFormModal
form={form}
closeOnSubmit
onClose={() => context.closeContextModal(id)}
{...innerProps}
>
<BaseFormModal.Body>
<>
<Fieldset legend={"Общие параметры"}>
<Flex direction={"column"} gap={rem(10)}>
<TextInput
label={"Название"}
placeholder={"Введите название маркетплейса"}
{...form.getInputProps("name")}
/>
<BaseMarketplaceSelect
label={"Базовый маркетплейс"}
placeholder={"Выберите базовый маркетплейс"}
{...form.getInputProps("baseMarketplace")}
/>
{form.values.baseMarketplace &&
<MarketplaceAuthDataInput
baseMarketplace={form.values.baseMarketplace}
value={form.values.authData as Record<string, string>}
onChange={(value) => form.setFieldValue("authData", value)}
error={form.getInputProps("authData").error}
/>
}
</Flex>
</Fieldset>
</>
</BaseFormModal.Body>
</BaseFormModal>
)
}
export default MarketplaceFormModal;

View File

@@ -0,0 +1,17 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
gap: rem(10);
}
.top-panel {
padding: rem(5);
gap: rem(10);
display: flex;
}
.top-panel-last-item {
margin-left: auto;
}

View File

@@ -0,0 +1,29 @@
import styles from './MarketplacesPage.module.css';
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import useMarketplacesPageState from "../hooks/useMarketplacesPageState.tsx";
import MarketplacesTable from "../components/MarketplacesTable/MarketplacesTable.tsx";
export const MarketplacesPage = () => {
const state = useMarketplacesPageState();
return (
<div className={styles['container']}>
<PageBlock>
<div className={styles['top-panel']}>
<ClientSelectNew
placeholder={'Выберите клиента'}
onChange={state.setClient}
/>
</div>
</PageBlock>
<PageBlock>
<>
<MarketplacesTable
{...state}
/>
</>
</PageBlock>
</div>
)
}

View File

@@ -0,0 +1,63 @@
import {FC, ReactNode, useEffect, useState} from "react";
import {TaskStatus} from "../../shared/enums/TaskStatus";
import usePollingEffect from "../../hooks/usePollingEffect.tsx";
import {RootState, useAppDispatch} from "../../redux/store.ts";
import {useSelector} from "react-redux";
import {TaskService} from "../../client";
import {addTask, failTask, successTask, Task} from "../../features/tasksSlice.tsx";
type Props = {
children: ReactNode;
};
const POLLING_STATUSES = [
TaskStatus.RETRY,
TaskStatus.STARTED,
TaskStatus.PENDING,
] as string[];
const TasksProvider: FC<Props> = ({children}) => {
const [isPooling, setIsPooling] = useState(false);
const tasks = useSelector((state: RootState) => state.tasks.tasks);
const notificationTaskMap = useSelector((state: RootState) => state.tasks.notificationTaskMap);
const dispatch = useAppDispatch();
const poolTasks = async () => {
// get statuses of all tasks
const taskInfos = await Promise.all(
tasks.map((task) => TaskService.getTaskInfo({taskId: task.id}))
);
taskInfos.forEach(({taskId, status}) => {
if (POLLING_STATUSES.includes(status)) return;
const task = tasks.find((task) => task.id === taskId);
if (!task) return;
if (status === TaskStatus.FAILURE) {
dispatch(failTask(task));
} else if (status === TaskStatus.SUCCESS) {
dispatch(successTask(task));
}
})
}
usePollingEffect(
poolTasks,
[tasks, notificationTaskMap, isPooling],
{interval: 1000, isActive: isPooling && tasks.length > 0}
)
useEffect(() => {
if (tasks.length === 0) {
setIsPooling(false);
return
}
setIsPooling(true);
}, [tasks]);
useEffect(() => {
// loading from localstorage
const tasks = JSON.parse(localStorage.getItem("tasks") || "[]");
tasks.forEach((task: Task) => {
dispatch(addTask(task));
});
}, []);
return <>{children}</>;
};
export default TasksProvider;

View File

@@ -2,11 +2,13 @@ import {configureStore} from "@reduxjs/toolkit";
import {useDispatch} from "react-redux";
import authReducer from '../features/authSlice';
import uiReducer from '../features/uiSlice';
import tasksReducer from '../features/tasksSlice.tsx';
export const store = configureStore({
reducer: {
auth: authReducer,
ui: uiReducer
ui: uiReducer,
tasks: tasksReducer
}
});

View File

@@ -21,6 +21,7 @@ const TestLazyImport = createFileRoute('/test')()
const ShippingwarehousesLazyImport = createFileRoute('/shipping_warehouses')()
const ServicesLazyImport = createFileRoute('/services')()
const ProductsLazyImport = createFileRoute('/products')()
const MarketplacesLazyImport = createFileRoute('/marketplaces')()
const LoginLazyImport = createFileRoute('/login')()
const LeadsLazyImport = createFileRoute('/leads')()
const ClientsLazyImport = createFileRoute('/clients')()
@@ -52,6 +53,11 @@ const ProductsLazyRoute = ProductsLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/products.lazy').then((d) => d.Route))
const MarketplacesLazyRoute = MarketplacesLazyImport.update({
path: '/marketplaces',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/marketplaces.lazy').then((d) => d.Route))
const LoginLazyRoute = LoginLazyImport.update({
path: '/login',
getParentRoute: () => rootRoute,
@@ -133,6 +139,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginLazyImport
parentRoute: typeof rootRoute
}
'/marketplaces': {
id: '/marketplaces'
path: '/marketplaces'
fullPath: '/marketplaces'
preLoaderRoute: typeof MarketplacesLazyImport
parentRoute: typeof rootRoute
}
'/products': {
id: '/products'
path: '/products'
@@ -180,6 +193,7 @@ export const routeTree = rootRoute.addChildren({
ClientsLazyRoute,
LeadsLazyRoute,
LoginLazyRoute,
MarketplacesLazyRoute,
ProductsLazyRoute,
ServicesLazyRoute,
ShippingwarehousesLazyRoute,
@@ -201,6 +215,7 @@ export const routeTree = rootRoute.addChildren({
"/clients",
"/leads",
"/login",
"/marketplaces",
"/products",
"/services",
"/shipping_warehouses",
@@ -226,6 +241,9 @@ export const routeTree = rootRoute.addChildren({
"/login": {
"filePath": "login.lazy.tsx"
},
"/marketplaces": {
"filePath": "marketplaces.lazy.tsx"
},
"/products": {
"filePath": "products.lazy.tsx"
},

View File

@@ -0,0 +1,6 @@
import {createLazyFileRoute} from '@tanstack/react-router'
import {MarketplacesPage} from "../pages/MarketplacesPage";
export const Route = createLazyFileRoute('/marketplaces')({
component: MarketplacesPage
})

View File

@@ -0,0 +1,6 @@
export enum BaseMarketplaceType {
WILDBERRIES = 'wb',
OZON = 'ozon',
YANDEX_MARKET = 'ym'
}

View File

@@ -0,0 +1,7 @@
export enum TaskStatus {
PENDING = "PENDING",
STARTED = "STARTED",
RETRY = "RETRY",
FAILURE = "FAILURE",
SUCCESS = "SUCCESS",
}