feat: deals table

This commit is contained in:
2024-08-10 06:53:45 +03:00
parent be9c8e604f
commit bb4324822a
15 changed files with 327 additions and 18 deletions

View File

@@ -8,6 +8,7 @@ export type DealSummary = {
name: string;
clientName: string;
changedAt: string;
createdAt: string;
deadline: string;
status: number;
totalPrice: number;

View File

@@ -134,10 +134,20 @@ export class DealService {
* @returns DealSummaryResponse Successful Response
* @throws ApiError
*/
public static getDealSummaries(): CancelablePromise<DealSummaryResponse> {
public static getDealSummaries({
full,
}: {
full: (boolean | null),
}): CancelablePromise<DealSummaryResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/deal/summaries',
query: {
'full': full,
},
errors: {
422: `Validation Error`,
},
});
}
/**

View File

@@ -2,13 +2,15 @@ import {Center, Flex, Image, rem, Stack, Tooltip, UnstyledButton, useMantineColo
import {
IconBarcode,
IconBox,
IconCash, IconDashboard,
IconCash,
IconDashboard,
IconFileBarcode,
IconHome2,
IconLogout,
IconMan,
IconMoon,
IconSun,
IconTable,
} from '@tabler/icons-react';
import classes from './Navbar.module.css';
import {useAppDispatch} from "../../redux/store.ts";
@@ -40,16 +42,17 @@ function NavbarLink(props: NavbarLinkProps) {
}
const mockdata = [
// {
// icon: IconHome2,
// label: 'Главная',
// href: '/'
// },
{
icon: IconCash,
label: 'Сделки',
href: '/leads'
},
{
icon: IconTable,
label: 'Таблица сделок',
href: '/deals'
},
{
icon: IconMan,
label: 'Клиенты',

View File

@@ -26,10 +26,12 @@ type RestProps<T> = {
filterBy?: (item: SelectObjectType<T>) => boolean;
};
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
return item.name;
}
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
if (!item) return item;
return item.id.toString();
}
export type ObjectSelectProps<T> =
@@ -85,6 +87,7 @@ const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
if (isControlled || !internalValue) return;
props.onChange(internalValue);
}, [internalValue]);
const restProps = omit(props, ['filterBy', 'groupBy', 'getValueFn', 'getLabelFn']);
return (
<Select

View File

@@ -0,0 +1,17 @@
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
import {ClientSchema} from "../../../client";
import {FC} from "react";
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
type Props = Omit<ObjectSelectProps<ClientSchema>, 'data'>
const ClientSelectNew: FC<Props> = (props) => {
const {clients} = useClientsList();
return (
<ObjectSelect
data={clients}
{...props}
/>
)
}
export default ClientSelectNew;

View File

@@ -0,0 +1,20 @@
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
import {FC} from "react";
import {DealStatuses} from "../../../../shared/enums/DealStatus.ts";
type DealStatus = {
name: string;
id: number
}
type Props = Omit<ObjectSelectProps<DealStatus>, 'data'>;
const DealStatusSelect: FC<Props> = (props) => {
const data: DealStatus[] = DealStatuses;
return (
<ObjectSelect
data={data}
{...props}
/>
)
}
export default DealStatusSelect;

View File

@@ -0,0 +1,49 @@
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {DealService, DealSummary} from "../../../../client";
import {FC} from "react";
import useDealsTableColumns from "./columns.tsx";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import {ActionIcon, Flex, Tooltip} from "@mantine/core";
import {IconEdit} from "@tabler/icons-react";
import {MRT_TableOptions} from "mantine-react-table";
import {useDealPageContext} from "../../../LeadsPage/contexts/DealPageContext.tsx";
type Props = CRUDTableProps<DealSummary>;
const DealsTable: FC<Props> = ({items}) => {
const columns = useDealsTableColumns();
const {setSelectedDeal} = useDealPageContext();
const onEditClick = (dealSummary: DealSummary) => {
DealService.getDealById({dealId: dealSummary.id})
.then((deal) => {
setSelectedDeal(deal);
})
}
return (
<BaseTable
data={items}
columns={columns}
restProps={{
enableSorting: true,
enableColumnActions: false,
enablePagination: true,
enableBottomToolbar: true,
paginationDisplayMode: "pages",
enableRowActions: true,
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onEditClick(row.original)}
variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
</Flex>
)
} as MRT_TableOptions<DealSummary>}
/>
)
}
export default DealsTable;

View File

@@ -0,0 +1,50 @@
import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table";
import {DealSummary} from "../../../../client";
import {ActionIcon, Image} from "@mantine/core";
const useDealsTableColumns = () => {
return useMemo<MRT_ColumnDef<DealSummary>[]>(() => [
{
header: "Маркетплейс",
size: 10,
Cell: ({row}) => (
<ActionIcon variant={"transparent"}>
<Image src={row.original.baseMarketplace?.iconUrl || ""}/>
</ActionIcon>
)
},
{
header: "Дата создания",
accessorKey: "createdAt",
Cell: ({row}) => (new Date(row.original.createdAt)).toLocaleString("ru-RU"),
enableSorting: true,
sortingFn: (rowA, rowB) => ((new Date(rowB.original.createdAt)).getTime() - (new Date(rowA.original.createdAt)).getTime())
},
{
accessorKey: "name",
header: "Название",
enableSorting: false
},
{
accessorKey: "clientName",
header: "Клиент",
enableSorting: false
},
{
Cell: ({row}) => (new Date(row.original.deadline)).toLocaleString("ru-RU"),
accessorKey: "deadline",
header: "Дедлайн",
sortingFn: (rowA, rowB) => ((new Date(rowB.original.deadline)).getTime() - (new Date(rowA.original.deadline)).getTime())
},
{
header: "Общая стоимость",
Cell: ({row}) => (row.original.totalPrice).toLocaleString("ru-RU") + '₽',
accessorKey:"totalPrice"
}
], [])
}
export default useDealsTableColumns;

View File

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

View File

@@ -0,0 +1,27 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
gap: rem(10);
}
.body-container {
}
.top-panel {
padding: rem(5);
gap: rem(10);
display: flex;
}
.table-container {
display: flex;
gap: rem(10);
flex-direction: column;
}
.table-pagination {
align-self: flex-end;
}

View File

@@ -0,0 +1,89 @@
import {FC, useEffect, useState} from "react";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import styles from './DealsPage.module.css';
import DealStatusSelect from "../components/DealStatusSelect/DealStatusSelect.tsx";
import DealsTable from "../components/DealsTable/DealsTable.tsx";
import {useDealSummariesFull} from "../../LeadsPage/hooks/useDealSummaries.tsx";
import {DealStatusType} from "../../../shared/enums/DealStatus.ts";
import {BaseMarketplaceSchema, ClientSchema} from "../../../client";
import BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import {useForm} from "@mantine/form";
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import {DealPageContextProvider} from "../../LeadsPage/contexts/DealPageContext.tsx";
import DealEditDrawer from "../../LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx";
type State = {
marketplace: BaseMarketplaceSchema | null;
dealStatus: DealStatusType | null;
client: ClientSchema | null;
}
export const DealsPage: FC = () => {
const {objects} = useDealSummariesFull();
const form = useForm<State>({
initialValues: {
marketplace: null,
dealStatus: null,
client: null
}
});
const [data, setData] = useState(objects);
const applyFilters = () => {
let result = objects;
if (form.values.marketplace) {
result = result.filter(obj => obj.baseMarketplace?.key === form.values.marketplace?.key);
}
if (form.values.dealStatus) {
result = result.filter(obj => obj.status === form.values.dealStatus?.id);
}
if (form.values.client) {
result = result.filter(obj => obj.clientName === form.values.client?.name);
}
setData(result);
}
useEffect(() => {
applyFilters();
}, [form.values, objects])
return (
<>
<DealPageContextProvider>
<div className={styles['container']}>
<PageBlock>
<div className={styles['top-panel']}>
<DealStatusSelect
onClear={() => form.setFieldValue("dealStatus", null)}
clearable
placeholder={"Выберите статус "}
{...form.getInputProps("dealStatus")}
/>
<BaseMarketplaceSelect
onClear={() => form.setFieldValue("marketplace", null)}
clearable
placeholder={"Выберите маркетплейс"}
{...form.getInputProps("marketplace")}
/>
<ClientSelectNew
onClear={() => form.setFieldValue("client", null)}
clearable
searchable
placeholder={"Выберите клиента"}
{...form.getInputProps("client")}
/>
</div>
</PageBlock>
<PageBlock>
<div className={styles['body-container']}>
<div className={styles['table-container']}>
<DealsTable items={data}/>
</div>
</div>
</PageBlock>
</div>
<DealEditDrawer
/>
</DealPageContextProvider>
</>
)
}

View File

@@ -1,10 +1,11 @@
import {useQuery} from "@tanstack/react-query";
import {DealService} from "../../../client";
import ObjectList from "../../../hooks/objectList.tsx";
export const useDealSummaries = () => {
const {data: summariesRaw = [], refetch} = useQuery({
queryKey: ['getDealSummaries'],
queryFn: DealService.getDealSummaries,
queryFn: () => DealService.getDealSummaries({full: false}),
select: data => data.summaries || [] // Трансформируем полученные данные
});
@@ -13,3 +14,8 @@ export const useDealSummaries = () => {
return {summariesRaw, refetch};
}
export const useDealSummariesFull = () => ObjectList({
queryFn: () => DealService.getDealSummaries({full: true}),
queryKey: "getDealSummariesFull",
getObjectsFn: response => response.summaries
});

View File

@@ -22,6 +22,7 @@ const ServicesLazyImport = createFileRoute('/services')()
const ProductsLazyImport = createFileRoute('/products')()
const LoginLazyImport = createFileRoute('/login')()
const LeadsLazyImport = createFileRoute('/leads')()
const DealsLazyImport = createFileRoute('/deals')()
const ClientsLazyImport = createFileRoute('/clients')()
const BarcodeLazyImport = createFileRoute('/barcode')()
const AdminLazyImport = createFileRoute('/admin')()
@@ -54,6 +55,11 @@ const LeadsLazyRoute = LeadsLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/leads.lazy').then((d) => d.Route))
const DealsLazyRoute = DealsLazyImport.update({
path: '/deals',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/deals.lazy').then((d) => d.Route))
const ClientsLazyRoute = ClientsLazyImport.update({
path: '/clients',
getParentRoute: () => rootRoute,
@@ -75,8 +81,8 @@ const IndexLazyRoute = IndexLazyImport.update({
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
const DealsDealIdRoute = DealsDealIdImport.update({
path: '/deals/$dealId',
getParentRoute: () => rootRoute,
path: '/$dealId',
getParentRoute: () => DealsLazyRoute,
} as any)
// Populate the FileRoutesByPath interface
@@ -111,6 +117,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ClientsLazyImport
parentRoute: typeof rootRoute
}
'/deals': {
id: '/deals'
path: '/deals'
fullPath: '/deals'
preLoaderRoute: typeof DealsLazyImport
parentRoute: typeof rootRoute
}
'/leads': {
id: '/leads'
path: '/leads'
@@ -148,10 +161,10 @@ declare module '@tanstack/react-router' {
}
'/deals/$dealId': {
id: '/deals/$dealId'
path: '/deals/$dealId'
path: '/$dealId'
fullPath: '/deals/$dealId'
preLoaderRoute: typeof DealsDealIdImport
parentRoute: typeof rootRoute
parentRoute: typeof DealsLazyImport
}
}
}
@@ -163,12 +176,12 @@ export const routeTree = rootRoute.addChildren({
AdminLazyRoute,
BarcodeLazyRoute,
ClientsLazyRoute,
DealsLazyRoute: DealsLazyRoute.addChildren({ DealsDealIdRoute }),
LeadsLazyRoute,
LoginLazyRoute,
ProductsLazyRoute,
ServicesLazyRoute,
TestLazyRoute,
DealsDealIdRoute,
})
/* prettier-ignore-end */
@@ -183,12 +196,12 @@ export const routeTree = rootRoute.addChildren({
"/admin",
"/barcode",
"/clients",
"/deals",
"/leads",
"/login",
"/products",
"/services",
"/test",
"/deals/$dealId"
"/test"
]
},
"/": {
@@ -203,6 +216,12 @@ export const routeTree = rootRoute.addChildren({
"/clients": {
"filePath": "clients.lazy.tsx"
},
"/deals": {
"filePath": "deals.lazy.tsx",
"children": [
"/deals/$dealId"
]
},
"/leads": {
"filePath": "leads.lazy.tsx"
},
@@ -219,7 +238,8 @@ export const routeTree = rootRoute.addChildren({
"filePath": "test.lazy.tsx"
},
"/deals/$dealId": {
"filePath": "deals.$dealId.tsx"
"filePath": "deals.$dealId.tsx",
"parent": "/deals"
}
}
}

View File

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

View File

@@ -20,3 +20,10 @@ export const DealStatusDictionary = {
[DealStatus.COMPLETED]: "Завершена",
[DealStatus.CANCELLED]: "Отменена",
}
export type DealStatusType = {
id: number,
name: string
}
export const DealStatuses: DealStatusType[] = Object.entries(DealStatusDictionary).map(([key, value]) => {
return {id: parseInt(key), name: value}
});