This commit is contained in:
2024-04-11 07:57:01 +03:00
parent 4ce516307d
commit 18157972a1
30 changed files with 911 additions and 50 deletions

View File

@@ -19,16 +19,26 @@ export type { ClientSchema } from './models/ClientSchema';
export type { ClientUpdateDetailsRequest } from './models/ClientUpdateDetailsRequest'; export type { ClientUpdateDetailsRequest } from './models/ClientUpdateDetailsRequest';
export type { ClientUpdateRequest } from './models/ClientUpdateRequest'; export type { ClientUpdateRequest } from './models/ClientUpdateRequest';
export type { ClientUpdateResponse } from './models/ClientUpdateResponse'; export type { ClientUpdateResponse } from './models/ClientUpdateResponse';
export type { DealAddServiceRequest } from './models/DealAddServiceRequest';
export type { DealAddServiceResponse } from './models/DealAddServiceResponse';
export type { DealAddServicesRequest } from './models/DealAddServicesRequest'; export type { DealAddServicesRequest } from './models/DealAddServicesRequest';
export type { DealAddServicesResponse } from './models/DealAddServicesResponse'; 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 { DealDeleteServiceRequest } from './models/DealDeleteServiceRequest';
export type { DealDeleteServiceResponse } from './models/DealDeleteServiceResponse';
export type { DealDeleteServicesRequest } from './models/DealDeleteServicesRequest';
export type { DealDeleteServicesResponse } from './models/DealDeleteServicesResponse';
export type { DealGetAllResponse } from './models/DealGetAllResponse';
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 { DealSchema } from './models/DealSchema';
export type { DealServiceSchema } from './models/DealServiceSchema'; 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 { DealUpdateServiceQuantityRequest } from './models/DealUpdateServiceQuantityRequest';
export type { DealUpdateServiceQuantityResponse } from './models/DealUpdateServiceQuantityResponse';
export type { HTTPValidationError } from './models/HTTPValidationError'; export type { HTTPValidationError } from './models/HTTPValidationError';
export type { PaginationInfoSchema } from './models/PaginationInfoSchema'; export type { PaginationInfoSchema } from './models/PaginationInfoSchema';
export type { ProductCreateRequest } from './models/ProductCreateRequest'; export type { ProductCreateRequest } from './models/ProductCreateRequest';

View File

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

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealAddServiceResponse = {
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 DealDeleteServiceRequest = {
dealId: number;
serviceId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealDeleteServiceResponse = {
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 DealDeleteServicesRequest = {
dealId: number;
serviceIds: Array<number>;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealDeleteServicesResponse = {
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 */
import type { DealSchema } from './DealSchema';
export type DealGetAllResponse = {
deals: Array<DealSchema>;
};

View File

@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { DealServiceSchema } from './DealServiceSchema';
export type DealSchema = {
id: number;
name: string;
clientId: number;
createdAt: string;
currentStatus: number;
services: Array<DealServiceSchema>;
};

View File

@@ -2,8 +2,9 @@
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { ServiceSchema } from './ServiceSchema';
export type DealServiceSchema = { export type DealServiceSchema = {
id: number; service: ServiceSchema;
quantity: number; quantity: number;
}; };

View File

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

View File

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

View File

@@ -2,14 +2,24 @@
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { DealAddServiceRequest } from '../models/DealAddServiceRequest';
import type { DealAddServiceResponse } from '../models/DealAddServiceResponse';
import type { DealAddServicesRequest } from '../models/DealAddServicesRequest'; import type { DealAddServicesRequest } from '../models/DealAddServicesRequest';
import type { DealAddServicesResponse } from '../models/DealAddServicesResponse'; 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';
import type { DealDeleteServiceRequest } from '../models/DealDeleteServiceRequest';
import type { DealDeleteServiceResponse } from '../models/DealDeleteServiceResponse';
import type { DealDeleteServicesRequest } from '../models/DealDeleteServicesRequest';
import type { DealDeleteServicesResponse } from '../models/DealDeleteServicesResponse';
import type { DealGetAllResponse } from '../models/DealGetAllResponse';
import type { DealQuickCreateRequest } from '../models/DealQuickCreateRequest'; import type { DealQuickCreateRequest } from '../models/DealQuickCreateRequest';
import type { DealQuickCreateResponse } from '../models/DealQuickCreateResponse'; import type { DealQuickCreateResponse } from '../models/DealQuickCreateResponse';
import type { DealSchema } from '../models/DealSchema';
import type { DealSummaryResponse } from '../models/DealSummaryResponse'; import type { DealSummaryResponse } from '../models/DealSummaryResponse';
import type { DealUpdateServiceQuantityRequest } from '../models/DealUpdateServiceQuantityRequest';
import type { DealUpdateServiceQuantityResponse } from '../models/DealUpdateServiceQuantityResponse';
import type { CancelablePromise } from '../core/CancelablePromise'; import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI'; import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request'; import { request as __request } from '../core/request';
@@ -90,11 +100,31 @@ export class DealService {
* @returns DealAddServicesResponse Successful Response * @returns DealAddServicesResponse Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static servicesAddDealServicesAddPost({ public static addMultipleDealServices({
requestBody, requestBody,
}: { }: {
requestBody: DealAddServicesRequest, requestBody: DealAddServicesRequest,
}): CancelablePromise<DealAddServicesResponse> { }): CancelablePromise<DealAddServicesResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/services/add/multiple',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Services Add
* @returns DealAddServiceResponse Successful Response
* @throws ApiError
*/
public static addDealService({
requestBody,
}: {
requestBody: DealAddServiceRequest,
}): CancelablePromise<DealAddServiceResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: 'POST',
url: '/deal/services/add', url: '/deal/services/add',
@@ -105,4 +135,96 @@ export class DealService {
}, },
}); });
} }
/**
* Services Update
* @returns DealUpdateServiceQuantityResponse Successful Response
* @throws ApiError
*/
public static updateDealServiceQuantity({
requestBody,
}: {
requestBody: DealUpdateServiceQuantityRequest,
}): CancelablePromise<DealUpdateServiceQuantityResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/services/update-quantity',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Services Delete
* @returns DealDeleteServiceResponse Successful Response
* @throws ApiError
*/
public static deleteDealService({
requestBody,
}: {
requestBody: DealDeleteServiceRequest,
}): CancelablePromise<DealDeleteServiceResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/services/delete',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Services Delete
* @returns DealDeleteServicesResponse Successful Response
* @throws ApiError
*/
public static deleteMultipleDealServices({
requestBody,
}: {
requestBody: DealDeleteServicesRequest,
}): CancelablePromise<DealDeleteServicesResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/services/delete/multiple',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get All
* @returns DealGetAllResponse Successful Response
* @throws ApiError
*/
public static getAllDeals(): CancelablePromise<DealGetAllResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/deal/get-all',
});
}
/**
* Get Deal By Id
* @returns DealSchema Successful Response
* @throws ApiError
*/
public static getDealById({
dealId,
}: {
dealId: number,
}): CancelablePromise<DealSchema> {
return __request(OpenAPI, {
method: 'GET',
url: '/deal/get/{deal_id}',
path: {
'deal_id': dealId,
},
errors: {
422: `Validation Error`,
},
});
}
} }

View File

@@ -7,10 +7,11 @@ import {
useMantineReactTable useMantineReactTable
} from "mantine-react-table"; } from "mantine-react-table";
import {MRT_Localization_RU} from "mantine-react-table/locales/ru/index.cjs"; import {MRT_Localization_RU} from "mantine-react-table/locales/ru/index.cjs";
import {forwardRef, useImperativeHandle} from 'react'; import {forwardRef, useEffect, useImperativeHandle} from 'react';
type Props<T extends Record<string, any>, K extends keyof T> = { type Props<T extends Record<string, any>, K extends keyof T> = {
data: T[], data: T[],
onSelectionChange?: (selectedRows: T[]) => void,
columns: MRT_ColumnDef<T>[], columns: MRT_ColumnDef<T>[],
restProps?: MRT_TableOptions<T>, restProps?: MRT_TableOptions<T>,
striped?: boolean striped?: boolean
@@ -23,7 +24,7 @@ export type BaseTableRef<T extends MRT_RowData> = {
}; };
export const BaseTable = forwardRef<BaseTableRef<any>, Props<any>>((props, ref) => { export const BaseTable = forwardRef<BaseTableRef<any>, Props<any>>((props, ref) => {
const {data, columns, restProps, striped} = props; const {data, columns, restProps, striped, onSelectionChange} = props;
const table = useMantineReactTable({ const table = useMantineReactTable({
localization: MRT_Localization_RU, localization: MRT_Localization_RU,
@@ -34,8 +35,14 @@ export const BaseTable = forwardRef<BaseTableRef<any>, Props<any>>((props, ref)
striped: striped striped: striped
}, },
enableTopToolbar: false, enableTopToolbar: false,
enableBottomToolbar: false,
enableRowSelection: onSelectionChange !== undefined,
...restProps, ...restProps,
}); });
useEffect(() => {
if (!onSelectionChange) return;
onSelectionChange(table.getSelectedRowModel().rows.map(row => row.original))
}, [table.getState().rowSelection]);
// Используем useImperativeHandle для определения, что будет доступно через ref // Используем useImperativeHandle для определения, что будет доступно через ref
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({

View File

@@ -1,17 +1,25 @@
import {FC} from "react"; import {FC} from "react";
import {DealSummary} from "../../../client"; import {DealService, DealSummary} from "../../../client";
import styles from './DealSummaryCard.module.css'; import styles from './DealSummaryCard.module.css';
import {Text} from '@mantine/core'; import {Text} from '@mantine/core';
import classNames from "classnames"; import classNames from "classnames";
import {useDealPageContext} from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
type Props = { type Props = {
dealSummary: DealSummary dealSummary: DealSummary
} }
const DealSummaryCard: FC<Props> = ({dealSummary}) => { const DealSummaryCard: FC<Props> = ({dealSummary}) => {
const {setSelectedDeal} = useDealPageContext();
const onDealSummaryClick = () => {
DealService.getDealById({dealId: dealSummary.id})
.then((deal) => {
setSelectedDeal(deal);
})
}
return ( return (
<div className={styles['container']}> <div onClick={() => onDealSummaryClick()} className={styles['container']}>
<div className={styles['flex-row']}> <div className={styles['flex-row']}>
<div className={styles['flex-item']}> <div className={styles['flex-item']}>
<Text size={"sm"} c={"gray.6"}> <Text size={"sm"} c={"gray.6"}>

View File

@@ -0,0 +1,3 @@
.number-input {
width: rem(50);
}

View File

@@ -0,0 +1,97 @@
import {ActionIcon, Flex, NumberInput, rem} from "@mantine/core";
import {IconMinus, IconPlus} from "@tabler/icons-react";
import styles from './PlusMinusInput.module.css';
import {FC, useEffect, useState} from "react";
type ControlledValueProps = {
value: number;
onChange: (value: number) => void;
}
type RestProps = {
defaultValue?: number;
onChange: (value: number) => void;
}
type Props = RestProps & Partial<ControlledValueProps>;
const PlusMinusInput: FC<Props> = (props: Props) => {
const isControlled = props.value !== undefined;
const [internalValue, setInternalValue] = useState(props.defaultValue || 0);
const value = isControlled ? props.value : internalValue;
const onMinusClick = () => {
const newValue = (value || 0) - 1;
if (newValue < 0) {
return;
}
if (isControlled) {
props.onChange(newValue);
} else {
setInternalValue(newValue);
}
}
const onPlusClick = () => {
const newValue = (value || 0) + 1;
if (isControlled) {
props.onChange(newValue);
} else {
setInternalValue(newValue);
}
}
const handleInputChange = (event: number | string) => {
let newValue = typeof event === "string" ? 0 : event;
if (isNaN(newValue) || newValue < 0) {
newValue = 0;
}
if (isControlled) {
props.onChange(newValue);
} else {
setInternalValue(newValue);
}
}
useEffect(() => {
if (!isControlled) {
props.onChange(internalValue);
}
}, [internalValue]);
return (
<Flex
align={"center"}
gap={rem(10)}
>
<ActionIcon
disabled={value === 0}
onClick={onMinusClick}
variant={"default"}>
<IconMinus/>
</ActionIcon>
<NumberInput
min={0}
styles={{
input: {
textAlign: "center"
}
}}
allowNegative={false}
hideControls
value={value}
className={styles['number-input']}
onChange={(event) => handleInputChange(event)}
/>
<ActionIcon
onClick={onPlusClick}
variant={"default"}>
<IconPlus/>
</ActionIcon>
</Flex>
)
}
export default PlusMinusInput;

View File

@@ -0,0 +1,63 @@
import {ServiceSchema} from "../../client";
import {FC, useEffect, useMemo, useState} from "react";
import {Select, SelectProps} from "@mantine/core";
import useServicesList from "../../pages/ServicesPage/hooks/useServicesList.tsx";
type ControlledValueProps = {
value: ServiceSchema;
onChange: (value: ServiceSchema) => void;
}
type RestProps = {
defaultValue?: ServiceSchema;
onChange: (value: ServiceSchema) => void;
}
type Props = (RestProps & Partial<ControlledValueProps>) & Omit<SelectProps, 'value' | 'onChange'>;
const ServiceSelect: FC<Props> = (props) => {
const isControlled = props.value !== undefined;
const [internalValue, setInternalValue] = useState<ServiceSchema | undefined>(props.defaultValue);
const value = isControlled ? props.value : internalValue;
const {services} = useServicesList();
const categories = useMemo(() => services.reduce((acc, service) => {
if (!acc.includes(service.category.name)) {
acc.push(service.category.name);
}
return acc;
}, [] as string[]), [services]);
const data = useMemo(() => categories.map(category => ({
group: category,
items: services.filter(service => service.category.name === category)
.map(service => ({
value: service.id.toString(),
label: service.name
}))
})), [services, categories]);
const handleOnChange = (value: string) => {
if (isControlled) {
props.onChange(services.find(service => service.id.toString() === value) as ServiceSchema);
return;
}
setInternalValue(services.find(service => service.id.toString() === value) as ServiceSchema);
}
useEffect(() => {
if (!isControlled) {
props.onChange(internalValue as ServiceSchema);
}
}, [internalValue]);
return (
<Select
{...props}
value={value?.id.toString()}
withCheckIcon={false}
searchable
onChange={event => event && handleOnChange(event)}
data={data}
/>
)
}
export default ServiceSelect;

View File

@@ -3,6 +3,7 @@ import CreateServiceCategoryModal from "../pages/ServicesPage/modals/CreateServi
import CreateServiceModal from "../pages/ServicesPage/modals/CreateServiceModal.tsx"; import CreateServiceModal from "../pages/ServicesPage/modals/CreateServiceModal.tsx";
import createProductModal from "../pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx"; import createProductModal from "../pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx";
import ProductFormModal from "../pages/ClientsPage/modals/ClientFormModal/ClientFormModal.tsx"; import ProductFormModal from "../pages/ClientsPage/modals/ClientFormModal/ClientFormModal.tsx";
import AddDealServiceModal from "../pages/LeadsPage/modals/AddDealServiceModal.tsx";
export const modals = { export const modals = {
enterDeadline: EnterDeadlineModal, enterDeadline: EnterDeadlineModal,
@@ -10,4 +11,5 @@ export const modals = {
createService: CreateServiceModal, createService: CreateServiceModal,
createProduct: createProductModal, createProduct: createProductModal,
productFormModal: ProductFormModal, productFormModal: ProductFormModal,
addDealService: AddDealServiceModal
} }

View File

@@ -1,4 +1,4 @@
import {FC} from "react"; import {FC, useState} from "react";
import ClientsTable from "./components/ClientsTable/ClientsTable.tsx"; import ClientsTable from "./components/ClientsTable/ClientsTable.tsx";
import useClientsList from "./hooks/useClientsList.tsx"; import useClientsList from "./hooks/useClientsList.tsx";
import PageBlock from "../../components/PageBlock/PageBlock.tsx"; import PageBlock from "../../components/PageBlock/PageBlock.tsx";
@@ -7,6 +7,7 @@ import {Button} from "@mantine/core";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
import {ClientSchema, ClientService} from "../../client"; import {ClientSchema, ClientService} from "../../client";
import {notifications} from "../../shared/lib/notifications.ts"; import {notifications} from "../../shared/lib/notifications.ts";
import PlusMinusInput from "../../components/PlusMinusInput/PlusMinusInput.tsx";
const ClientsPage: FC = () => { const ClientsPage: FC = () => {
const {clients, refetch} = useClientsList(); const {clients, refetch} = useClientsList();
@@ -58,6 +59,8 @@ const ClientsPage: FC = () => {
} }
}) })
} }
const [a, setA] = useState(5);
return ( return (
<div className={styles['container']}> <div className={styles['container']}>
<PageBlock> <PageBlock>
@@ -68,7 +71,14 @@ const ClientsPage: FC = () => {
> >
Создать клиента Создать клиента
</Button> </Button>
<PlusMinusInput onChange={event=>{
console.log(event);
setA(event)
}}
value={a}
/>
</div> </div>
</PageBlock> </PageBlock>
<PageBlock> <PageBlock>
<ClientsTable <ClientsTable

View File

@@ -25,6 +25,8 @@ const BaseFormModal = <T, >(props: Props<T>) => {
const isEditing = 'onChange' in props; const isEditing = 'onChange' in props;
const onSubmit = (values: T) => { const onSubmit = (values: T) => {
console.log('----values');
console.log(values)
if (isEditing) { if (isEditing) {
props.onChange(values); props.onChange(values);
} else { } else {

View File

@@ -0,0 +1,98 @@
import {FC} from "react";
import {useDealServicesTableColumns} from "./columns.tsx";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import {DealServiceSchema} from "../../../../client";
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {MRT_TableOptions} from "mantine-react-table";
import {ActionIcon, Box, Button, Flex, rem, Tooltip} from "@mantine/core";
import {openContextModal} from "@mantine/modals";
import {IconTrash} from "@tabler/icons-react";
type RestProps = {
onMultipleDelete?: (items: DealServiceSchema[]) => void;
}
type Props = CRUDTableProps<DealServiceSchema> & RestProps;
const DealServicesTable: FC<Props> = (
{
items,
onChange,
onDelete,
onCreate,
onSelectionChange,
onMultipleDelete,
tableRef
}) => {
const onQuantityChange = (service: DealServiceSchema, quantity: number) => {
if (!onChange) return;
if (quantity <= 0 && onDelete) {
onDelete(service);
return;
}
onChange({...service, quantity});
}
const columns = useDealServicesTableColumns({
onChange: onQuantityChange,
data: items
});
const onCreateClick = () => {
if (!onCreate) return;
openContextModal({
title: "Добавление услуги",
modal: "addDealService",
innerProps: {
onCreate: (event) => onCreate(event as DealServiceSchema)
}
})
}
return (
<BaseTable
ref={tableRef}
data={items}
columns={columns}
onSelectionChange={onSelectionChange}
restProps={{
enableGrouping: true,
initialState: {grouping: ["service.category"]},
enableColumnActions: false,
enableSorting: false,
enableBottomToolbar: true,
enableRowActions: true,
enableRowSelection: true,
renderBottomToolbar: ({table}) => (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
{(onMultipleDelete && table.getSelectedRowModel().rows.length > 0) && (
<Button
onClick={() => {
onMultipleDelete(table.getSelectedRowModel().rows.map(row => row.original))
}}
variant={"filled"}
color={"red"}
>
Удалить выбранные
</Button>
)}
<Button onClick={onCreateClick} variant={"default"}>
Добавить услугу
</Button>
</Flex>
),
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon onClick={() => {
if (onDelete) onDelete(row.original);
}} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
</Flex>
)
} as MRT_TableOptions<DealServiceSchema>}
/>
)
}
export default DealServicesTable;

View File

@@ -0,0 +1,66 @@
import {MRT_ColumnDef} from "mantine-react-table";
import {useMemo} from "react";
import {DealServiceSchema} from "../../../../client";
import PlusMinusInput from "../../../../components/PlusMinusInput/PlusMinusInput.tsx";
type Props = {
onChange: (service: DealServiceSchema, quantity: number) => void;
data: DealServiceSchema[];
}
export const useDealServicesTableColumns = (props: Props) => {
const {onChange, data} = props;
const totalPrice = useMemo(() =>
data.reduce((acc, row) => acc + row.quantity * row.service.price, 0)
,
[data]);
return useMemo<MRT_ColumnDef<DealServiceSchema>[]>(() => [
{
accessorKey: "service.category",
header: "Категория",
accessorFn: (row) => row.service.category.name,
},
{
enableGrouping: false,
accessorKey: "service.name",
header: "Услуга",
},
{
enableGrouping: false,
accessorKey: "service.price",
header: "Цена",
},
{
enableGrouping: false,
accessorKey: "quantity",
header: "Количество",
Cell: ({row}) => {
return (
<PlusMinusInput
value={row.original.quantity}
onChange={(value) => onChange(row.original, value)}
/>
)
}
},
{
enableGrouping: false,
header: "Сумма",
Cell: ({row}) => {
return row.original.quantity * row.original.service.price;
},
aggregationFn: "sum",
AggregatedCell: ({cell}) => {
return <>Итоговая сумма по категории: {" "}
{
cell.row.subRows?.reduce((acc, row) =>
acc + row.original.quantity * row.original.service.price, 0)
}
</>;
},
Footer: <>Итоговая сумма по услугам: {totalPrice}</>
}
], [onChange]);
}

View File

@@ -0,0 +1,34 @@
import {createContext, FC, useContext, useState} from "react";
import {DealSchema} from "../../../client";
type DealPageContextState = {
selectedDeal?: DealSchema;
setSelectedDeal: (deal: DealSchema | undefined) => void;
}
const DealPageContext = createContext<DealPageContextState | undefined>(undefined);
const useDealPageContextState = () => {
const [selectedDeal, setSelectedDeal] = useState<DealSchema | undefined>(undefined);
return {selectedDeal, setSelectedDeal};
}
type DealPageContextProviderProps = {
children: React.ReactNode;
}
export const DealPageContextProvider: FC<DealPageContextProviderProps> = ({children}) => {
const state = useDealPageContextState();
return (
<DealPageContext.Provider value={state}>
{children}
</DealPageContext.Provider>
);
}
export const useDealPageContext = () => {
const context = useContext(DealPageContext);
if (!context) {
throw new Error('useDealPageContext must be used within a DealPageContextProvider');
}
return context;
}

View File

@@ -0,0 +1,175 @@
import {Drawer, Text} from "@mantine/core";
import {FC, useRef} from "react";
import DealServicesTable from "../../components/DealServicesTable/DealServicesTable.tsx";
import {useDealPageContext} from "../../contexts/DealPageContext.tsx";
import {DealService, DealServiceSchema} from "../../../../client";
import {notifications} from "../../../../shared/lib/notifications.ts";
import {modals} from "@mantine/modals";
import {BaseTableRef} from "../../../../components/BaseTable/BaseTable.tsx";
const useDealServicesTableState = () => {
const {selectedDeal, setSelectedDeal} = useDealPageContext();
const tableRef = useRef<BaseTableRef<DealServiceSchema>>(null);
const onServiceUpdate = (service: DealServiceSchema) => {
if (!selectedDeal) return;
DealService.updateDealServiceQuantity({
requestBody: {
dealId: selectedDeal.id,
serviceId: service.service.id,
quantity: service.quantity
}
}).then(async ({ok, message}) => {
if (!ok) {
notifications.guess(ok, {message});
return;
}
await DealService.getDealById({dealId: selectedDeal.id})
.then(setSelectedDeal)
})
}
const onServiceDelete = (service: DealServiceSchema) => {
if (!selectedDeal) return;
modals.openConfirmModal({
title: "Удаление услуги",
children: (
<>
<Text>
Вы уверены, что хотите удалить услугу:
</Text>
<Text>
{service.service.name}?
</Text>
</>
),
onConfirm: () => {
DealService.deleteDealService({
requestBody: {
dealId: selectedDeal.id,
serviceId: service.service.id
}
}).then(async ({ok, message}) => {
if (!ok) {
notifications.guess(ok, {message});
return;
}
await DealService.getDealById({dealId: selectedDeal.id})
.then(setSelectedDeal)
})
},
labels: {
cancel: "Отмена",
confirm: "Удалить"
}
})
}
const onServiceCreate = (service: DealServiceSchema) => {
console.log('-------Drawer')
console.log(service);
if (!selectedDeal) return;
DealService.addDealService({
requestBody: {
dealId: selectedDeal.id,
serviceId: service.service.id,
quantity: service.quantity
}
}).then(async ({ok, message}) => {
if (!ok) {
notifications.guess(ok, {message});
return;
}
await DealService.getDealById({dealId: selectedDeal.id})
.then(setSelectedDeal)
})
}
const onsServiceMultipleDelete = (items: DealServiceSchema[]) => {
if (!selectedDeal) return;
modals.openConfirmModal({
title: "Удаление услуг",
children: (
<>
<Text>
Вы уверены, что хотите удалить выбранные услуги?
</Text>
</>
),
onConfirm: () => {
DealService.deleteMultipleDealServices({
requestBody: {
dealId: selectedDeal.id,
serviceIds: items.map(item => item.service.id)
}
}).then(async ({ok, message}) => {
if (!ok) {
notifications.guess(ok, {message});
return;
}
await DealService.getDealById({dealId: selectedDeal.id})
.then(setSelectedDeal)
})
},
labels: {
cancel: "Отмена",
confirm: "Удалить"
}
})
}
return {
onServiceUpdate,
onServiceDelete,
onServiceCreate,
onsServiceMultipleDelete,
tableRef,
services: selectedDeal?.services || []
}
}
const DealEditDrawerServicesTable = () => {
const {
services,
tableRef,
onServiceCreate,
onServiceUpdate,
onServiceDelete,
onsServiceMultipleDelete
} = useDealServicesTableState();
return (<DealServicesTable
tableRef={tableRef}
items={services}
onChange={onServiceUpdate}
onDelete={onServiceDelete}
onCreate={onServiceCreate}
onMultipleDelete={onsServiceMultipleDelete}
/>)
}
const useDealEditDrawerState = () => {
const {selectedDeal, setSelectedDeal} = useDealPageContext();
return {
isVisible: selectedDeal !== undefined,
onClose: () => setSelectedDeal(undefined)
}
}
const DealEditDrawer: FC = () => {
const {isVisible, onClose} = useDealEditDrawerState();
return (
<Drawer
size={"95%"}
position={"right"}
onClose={onClose}
opened={isVisible}>
<DealEditDrawerServicesTable/>
</Drawer>
);
}
export default DealEditDrawer;

View File

@@ -0,0 +1,55 @@
import {ContextModalProps} from "@mantine/modals";
import BaseFormModal, {CreateEditFormProps} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import {DealServiceSchema} from "../../../client";
import {useForm} from "@mantine/form";
import {NumberInput} from "@mantine/core";
import ServiceSelect from "../../../components/ServiceSelect/ServiceSelect.tsx";
type Props = CreateEditFormProps<Partial<DealServiceSchema>>;
const AddDealServiceModal = ({
context,
id,
innerProps
}: ContextModalProps<Props>) => {
const form = useForm<Partial<DealServiceSchema>>({
initialValues: {
service: undefined,
quantity: 0,
},
validate: {
service: (service?: DealServiceSchema['service']) => service !== undefined ? null : "Необходимо выбрать услугу",
quantity: (quantity?: number) => (quantity && quantity > 0) ? null : "Количество должно быть больше 0"
}
});
const onClose = () => {
context.closeContextModal(id);
}
return (
<BaseFormModal
{...innerProps}
form={form}
closeOnSubmit
onClose={onClose}>
<BaseFormModal.Body>
<>
<ServiceSelect
placeholder={"Выберите услугу"}
label={"Услуга"}
{...form.getInputProps('service')}
/>
<NumberInput
placeholder={"Введите количество"}
label={"Количество"}
min={1}
{...form.getInputProps('quantity')}
/>
</>
</BaseFormModal.Body>
</BaseFormModal>
)
}
export default AddDealServiceModal;

View File

@@ -5,6 +5,8 @@ import {DragDropContext} from "@hello-pangea/dnd";
import {useDealSummaries} from "../hooks/useDealSummaries.tsx"; import {useDealSummaries} from "../hooks/useDealSummaries.tsx";
import {DealStatus} from "../../../shared/enums/DealStatus.ts"; import {DealStatus} from "../../../shared/enums/DealStatus.ts";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx"; import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import DealEditDrawer from "../drawers/DealEditDrawer/DealEditDrawer.tsx";
import {DealPageContextProvider} from "../contexts/DealPageContext.tsx";
export const LeadsPage: FC = () => { export const LeadsPage: FC = () => {
@@ -25,45 +27,50 @@ export const LeadsPage: FC = () => {
} }
return ( return (
<> <>
<PageBlock> <DealPageContextProvider>
<div className={styles['container']}>
<div className={styles['boards']}> <PageBlock>
<DragDropContext onDragEnd={onDragEnd}> <div className={styles['container']}>
<Board <div className={styles['boards']}>
withCreateButton <DragDropContext onDragEnd={onDragEnd}>
summaries={summaries <Board
.filter(summary => summary.status == DealStatus.AWAITING_ACCEPTANCE)} withCreateButton
title={"Ожидает приемки"} summaries={summaries
droppableId={"AWAITING_ACCEPTANCE"} .filter(summary => summary.status == DealStatus.AWAITING_ACCEPTANCE)}
/> title={"Ожидает приемки"}
<Board droppableId={"AWAITING_ACCEPTANCE"}
summaries={summaries />
.filter(summary => summary.status == DealStatus.PACKAGING)} <Board
title={"Упаковка"} summaries={summaries
droppableId={"PACKAGING"} .filter(summary => summary.status == DealStatus.PACKAGING)}
/> title={"Упаковка"}
<Board droppableId={"PACKAGING"}
summaries={summaries />
.filter(summary => summary.status == DealStatus.AWAITING_SHIPMENT)} <Board
title={"Ожидает отгрузки"} summaries={summaries
droppableId={"AWAITING_SHIPMENT"} .filter(summary => summary.status == DealStatus.AWAITING_SHIPMENT)}
/> title={"Ожидает отгрузки"}
<Board droppableId={"AWAITING_SHIPMENT"}
summaries={summaries />
.filter(summary => summary.status == DealStatus.AWAITING_PAYMENT)} <Board
title={"Ожидает оплаты"} summaries={summaries
droppableId={"AWAITING_PAYMENT"} .filter(summary => summary.status == DealStatus.AWAITING_PAYMENT)}
/> title={"Ожидает оплаты"}
<Board droppableId={"AWAITING_PAYMENT"}
summaries={summaries />
.filter(summary => summary.status == DealStatus.COMPLETED)} <Board
title={"Завершена"} summaries={summaries
droppableId={"COMPLETED"} .filter(summary => summary.status == DealStatus.COMPLETED)}
/> title={"Завершена"}
</DragDropContext> droppableId={"COMPLETED"}
/>
</DragDropContext>
</div>
</div> </div>
</div> </PageBlock>
</PageBlock> <DealEditDrawer
/>
</DealPageContextProvider>
</> </>

View File

@@ -6,7 +6,7 @@ type Props = {
page: number, page: number,
itemsPerPage: number, itemsPerPage: number,
} }
const useServicesList = (props: Props) => { const useProductsList = (props: Props) => {
const {clientId, page, itemsPerPage} = props; const {clientId, page, itemsPerPage} = props;
const {data, refetch} = useQuery({ const {data, refetch} = useQuery({
queryKey: ['getAllServices', clientId, page, itemsPerPage], queryKey: ['getAllServices', clientId, page, itemsPerPage],
@@ -16,4 +16,4 @@ const useServicesList = (props: Props) => {
const paginationInfo = data?.paginationInfo; const paginationInfo = data?.paginationInfo;
return {products, paginationInfo, refetch} return {products, paginationInfo, refetch}
} }
export default useServicesList; export default useProductsList;

View File

@@ -4,11 +4,13 @@ import styles from './ProductsPage.module.css';
import {Button, Text, Pagination} from "@mantine/core"; import {Button, Text, Pagination} from "@mantine/core";
import ClientSelect from "../../../components/Selects/ClientSelect/ClientSelect.tsx"; import ClientSelect from "../../../components/Selects/ClientSelect/ClientSelect.tsx";
import ProductsTable from "../components/ProductsTable/ProductsTable.tsx"; import ProductsTable from "../components/ProductsTable/ProductsTable.tsx";
import useProductsList from "../hooks/useProductsList.tsx";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
import {notifications} from "../../../shared/lib/notifications.ts"; import {notifications} from "../../../shared/lib/notifications.ts";
import {CreateProductRequest} from "../types.ts"; import {CreateProductRequest} from "../types.ts";
import {ProductSchema, ProductService} from "../../../client"; import {ProductSchema, ProductService} from "../../../client";
import ServiceSelect from "../../../components/ServiceSelect/ServiceSelect.tsx";
import useProductsList from "../hooks/useProductsList.tsx";
import useServicesList from "../../ServicesPage/hooks/useServicesList.tsx";
export const ProductsPage: FC = () => { export const ProductsPage: FC = () => {
const [clientId, setClientId] = useState(-1); const [clientId, setClientId] = useState(-1);
@@ -79,8 +81,6 @@ export const ProductsPage: FC = () => {
if (!paginationInfo) return; if (!paginationInfo) return;
setTotalPages(paginationInfo.totalPages); setTotalPages(paginationInfo.totalPages);
}, [paginationInfo]); }, [paginationInfo]);
return ( return (
<> <>
<div className={styles['container']}> <div className={styles['container']}>
@@ -91,6 +91,9 @@ export const ProductsPage: FC = () => {
onClick={() => onCreateProductClick()} onClick={() => onCreateProductClick()}
variant={"default"} variant={"default"}
>Создать</Button> >Создать</Button>
{/*<ServiceSelect*/}
{/* value={selectedService}*/}
{/* onChange={setSelectedService}/>*/}
</div> </div>
</PageBlock> </PageBlock>
<PageBlock> <PageBlock>

View File

@@ -7,5 +7,6 @@ export interface CRUDTableProps<T extends MRT_RowData> {
onCreate?: (item: T) => void; onCreate?: (item: T) => void;
onDelete?: (item: T) => void; onDelete?: (item: T) => void;
onChange?: (item: T) => void; onChange?: (item: T) => void;
onSelectionChange?: (selectedItems: T[]) => void;
tableRef?: RefObject<BaseTableRef<T>> tableRef?: RefObject<BaseTableRef<T>>
} }