This commit is contained in:
2024-03-04 04:15:58 +03:00
parent 0db252bb27
commit 659d76b694
18 changed files with 366 additions and 75 deletions

View File

@@ -12,12 +12,13 @@
},
"dependencies": {
"@hello-pangea/dnd": "^16.5.0",
"@mantine/core": "^7.5.3",
"@mantine/dates": "^7.5.3",
"@mantine/dropzone": "^7.5.3",
"@mantine/hooks": "^7.5.3",
"@mantine/modals": "^7.5.3",
"@mantine/notifications": "^7.5.3",
"@mantine/core": "^7.6.1",
"@mantine/dates": "^7.6.1",
"@mantine/dropzone": "^7.6.1",
"@mantine/form": "^7.6.1",
"@mantine/hooks": "^7.6.1",
"@mantine/modals": "^7.6.1",
"@mantine/notifications": "^7.6.1",
"@reduxjs/toolkit": "^2.2.1",
"@tabler/icons-react": "^2.47.0",
"@tanstack/react-query": "^5.22.2",
@@ -27,11 +28,13 @@
"axios": "^1.6.7",
"clsx": "^2.1.0",
"dayjs": "^1.11.10",
"mantine-form-zod-resolver": "^1.1.0",
"mantine-react-table": "^2.0.0-beta.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"reactflow": "^11.10.4"
"reactflow": "^11.10.4",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/react": "^18.2.56",

View File

@@ -9,7 +9,12 @@ export type { OpenAPIConfig } from './core/OpenAPI';
export type { AuthLoginRequest } from './models/AuthLoginRequest';
export type { AuthLoginResponse } from './models/AuthLoginResponse';
export type { DealChangeStatusRequest } from './models/DealChangeStatusRequest';
export type { DealChangeStatusResponse } from './models/DealChangeStatusResponse';
export type { DealCreateRequest } from './models/DealCreateRequest';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { ValidationError } from './models/ValidationError';
export { AuthService } from './services/AuthService';
export { ClientService } from './services/ClientService';
export { DealService } from './services/DealService';

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealChangeStatusRequest = {
deal_id: number;
new_status: number;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealChangeStatusResponse = {
ok: boolean;
};

View File

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

View File

@@ -0,0 +1,30 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class ClientService {
/**
* Search Clients
* @returns any Successful Response
* @throws ApiError
*/
public static searchClients({
name,
}: {
name: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/client/search',
query: {
'name': name,
},
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -0,0 +1,52 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { DealChangeStatusRequest } from '../models/DealChangeStatusRequest';
import type { DealChangeStatusResponse } from '../models/DealChangeStatusResponse';
import type { DealCreateRequest } from '../models/DealCreateRequest';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class DealService {
/**
* Create
* @returns any Successful Response
* @throws ApiError
*/
public static createDealCreatePost({
requestBody,
}: {
requestBody: DealCreateRequest,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/create',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Change Status
* @returns DealChangeStatusResponse Successful Response
* @throws ApiError
*/
public static changeStatusDealChangeStatusPost({
requestBody,
}: {
requestBody: DealChangeStatusRequest,
}): CancelablePromise<DealChangeStatusResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/changeStatus',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -2,7 +2,7 @@ import {FC} from "react";
import styles from './Board.module.css';
import {Divider, Text, Title} from '@mantine/core';
import {Draggable, Droppable} from "@hello-pangea/dnd";
import CreateLeadButton from "../CreateLeadButton/CreateLeadButton.tsx";
import CreateDealButton from "../CreateDealButton/CreateDealButton.tsx";
type Props = {
droppableId: string;
@@ -21,16 +21,16 @@ export const Board: FC<Props> = ({droppableId, title, withCreateButton = false})
<Divider size={"xl"} my={10} color={"blue"}/>
</div>
<Droppable droppableId={droppableId}>
{(provided, snapshot) => (
{(provided) => (
<div ref={provided.innerRef} className={styles["items-list"]}>
{withCreateButton &&
<CreateLeadButton
<CreateDealButton
onClick={() => {
}}
/>}
<Draggable draggableId={droppableId + '1'} index={1}>
{(provided, snapshot) => (
{(provided) => (
<div {...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}

View File

@@ -0,0 +1,31 @@
import {FC, useState} from "react";
import styles from './CreateDealButton.module.css';
import {Button, rem, Text, TextInput, Transition} from '@mantine/core';
import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx";
type Props = {
onClick: () => void;
}
const CreateDealButton: FC<Props> = ({onClick}) => {
const [isCreating, setIsCreating] = useState(false);
return (
<div className={styles['container']}
onClick={() => {
if (isCreating) return;
setIsCreating(prevState => !prevState)
}}
>
<Text>Быстрое добавление</Text>
<CreateDealFrom
onCancel={() => {
}}
onSubmit={() => {
}}
/>
</div>
)
}
export default CreateDealButton;

View File

@@ -0,0 +1,15 @@
.inputs {
display: flex;
flex-direction: column;
width: 100%
}
.inputs * {
width: 100%;
}
.buttons {
display: flex;
gap: rem(10);
}

View File

@@ -0,0 +1,83 @@
import {Button, rem, Textarea, TextInput} from "@mantine/core";
import {QuickDeal} from "../../../types/QuickDeal.ts";
import {FC} from "react";
import {useForm} from "@mantine/form";
import styles from './CreateDealForm.module.css';
import ClientSelect from "../../Selects/ClientSelect/ClientSelect.tsx";
import {DateTimePicker} from "@mantine/dates";
type Props = {
onSubmit: (quickDeal: QuickDeal) => void
onCancel: () => void;
}
const CreateDealFrom: FC<Props> = ({onSubmit, onCancel}) => {
const form = useForm({
initialValues: {
name: '',
clientName: '',
clientAddress: '',
comment: '',
acceptance_date: new Date()
}
});
return (
<form
style={{width: '100%'}}
onSubmit={form.onSubmit((values) => console.log(values))}
>
<div style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: rem(10),
padding: rem(10)
}}>
<div className={styles['inputs']}>
<TextInput
placeholder={'Название'}
{...form.getInputProps('name')}
/>
</div>
<div className={styles['inputs']}>
<ClientSelect
withAddress
nameRestProps={form.getInputProps('clientName')}
addressRestProps={form.getInputProps('clientAddress')}
onSelect={() => {
}}/>
</div>
<div className={styles['inputs']}>
<Textarea
autosize
placeholder={'Комментарий'}
minRows={2}
maxRows={4}
{...form.getInputProps('comment')}
/>
</div>
<div className={styles['inputs']}>
<DateTimePicker
placeholder={'Дата приемки'}
/>
</div>
<div className={styles['buttons']}>
<Button
type={'submit'}
>Добавить</Button>
<Button
variant={'outline'}
onClick={() => onCancel()}
>Отменить</Button>
</div>
</div>
</form>
)
}
export default CreateDealFrom;

View File

@@ -1,60 +0,0 @@
import React, {FC, useState} from "react";
import styles from './CreateLeadButton.module.css';
import {Button, Center, rem, Text, TextInput, Transition} from '@mantine/core';
type Props = {
onClick: () => void;
}
const CreateLeadButton: FC<Props> = ({onClick}) => {
const [isCreating, setIsCreating] = useState(false);
const [showButton, setShowButton] = useState(true);
console.log(`isCreating: ${isCreating}`)
console.log(`showButton: ${showButton}`)
return (
<div className={styles['container']}
onClick={() => {
if (isCreating) return;
setIsCreating(prevState => !prevState)
setShowButton(false);
}}
>
{(!isCreating && showButton) &&
<Text>Быстрое добавление</Text>
}
<Transition
mounted={isCreating}
transition={'scale-y'}
duration={300}
// onExited={()=>setShowButton(true)}
keepMounted
>
{(styles) => <div style={{...styles, width: '100%'}}>
<div style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: rem(10),
padding: rem(10)
}}>
<div style={{display: "flex", flexDirection: "column", width: '100%'}}>
<TextInput placeholder={'Название'} w={'100%'}/>
</div>
<div style={{display: "flex", flexDirection: "column", width: '100%'}}>
<TextInput placeholder={'Компания: название'} w={'100%'}/>
<TextInput placeholder={'Компания: адрес'} w={'100%'}/>
</div>
<div style={{gap:rem(10), display:"flex"}}>
<Button>Добавить</Button>
<Button variant={'outline'} onClick={()=>setIsCreating(false)}>Отменить</Button>
</div>
</div>
</div>}
</Transition>
</div>
)
}
export default CreateLeadButton;

View File

@@ -0,0 +1,87 @@
import {useDebouncedValue} from "@mantine/hooks";
import {Autocomplete, AutocompleteProps, TextInput, TextInputProps} from "@mantine/core";
import {FC, useEffect, useState} from "react";
import {Client} from "../../../types/Client.ts";
import {ClientService} from "../../../client";
type Props = {
onSelect?: (client: Client) => void;
withAddress?: boolean;
nameRestProps?: AutocompleteProps;
addressRestProps?: TextInputProps;
}
const ClientSelect: FC<Props> = ({onSelect, addressRestProps, nameRestProps, withAddress = false}) => {
const [value, setValue] = useState('');
const [debouncedValue] = useDebouncedValue(value, 200);
// const [isLoading, setIsLoading] = useState(false);
const [clients, setClients] = useState<Client[]>([])
const [selectedClient, selectClient] = useState<Client>();
const handleChange = (value: string) => {
setClients([]);
setValue(value);
}
const handleDebouncedChange = () => {
if (!value.trim()) return;
// setIsLoading(true);
ClientService.searchClients({name: value}).then(({clients}) => {
setClients(clients);
// setIsLoading(false);
})
}
useEffect(() => {
handleDebouncedChange();
}, [debouncedValue]);
useEffect(() => {
selectClient(clients.find(client =>
client.name.toLowerCase().trim() == value.toLowerCase().trim())
||
{
name: value,
id: -1,
address: ''
});
}, [value]);
useEffect(() => {
if (!selectedClient) return;
if (onSelect) onSelect(selectedClient);
if (nameRestProps?.onChange) nameRestProps.onChange(selectedClient.name);
if (addressRestProps?.onChange) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
addressRestProps.onChange(selectedClient.address);
}
}, [selectedClient]);
return (
<>
<Autocomplete
{...nameRestProps}
placeholder={'Клиент: название'}
onChange={handleChange}
value={value}
data={clients.map(client => client.name)}
styles={withAddress ? {
input: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
}
} : {}}
/>
{withAddress &&
<TextInput
placeholder={'Клиент: адрес'}
value={selectedClient?.address}
onChange={event => {
selectClient(prevState => prevState && {...prevState, address: event.target.value})
}}
/>
}
</>
)
}
export default ClientSelect;

View File

@@ -1,4 +1,3 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import {RouterProvider, createRouter} from '@tanstack/react-router'
import {routeTree} from './routeTree.gen'
@@ -9,11 +8,14 @@ import {store} from "./redux/store.ts";
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import '@mantine/dates/styles.css';
import 'dayjs/locale/ru';
import './main.css';
import {Notifications} from "@mantine/notifications";
import {ModalsProvider} from "@mantine/modals";
import {OpenAPI} from "./client";
import PageWrapper from "./pages/PageWrapper/PageWrapper.tsx";
import {DatesProvider} from "@mantine/dates";
// Configuring router
const router = createRouter({routeTree})
@@ -35,10 +37,12 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<MantineProvider defaultColorScheme={"dark"}>
<ModalsProvider>
<DatesProvider settings={{locale: 'ru'}}>
<RouterProvider router={router}/>
<Notifications/>
</DatesProvider>
<Notifications/>
</ModalsProvider>
</MantineProvider>
</QueryClientProvider>

View File

@@ -3,12 +3,15 @@ import styles from './LeadsPage.module.css';
import Board from "../../../components/Dnd/Board/Board.tsx";
import {Button, TextInput} from "@mantine/core";
import {DragDropContext} from "@hello-pangea/dnd";
import ClientSelect from "../../../components/Selects/ClientSelect/ClientSelect.tsx";
import {DateTimePicker} from "@mantine/dates";
export const LeadsPage: FC = () => {
return (
<div className={styles['container']}>
<div className={styles['header']}>
<TextInput
radius={0}
placeholder={"Поиск и фильтры"}
@@ -18,7 +21,7 @@ export const LeadsPage: FC = () => {
<Button
radius={0}
color={"gray"}
variant={"light"}
variant={'default'}
className={styles['header-button']}
>Поиск</Button>
</div>

5
src/types/Client.ts Normal file
View File

@@ -0,0 +1,5 @@
export type Client = {
id?: number;
name: string;
address: string;
}

8
src/types/QuickDeal.ts Normal file
View File

@@ -0,0 +1,8 @@
import {Client} from "./Client.ts";
export type QuickDeal = {
name: string;
client: Client
comment: string;
acceptance_date: string;
}