feat: deals table

This commit is contained in:
2024-08-20 19:58:00 +03:00
parent 191f8ee972
commit a96b99eb01
9 changed files with 290 additions and 159 deletions

View File

@@ -10,7 +10,6 @@ import {
IconMan, IconMan,
IconMoon, IconMoon,
IconSun, IconSun,
IconTable,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import classes from './Navbar.module.css'; import classes from './Navbar.module.css';
import {useAppDispatch} from "../../redux/store.ts"; import {useAppDispatch} from "../../redux/store.ts";
@@ -48,11 +47,11 @@ const mockdata = [
label: 'Сделки', label: 'Сделки',
href: '/leads' href: '/leads'
}, },
{ // {
icon: IconTable, // icon: IconTable,
label: 'Таблица сделок', // label: 'Таблица сделок',
href: '/deals' // href: '/deals'
}, // },
{ {
icon: IconMan, icon: IconMan,
label: 'Клиенты', label: 'Клиенты',
@@ -104,6 +103,7 @@ export function Navbar() {
p={rem(5)} p={rem(5)}
> >
<Image <Image
flex={1}
// style={{filter: "drop-shadow(0 0 30px #fff)"}} // style={{filter: "drop-shadow(0 0 30px #fff)"}}
src={colorScheme == "dark" ? "/icons/logo-light.png" : "/icons/logo.png"} src={colorScheme == "dark" ? "/icons/logo-light.png" : "/icons/logo.png"}
/> />

View File

@@ -19,5 +19,9 @@
.container-full-height { .container-full-height {
min-height: calc(100vh - (rem(20) * 2)); min-height: calc(100vh - (rem(20) * 2));
}
.container-full-height-fixed {
height: calc(100vh - (rem(20) * 2));
} }

View File

@@ -7,11 +7,19 @@ type Props = {
fluid?: boolean; fluid?: boolean;
style?: CSSProperties; style?: CSSProperties;
fullHeight?: boolean; fullHeight?: boolean;
fullHeightFixed?: boolean;
} }
export const PageBlock: FC<Props> = ({children, style, fluid = true, fullHeight = false}) => { export const PageBlock: FC<Props> = ({children, style, fluid = true, fullHeight = false, fullHeightFixed = false}) => {
return ( return (
<div style={style} <div style={style}
className={classNames(styles['container'], fluid && styles['container-fluid'], fullHeight && styles['container-full-height'])}> className={
classNames(
styles['container'],
fluid && styles['container-fluid'],
fullHeight && styles['container-full-height'],
fullHeightFixed && styles['container-full-height-fixed']
)
}>
{children} {children}
</div> </div>
) )

View File

@@ -0,0 +1,42 @@
import {useDealSummariesFull} from "../../LeadsPage/hooks/useDealSummaries.tsx";
import {useForm} from "@mantine/form";
import {useEffect, useState} from "react";
import {BaseMarketplaceSchema, ClientSchema} from "../../../client";
import {DealStatusType} from "../../../shared/enums/DealStatus.ts";
type State = {
marketplace: BaseMarketplaceSchema | null;
dealStatus: DealStatusType | null;
client: ClientSchema | null;
}
const useDealsPageState = () => {
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 {data, form}
}
export default useDealsPageState;

View File

@@ -1,50 +1,16 @@
import {FC, useEffect, useState} from "react"; import {FC} from "react";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx"; import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import styles from './DealsPage.module.css'; import styles from './DealsPage.module.css';
import DealStatusSelect from "../components/DealStatusSelect/DealStatusSelect.tsx"; import DealStatusSelect from "../components/DealStatusSelect/DealStatusSelect.tsx";
import DealsTable from "../components/DealsTable/DealsTable.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 BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import {useForm} from "@mantine/form";
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx"; import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import {DealPageContextProvider} from "../../LeadsPage/contexts/DealPageContext.tsx"; import {DealPageContextProvider} from "../../LeadsPage/contexts/DealPageContext.tsx";
import DealEditDrawer from "../../LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx"; import DealEditDrawer from "../../LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx";
import useDealsPageState from "../hooks/useDealsPageState.tsx";
type State = {
marketplace: BaseMarketplaceSchema | null;
dealStatus: DealStatusType | null;
client: ClientSchema | null;
}
export const DealsPage: FC = () => { export const DealsPage: FC = () => {
const {objects} = useDealSummariesFull(); const {data, form} = useDealsPageState();
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 ( return (
<> <>
<DealPageContextProvider> <DealPageContextProvider>

View File

@@ -5,7 +5,6 @@
height: 100%; height: 100%;
} }
.search-input { .search-input {
} }
@@ -37,5 +36,10 @@
.delete-hidden { .delete-hidden {
border: none; border: none;
}
.top-panel {
padding: rem(5);
gap: rem(10);
display: flex;
} }

View File

@@ -9,14 +9,28 @@ import DealEditDrawer from "../drawers/DealEditDrawer/DealEditDrawer.tsx";
import {DealPageContextProvider} from "../contexts/DealPageContext.tsx"; import {DealPageContextProvider} from "../contexts/DealPageContext.tsx";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
import {DealService, DealSummaryReorderRequest} from "../../../client"; import {DealService, DealSummaryReorderRequest} from "../../../client";
import {Flex} from "@mantine/core"; import {ActionIcon, Flex, rem, Text} from "@mantine/core";
import classNames from "classnames"; import classNames from "classnames";
import {notifications} from "../../../shared/lib/notifications.ts"; import {notifications} from "../../../shared/lib/notifications.ts";
import {IconMenu2, IconMenuDeep} from "@tabler/icons-react";
import useDealsPageState from "../../DealsPage/hooks/useDealsPageState.tsx";
import DealStatusSelect from "../../DealsPage/components/DealStatusSelect/DealStatusSelect.tsx";
import BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import DealsTable from "../../DealsPage/components/DealsTable/DealsTable.tsx";
import {motion} from "framer-motion";
enum DisplayMode {
BOARD,
TABLE
}
export const LeadsPage: FC = () => { export const LeadsPage: FC = () => {
const {data, form} = useDealsPageState();
const {summariesRaw, refetch} = useDealSummaries(); const {summariesRaw, refetch} = useDealSummaries();
const [summaries, setSummaries] = useState(summariesRaw); const [summaries, setSummaries] = useState(summariesRaw);
const [displayMode, setDisplayMode] = useState<DisplayMode>(DisplayMode.BOARD);
const [isDragEnded, setIsDragEnded] = useState(true); const [isDragEnded, setIsDragEnded] = useState(true);
useEffect(() => { useEffect(() => {
setSummaries(summariesRaw); setSummaries(summariesRaw);
@@ -92,99 +106,218 @@ export const LeadsPage: FC = () => {
}); });
} }
return ( const getTableBody = () => {
<> return (
<DealPageContextProvider> <motion.div
key={displayMode}
<PageBlock fullHeight style={{ initial={{opacity: 0}}
display: "flex", animate={{opacity: 1}}
flexDirection: "column" transition={{duration: 0.2}}
}}> >
<DragDropContext
onDragStart={() => {
setIsDragEnded(false);
}}
onDragEnd={onDragEnd}>
<div className={styles['boards']}>
<Board
withCreateButton
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_ACCEPTANCE)}
title={"Ожидает приемки"}
droppableId={"AWAITING_ACCEPTANCE"}
color={'#4A90E2'}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.PACKAGING)}
title={"Упаковка"}
droppableId={"PACKAGING"}
color={'#F5A623'}
/> <DealsTable items={data}/>
<Board </motion.div>
summaries={summaries )
.filter(summary => summary.status == DealStatus.AWAITING_SHIPMENT)} }
title={"Ожидает отгрузки"} const getBoardBody = () => {
droppableId={"AWAITING_SHIPMENT"} return (
color={'#7ED321'} <motion.div
key={displayMode}
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{duration: 0.2}}
>
/> <DragDropContext
<Board onDragStart={() => {
summaries={summaries setIsDragEnded(false);
.filter(summary => summary.status == DealStatus.AWAITING_PAYMENT)} }}
title={"Ожидает оплаты"} onDragEnd={onDragEnd}>
droppableId={"AWAITING_PAYMENT"} <div className={styles['boards']}>
color={'#D0021B'} <Board
withCreateButton
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_ACCEPTANCE)}
title={"Ожидает приемки"}
droppableId={"AWAITING_ACCEPTANCE"}
color={'#4A90E2'}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.PACKAGING)}
title={"Упаковка"}
droppableId={"PACKAGING"}
color={'#F5A623'}
/> />
<Board <Board
summaries={summaries summaries={summaries
.filter(summary => summary.status == DealStatus.COMPLETED)} .filter(summary => summary.status == DealStatus.AWAITING_SHIPMENT)}
title={"Завершена"} title={"Ожидает отгрузки"}
droppableId={"COMPLETED"} droppableId={"AWAITING_SHIPMENT"}
color={'#417505'} color={'#7ED321'}
/>
</div> />
<Flex justify={"flex-end"}> <Board
<div summaries={summaries
className={ .filter(summary => summary.status == DealStatus.AWAITING_PAYMENT)}
classNames( title={"Ожидает оплаты"}
styles['delete'], droppableId={"AWAITING_PAYMENT"}
isDragEnded && styles['delete-hidden'] color={'#D0021B'}
)
} />
> <Board
<Droppable droppableId={"DELETE"}> summaries={summaries
{(provided) => ( .filter(summary => summary.status == DealStatus.COMPLETED)}
<> title={"Завершена"}
<div droppableId={"COMPLETED"}
{...provided.droppableProps} color={'#417505'}
ref={provided.innerRef} />
> </div>
{!isDragEnded && <Flex justify={"flex-end"}>
<span> <div
className={
classNames(
styles['delete'],
isDragEnded && styles['delete-hidden']
)
}
>
<Droppable droppableId={"DELETE"}>
{(provided) => (
<>
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
{!isDragEnded &&
<span>
Удалить Удалить
</span> </span>
} }
</div> </div>
{provided.placeholder} {provided.placeholder}
</> </>
)} )}
</Droppable> </Droppable>
</div> </div>
</Flex>
</DragDropContext>
</motion.div>
)
}
const getBody = () => {
return displayMode === DisplayMode.TABLE ? getTableBody() : getBoardBody();
}
return (
<PageBlock
fullHeight
style={{
gap: rem(10),
display: "flex",
flexDirection: "column",
backgroundColor: "transparent",
boxShadow: "none",
}}
>
<DealPageContextProvider>
<PageBlock
style={{flex: 0}}
>
<Flex
align={"center"}
justify={"space-between"}
>
<Flex
gap={rem(10)}
direction={"column"}
align={"center"}
>
<Text size={"xs"}>Вид</Text>
<Flex gap={rem(10)}>
<ActionIcon
onClick={() => setDisplayMode(DisplayMode.BOARD)}
variant={
displayMode === DisplayMode.BOARD ?
"filled"
:
"default"
}>
<IconMenuDeep
style={{rotate: "-90deg"}}
/>
</ActionIcon>
<ActionIcon
onClick={() => setDisplayMode(DisplayMode.TABLE)}
variant={
displayMode === DisplayMode.TABLE ?
"filled"
:
"default"
}
>
<IconMenu2
/>
</ActionIcon>
</Flex>
</Flex> </Flex>
</DragDropContext> <motion.div
key={displayMode}
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{duration: 0.2}}
>
<div
className={styles['top-panel']}
style={{display: displayMode === DisplayMode.TABLE ? "flex" : "none"}}
>
<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>
</motion.div>
</Flex>
</PageBlock>
<PageBlock
style={{
display: "flex",
flexDirection: "column",
flex: 1,
height: "100%"
}}>
{getBody()}
</PageBlock> </PageBlock>
<DealEditDrawer <DealEditDrawer
/> />
</DealPageContextProvider> </DealPageContextProvider>
</> </PageBlock>
) )
} }

View File

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

View File

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