This commit is contained in:
2024-03-28 08:22:27 +03:00
parent c9f3d4ee12
commit 806e73bb5a
27 changed files with 432 additions and 92 deletions

View File

@@ -13,14 +13,21 @@ export type { ClientDetailsSchema } from './models/ClientDetailsSchema';
export type { ClientGetAllResponse } from './models/ClientGetAllResponse'; export type { ClientGetAllResponse } from './models/ClientGetAllResponse';
export type { ClientSchema } from './models/ClientSchema'; export type { ClientSchema } from './models/ClientSchema';
export type { ClientUpdateDetailsRequest } from './models/ClientUpdateDetailsRequest'; export type { ClientUpdateDetailsRequest } from './models/ClientUpdateDetailsRequest';
export type { DealAddServicesRequest } from './models/DealAddServicesRequest';
export type { DealAddServicesResponse } from './models/DealAddServicesResponse';
export type { DealChangeStatusRequest } from './models/DealChangeStatusRequest'; export type { DealChangeStatusRequest } from './models/DealChangeStatusRequest';
export type { DealChangeStatusResponse } from './models/DealChangeStatusResponse'; export type { DealChangeStatusResponse } from './models/DealChangeStatusResponse';
export type { DealCreateRequest } from './models/DealCreateRequest'; export type { DealCreateRequest } from './models/DealCreateRequest';
export type { DealQuickCreateRequest } from './models/DealQuickCreateRequest'; export type { DealQuickCreateRequest } from './models/DealQuickCreateRequest';
export type { DealQuickCreateResponse } from './models/DealQuickCreateResponse'; export type { DealQuickCreateResponse } from './models/DealQuickCreateResponse';
export type { DealServiceSchema } from './models/DealServiceSchema';
export type { DealSummary } from './models/DealSummary'; export type { DealSummary } from './models/DealSummary';
export type { DealSummaryResponse } from './models/DealSummaryResponse'; export type { DealSummaryResponse } from './models/DealSummaryResponse';
export type { HTTPValidationError } from './models/HTTPValidationError'; export type { HTTPValidationError } from './models/HTTPValidationError';
export type { ProductCreateRequest } from './models/ProductCreateRequest';
export type { ProductCreateResponse } from './models/ProductCreateResponse';
export type { ProductGetResponse } from './models/ProductGetResponse';
export type { ProductSchema } from './models/ProductSchema';
export type { ServiceCategorySchema } from './models/ServiceCategorySchema'; export type { ServiceCategorySchema } from './models/ServiceCategorySchema';
export type { ServiceCreateCategoryRequest } from './models/ServiceCreateCategoryRequest'; export type { ServiceCreateCategoryRequest } from './models/ServiceCreateCategoryRequest';
export type { ServiceCreateCategoryResponse } from './models/ServiceCreateCategoryResponse'; export type { ServiceCreateCategoryResponse } from './models/ServiceCreateCategoryResponse';
@@ -34,4 +41,5 @@ export type { ValidationError } from './models/ValidationError';
export { AuthService } from './services/AuthService'; export { AuthService } from './services/AuthService';
export { ClientService } from './services/ClientService'; export { ClientService } from './services/ClientService';
export { DealService } from './services/DealService'; export { DealService } from './services/DealService';
export { ProductService } from './services/ProductService';
export { ServiceService } from './services/ServiceService'; export { ServiceService } from './services/ServiceService';

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { DealServiceSchema } from './DealServiceSchema';
export type DealAddServicesRequest = {
deal_id: number;
services: Array<DealServiceSchema>;
};

View File

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

View File

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

View File

@@ -8,5 +8,6 @@ export type DealSummary = {
client_name: string; client_name: string;
changed_at: string; changed_at: string;
status: number; status: number;
total_price: number;
}; };

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ProductSchema = {
id: number;
name: string;
article: string;
client_id: number;
};

View File

@@ -2,6 +2,8 @@
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { DealAddServicesRequest } from '../models/DealAddServicesRequest';
import type { DealAddServicesResponse } from '../models/DealAddServicesResponse';
import type { DealChangeStatusRequest } from '../models/DealChangeStatusRequest'; import type { DealChangeStatusRequest } from '../models/DealChangeStatusRequest';
import type { DealChangeStatusResponse } from '../models/DealChangeStatusResponse'; import type { DealChangeStatusResponse } from '../models/DealChangeStatusResponse';
import type { DealCreateRequest } from '../models/DealCreateRequest'; import type { DealCreateRequest } from '../models/DealCreateRequest';
@@ -83,4 +85,24 @@ export class DealService {
url: '/deal/summaries', url: '/deal/summaries',
}); });
} }
/**
* Services Add
* @returns DealAddServicesResponse Successful Response
* @throws ApiError
*/
public static servicesAddDealServicesAddPost({
requestBody,
}: {
requestBody: DealAddServicesRequest,
}): CancelablePromise<DealAddServicesResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/services/add',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
} }

View File

@@ -0,0 +1,59 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ProductCreateRequest } from '../models/ProductCreateRequest';
import type { ProductCreateResponse } from '../models/ProductCreateResponse';
import type { ProductGetResponse } from '../models/ProductGetResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class ProductService {
/**
* Create Product
* @returns ProductCreateResponse Successful Response
* @throws ApiError
*/
public static createProductProductCreatePost({
requestBody,
}: {
requestBody: ProductCreateRequest,
}): CancelablePromise<ProductCreateResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/product/create',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Product
* @returns ProductGetResponse Successful Response
* @throws ApiError
*/
public static getProductsByClientId({
clientId,
page,
itemsPerPage,
}: {
clientId: number,
page: number,
itemsPerPage: number,
}): CancelablePromise<ProductGetResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/product/get',
query: {
'client_id': clientId,
'page': page,
'items_per_page': itemsPerPage,
},
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -3,7 +3,7 @@ import {QuickDeal} from "../../../types/QuickDeal.ts";
import {FC} from "react"; import {FC} from "react";
import {useForm} from "@mantine/form"; import {useForm} from "@mantine/form";
import styles from './CreateDealForm.module.css'; import styles from './CreateDealForm.module.css';
import ClientSelect from "../../Selects/ClientSelect/ClientSelect.tsx"; import ClientAutocomplete from "../../Selects/ClientAutocomplete/ClientAutocomplete.tsx";
import {DateTimePicker} from "@mantine/dates"; import {DateTimePicker} from "@mantine/dates";
type Props = { type Props = {
@@ -41,7 +41,7 @@ const CreateDealFrom: FC<Props> = ({onSubmit, onCancel}) => {
</div> </div>
<div className={styles['inputs']}> <div className={styles['inputs']}>
<ClientSelect <ClientAutocomplete
withAddress withAddress
nameRestProps={form.getInputProps('client_name')} nameRestProps={form.getInputProps('client_name')}
addressRestProps={form.getInputProps('client_address')} addressRestProps={form.getInputProps('client_address')}

View File

@@ -23,7 +23,7 @@ const DealSummaryCard: FC<Props> = ({dealSummary}) => {
</div> </div>
<div className={styles['flex-item']}> <div className={styles['flex-item']}>
<Text size={"sm"} c={"gray.6"}> <Text size={"sm"} c={"gray.6"}>
228 руб {dealSummary.total_price.toLocaleString('ru-RU')} руб
</Text> </Text>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import {Center, Image, rem, Stack, Tooltip, UnstyledButton, useMantineColorScheme} from '@mantine/core'; import {Center, Image, rem, Stack, Tooltip, UnstyledButton, useMantineColorScheme} from '@mantine/core';
import {IconBox, IconCash, IconHome2, IconLogout, IconMan, IconMoon, IconSun,} from '@tabler/icons-react'; import {IconBarcode, IconBox, IconCash, IconHome2, IconLogout, IconMan, IconMoon, IconSun,} 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";
import {logout} from "../../features/authSlice.ts"; import {logout} from "../../features/authSlice.ts";
@@ -50,6 +50,11 @@ const mockdata = [
label: 'Услуги', label: 'Услуги',
href: '/services' href: '/services'
}, },
{
icon: IconBarcode,
label: 'Товары',
href: '/products'
}
]; ];
export function Navbar() { export function Navbar() {

View File

@@ -2,5 +2,8 @@
border-radius: rem(20); border-radius: rem(20);
background-color: var(--mantine-color-body); background-color: var(--mantine-color-body);
padding: rem(10); padding: rem(10);
}
.container-fluid {
flex: 1; flex: 1;
} }

View File

@@ -1,12 +1,14 @@
import {FC, ReactNode} from "react"; import {FC, ReactNode} from "react";
import styles from './PageBlock.module.css'; import styles from './PageBlock.module.css';
import classNames from "classnames";
type Props = { type Props = {
children: ReactNode children: ReactNode
fluid?: boolean;
} }
export const PageBlock: FC<Props> = ({children}) => { export const PageBlock: FC<Props> = ({children, fluid = true}) => {
return ( return (
<div className={styles['container']}> <div className={classNames(styles['container'], fluid && styles['container-fluid'])}>
{children} {children}
</div> </div>
) )

View File

@@ -0,0 +1,93 @@
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 ClientAutocomplete: 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={'Клиент: адрес'}
styles={{
input: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
}
}}
value={selectedClient?.address || ''}
onChange={event => {
selectClient(prevState => prevState && {...prevState, address: event.target.value})
}}
/>
}
</>
)
}
export default ClientAutocomplete;

View File

@@ -1,93 +1,31 @@
import {useDebouncedValue} from "@mantine/hooks"; import {FC} from "react";
import {Autocomplete, AutocompleteProps, TextInput, TextInputProps} from "@mantine/core"; import {Select} from "@mantine/core";
import {FC, useEffect, useState} from "react"; import {ClientSchema} from "../../../client";
import {Client} from "../../../types/Client.ts"; import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
import {ClientService} from "../../../client";
type Props = { type Props = {
onSelect?: (client: Client) => void; value?: ClientSchema;
withAddress?: boolean; onChange: (client: ClientSchema) => void;
nameRestProps?: AutocompleteProps; withLabel?: boolean;
addressRestProps?: TextInputProps;
} }
const ClientSelect: FC<Props> = ({onSelect, addressRestProps, nameRestProps, withAddress = false}) => { const ClientSelect: FC<Props> = ({value, onChange, withLabel = false}) => {
const [value, setValue] = useState(''); const {clients} = useClientsList();
const [debouncedValue] = useDebouncedValue(value, 200); const options = clients.map(client => ({label: client.name, value: client.id.toString()}))
// 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 ( return (
<> <Select
<Autocomplete
{...nameRestProps}
placeholder={'Клиент: название'}
onChange={handleChange}
value={value}
data={clients.map(client => client.name)}
styles={withAddress ? {
input: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
}
} : {}}
/>
{withAddress && placeholder={"Выберите клиента"}
<TextInput value={value && options.find(client => client.value == value.id.toString())?.value}
placeholder={'Клиент: адрес'}
styles={{
input: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
}
}}
value={selectedClient?.address || ''}
onChange={event => { onChange={event => {
selectClient(prevState => prevState && {...prevState, address: event.target.value}) if (!event) return;
const client = clients.find(client => client.id == parseInt(event));
if (!client) return;
onChange(client);
}} }}
data={options}
label={withLabel && "Клиент"}
/> />
}
</>
) )
} }
export default ClientSelect; export default ClientSelect;

View File

@@ -10,5 +10,5 @@
.container { .container {
padding: rem(20); padding: rem(20);
display: flex; display: flex;
min-height: 100vh; //min-height: 100vh;
} }

View File

@@ -0,0 +1,29 @@
import {ProductSchema} from "../../../../client";
import {FC, RefObject} from "react";
import {BaseTable, BaseTableRef} from "../../../../components/BaseTable/BaseTable.tsx";
import {MRT_TableOptions} from "mantine-react-table";
import {useProductsTableColumns} from "./columns.tsx";
type Props = {
products: ProductSchema[];
tableRef?: RefObject<BaseTableRef<ProductSchema>>
}
const ProductsTable: FC<Props> = ({products, tableRef}) => {
const columns = useProductsTableColumns();
return (
<BaseTable
ref={tableRef}
data={products}
columns={columns}
restProps={{
enableColumnActions: false,
manualPagination: true,
enableBottomToolbar: true,
enablePagination: true
} as MRT_TableOptions<ProductSchema>}
/>
)
}
export default ProductsTable;

View File

@@ -0,0 +1,20 @@
import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table";
import {ProductSchema} from "../../../../client";
export const useProductsTableColumns = () => {
return useMemo<MRT_ColumnDef<ProductSchema>[]>(() => [
{
accessorKey: "article",
header: "Артикул",
enableSorting: false,
},
{
accessorKey: "name",
header: "Название",
enableSorting: false,
},
], []);
}

View File

@@ -0,0 +1,19 @@
import {useQuery} from "@tanstack/react-query";
import {ProductService} from "../../../client";
type Props = {
clientId: number,
page: number,
itemsPerPage: number,
}
const useServicesList = (props: Props) => {
const {clientId, page, itemsPerPage} = props;
const {isPending, error, data, refetch} = useQuery({
queryKey: ['getAllServices', clientId, page, itemsPerPage],
queryFn: () => ProductService.getProductsByClientId({clientId, page, itemsPerPage})
});
const products = isPending || error || !data ? [] : data.products;
return {products, refetch}
}
export default useServicesList;

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import {FC, useState} from "react";
import styles from './ProductsPage.module.css';
import {Drawer} from "@mantine/core";
import {useDisclosure} from "@mantine/hooks";
import ClientSelect from "../../../components/Selects/ClientSelect/ClientSelect.tsx";
import ProductsTable from "../components/ProductsTable/ProductsTable.tsx";
import useProductsList from "../hooks/useProductsList.tsx";
export const ProductsPage: FC = () => {
const [opened, {open, close}] = useDisclosure(false);
const [clientId, setClientId] = useState(-1);
const {products} = useProductsList({clientId, page: 0, itemsPerPage: 10});
return (
<>
<Drawer
opened={opened}
onClose={close}
position={"right"}
size={"100%"}
/>
<div className={styles['container']}>
<PageBlock>
<div className={styles['top-panel']}>
<ClientSelect onChange={event => setClientId(event.id)}/>
</div>
</PageBlock>
<PageBlock>
<div className={styles['body-container']}>
<ProductsTable products={products}/>
</div>
</PageBlock>
</div>
</>
)
}

View File

@@ -18,6 +18,7 @@ import { Route as rootRoute } from './routes/__root'
const TestLazyImport = createFileRoute('/test')() const TestLazyImport = createFileRoute('/test')()
const ServicesLazyImport = createFileRoute('/services')() const ServicesLazyImport = createFileRoute('/services')()
const ProductsLazyImport = createFileRoute('/products')()
const LoginLazyImport = createFileRoute('/login')() const LoginLazyImport = createFileRoute('/login')()
const LeadsLazyImport = createFileRoute('/leads')() const LeadsLazyImport = createFileRoute('/leads')()
const ClientsLazyImport = createFileRoute('/clients')() const ClientsLazyImport = createFileRoute('/clients')()
@@ -35,6 +36,11 @@ const ServicesLazyRoute = ServicesLazyImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/services.lazy').then((d) => d.Route)) } as any).lazy(() => import('./routes/services.lazy').then((d) => d.Route))
const ProductsLazyRoute = ProductsLazyImport.update({
path: '/products',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/products.lazy').then((d) => d.Route))
const LoginLazyRoute = LoginLazyImport.update({ const LoginLazyRoute = LoginLazyImport.update({
path: '/login', path: '/login',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
@@ -75,6 +81,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginLazyImport preLoaderRoute: typeof LoginLazyImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/products': {
preLoaderRoute: typeof ProductsLazyImport
parentRoute: typeof rootRoute
}
'/services': { '/services': {
preLoaderRoute: typeof ServicesLazyImport preLoaderRoute: typeof ServicesLazyImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
@@ -93,6 +103,7 @@ export const routeTree = rootRoute.addChildren([
ClientsLazyRoute, ClientsLazyRoute,
LeadsLazyRoute, LeadsLazyRoute,
LoginLazyRoute, LoginLazyRoute,
ProductsLazyRoute,
ServicesLazyRoute, ServicesLazyRoute,
TestLazyRoute, TestLazyRoute,
]) ])

View File

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