feat: deal product services

This commit is contained in:
2024-05-19 03:30:04 +03:00
parent b0cfaf3a8b
commit c5cd8e350f
28 changed files with 332 additions and 151 deletions

View File

@@ -70,8 +70,12 @@ export type { DealUpdateGeneralInfoRequest } from './models/DealUpdateGeneralInf
export type { DealUpdateGeneralInfoResponse } from './models/DealUpdateGeneralInfoResponse'; export type { DealUpdateGeneralInfoResponse } from './models/DealUpdateGeneralInfoResponse';
export type { DealUpdateProductQuantityRequest } from './models/DealUpdateProductQuantityRequest'; export type { DealUpdateProductQuantityRequest } from './models/DealUpdateProductQuantityRequest';
export type { DealUpdateProductQuantityResponse } from './models/DealUpdateProductQuantityResponse'; export type { DealUpdateProductQuantityResponse } from './models/DealUpdateProductQuantityResponse';
export type { DealUpdateProductRequest } from './models/DealUpdateProductRequest';
export type { DealUpdateProductResponse } from './models/DealUpdateProductResponse';
export type { DealUpdateServiceQuantityRequest } from './models/DealUpdateServiceQuantityRequest'; export type { DealUpdateServiceQuantityRequest } from './models/DealUpdateServiceQuantityRequest';
export type { DealUpdateServiceQuantityResponse } from './models/DealUpdateServiceQuantityResponse'; export type { DealUpdateServiceQuantityResponse } from './models/DealUpdateServiceQuantityResponse';
export type { DealUpdateServiceRequest } from './models/DealUpdateServiceRequest';
export type { DealUpdateServiceResponse } from './models/DealUpdateServiceResponse';
export type { GetAllBarcodeTemplateAttributesResponse } from './models/GetAllBarcodeTemplateAttributesResponse'; export type { GetAllBarcodeTemplateAttributesResponse } from './models/GetAllBarcodeTemplateAttributesResponse';
export type { GetAllBarcodeTemplateSizesResponse } from './models/GetAllBarcodeTemplateSizesResponse'; export type { GetAllBarcodeTemplateSizesResponse } from './models/GetAllBarcodeTemplateSizesResponse';
export type { GetAllBarcodeTemplatesResponse } from './models/GetAllBarcodeTemplatesResponse'; export type { GetAllBarcodeTemplatesResponse } from './models/GetAllBarcodeTemplatesResponse';

View File

@@ -6,5 +6,6 @@ export type DealAddServiceRequest = {
dealId: number; dealId: number;
serviceId: number; serviceId: number;
quantity: number; quantity: number;
price: number;
}; };

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { DealProductSchema } from './DealProductSchema';
export type DealUpdateProductRequest = {
dealId: number;
product: DealProductSchema;
};

View File

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

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 DealUpdateServiceRequest = {
dealId: number;
service: DealServiceSchema;
};

View File

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

View File

@@ -31,8 +31,12 @@ import type { DealUpdateGeneralInfoRequest } from '../models/DealUpdateGeneralIn
import type { DealUpdateGeneralInfoResponse } from '../models/DealUpdateGeneralInfoResponse'; import type { DealUpdateGeneralInfoResponse } from '../models/DealUpdateGeneralInfoResponse';
import type { DealUpdateProductQuantityRequest } from '../models/DealUpdateProductQuantityRequest'; import type { DealUpdateProductQuantityRequest } from '../models/DealUpdateProductQuantityRequest';
import type { DealUpdateProductQuantityResponse } from '../models/DealUpdateProductQuantityResponse'; import type { DealUpdateProductQuantityResponse } from '../models/DealUpdateProductQuantityResponse';
import type { DealUpdateProductRequest } from '../models/DealUpdateProductRequest';
import type { DealUpdateProductResponse } from '../models/DealUpdateProductResponse';
import type { DealUpdateServiceQuantityRequest } from '../models/DealUpdateServiceQuantityRequest'; import type { DealUpdateServiceQuantityRequest } from '../models/DealUpdateServiceQuantityRequest';
import type { DealUpdateServiceQuantityResponse } from '../models/DealUpdateServiceQuantityResponse'; import type { DealUpdateServiceQuantityResponse } from '../models/DealUpdateServiceQuantityResponse';
import type { DealUpdateServiceRequest } from '../models/DealUpdateServiceRequest';
import type { DealUpdateServiceResponse } from '../models/DealUpdateServiceResponse';
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';
@@ -241,7 +245,7 @@ export class DealService {
}); });
} }
/** /**
* Services Update * Services Update Quantity
* @returns DealUpdateServiceQuantityResponse Successful Response * @returns DealUpdateServiceQuantityResponse Successful Response
* @throws ApiError * @throws ApiError
*/ */
@@ -260,6 +264,26 @@ export class DealService {
}, },
}); });
} }
/**
* Services Update
* @returns DealUpdateServiceResponse Successful Response
* @throws ApiError
*/
public static updateDealService({
requestBody,
}: {
requestBody: DealUpdateServiceRequest,
}): CancelablePromise<DealUpdateServiceResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/services/update',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/** /**
* Services Delete * Services Delete
* @returns DealDeleteServiceResponse Successful Response * @returns DealDeleteServiceResponse Successful Response
@@ -380,4 +404,24 @@ export class DealService {
}, },
}); });
} }
/**
* Products Update
* @returns DealUpdateProductResponse Successful Response
* @throws ApiError
*/
public static updateDealProduct({
requestBody,
}: {
requestBody: DealUpdateProductRequest,
}): CancelablePromise<DealUpdateProductResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/product/update',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
} }

View File

@@ -24,6 +24,8 @@ const DealSummaryCard: FC<Props> = ({dealSummary}) => {
const currentDate = new Date(); const currentDate = new Date();
const diff = deadlineDate.getTime() - currentDate.getTime(); const diff = deadlineDate.getTime() - currentDate.getTime();
const diffDays = Math.ceil(diff / (1000 * 3600 * 24)); const diffDays = Math.ceil(diff / (1000 * 3600 * 24));
if (diffDays < 0)
return 'grey.8'; // for past deadlines
if (diffDays === 1) { if (diffDays === 1) {
return 'yellow.8'; return 'yellow.8';
} }

View File

@@ -1,7 +1,7 @@
import {Select, SelectProps} from "@mantine/core"; import {Select, SelectProps} from "@mantine/core";
import {useEffect, useMemo, useState} from "react"; import {useEffect, useMemo, useState} from "react";
import {ObjectWithNameAndId} from "../../types/utils.ts"; import {ObjectWithNameAndId} from "../../types/utils.ts";
import {groupBy} from "lodash"; import {groupBy, omit} from "lodash";
export type SelectObjectType<T extends ObjectWithNameAndId> = T; export type SelectObjectType<T extends ObjectWithNameAndId> = T;
@@ -16,6 +16,7 @@ type RestProps<T extends ObjectWithNameAndId> = {
onChange: (value: SelectObjectType<T>) => void; onChange: (value: SelectObjectType<T>) => void;
data: SelectObjectType<T>[]; data: SelectObjectType<T>[];
groupBy?: (item: SelectObjectType<T>) => string; groupBy?: (item: SelectObjectType<T>) => string;
filterBy?: (item: SelectObjectType<T>) => boolean;
} }
export type ObjectSelectProps<T extends ObjectWithNameAndId> = export type ObjectSelectProps<T extends ObjectWithNameAndId> =
@@ -30,8 +31,10 @@ const ObjectSelect = <T extends ObjectWithNameAndId, >(props: ObjectSelectProps<
const value = isControlled ? props.value : internalValue; const value = isControlled ? props.value : internalValue;
const data = useMemo(() => { const data = useMemo(() => {
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
if (props.groupBy) { if (props.groupBy) {
const groupedData = groupBy(props.data, props.groupBy);
const groupedData = groupBy(propsData, props.groupBy);
return Object.entries(groupedData).map(([group, items]) => ({ return Object.entries(groupedData).map(([group, items]) => ({
group, group,
items: items.map(item => ({ items: items.map(item => ({
@@ -40,7 +43,7 @@ const ObjectSelect = <T extends ObjectWithNameAndId, >(props: ObjectSelectProps<
})) }))
})); }));
} else { } else {
return props.data.map(item => ({ return propsData.map(item => ({
label: item.name, label: item.name,
value: item.id.toString() value: item.id.toString()
})); }));
@@ -62,10 +65,10 @@ const ObjectSelect = <T extends ObjectWithNameAndId, >(props: ObjectSelectProps<
if (isControlled || !internalValue) return; if (isControlled || !internalValue) return;
props.onChange(internalValue); props.onChange(internalValue);
}, [internalValue]); }, [internalValue]);
const restProps = omit(props, ['filterBy', 'groupBy']);
return ( return (
<Select <Select
{...props} {...restProps}
value={value?.id.toString()} value={value?.id.toString()}
onChange={handleOnChange} onChange={handleOnChange}
data={data} data={data}

View File

@@ -1,6 +1,5 @@
import {ProductSchema} from "../../client"; import {ProductSchema} from "../../client";
import {Select, SelectProps} from "@mantine/core"; import {FC} from "react";
import {FC, useEffect, useMemo, useState} from "react";
import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx"; import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx";
import {omit} from "lodash"; import {omit} from "lodash";
import ObjectSelect, {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx";

View File

@@ -1,27 +1,37 @@
import {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx"; import {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx";
import {ServiceSchema} from "../../client"; import {ServiceSchema} from "../../client";
import {Flex, FlexProps, NumberInput, NumberInputProps, rem} from "@mantine/core"; import {ActionIcon, Flex, FlexProps, NumberInput, NumberInputProps, rem} from "@mantine/core";
import {FC, useEffect, useState} from "react"; import {FC, useEffect, useRef, useState} from "react";
import ServiceSelectNew from "../Selects/ServiceSelectNew/ServiceSelectNew.tsx"; import ServiceSelectNew from "../Selects/ServiceSelectNew/ServiceSelectNew.tsx";
import {ServiceType} from "../../shared/enums/ServiceType.ts";
import {IconReload, IconTrash, IconUpload} from "@tabler/icons-react";
type ServiceProps = Omit<ObjectSelectProps<ServiceSchema>, 'data'>; type ServiceProps = Omit<ObjectSelectProps<ServiceSchema>, 'data'>;
type PriceProps = NumberInputProps; type PriceProps = NumberInputProps;
type Props = { type Props = {
serviceProps: ServiceProps, serviceProps: ServiceProps,
priceProps: PriceProps priceProps: PriceProps,
quantity: number; quantity: number,
containerProps: FlexProps containerProps: FlexProps,
filterType?: ServiceType
} }
const ServiceWithPriceInput: FC<Props> = ({serviceProps, priceProps, quantity, containerProps}) => { const ServiceWithPriceInput: FC<Props> = ({
serviceProps,
priceProps,
quantity,
containerProps,
filterType = ServiceType.PRODUCT_SERVICE
}) => {
const [price, setPrice] = useState<number | undefined>( const [price, setPrice] = useState<number | undefined>(
typeof priceProps.value === 'number' ? priceProps.value : undefined); typeof priceProps.value === 'number' ? priceProps.value : undefined);
const [service, setService] = useState<ServiceSchema | undefined>(serviceProps.value); const [service, setService] = useState<ServiceSchema | undefined>(serviceProps.value);
const isFirstRender = useRef(true);
const setPriceBasedOnQuantity = (): boolean => { const setPriceBasedOnQuantity = (): boolean => {
if (!service || !service.priceRanges.length) return false; if (!service || !service.priceRanges.length) return false;
const range = service.priceRanges.find(priceRange => const range = service.priceRanges.find(priceRange =>
quantity >= priceRange.fromQuantity && quantity <= priceRange.toQuantity) || service.priceRanges[0]; quantity >= priceRange.fromQuantity && quantity <= priceRange.toQuantity) || service.priceRanges[0];
setPrice(range.price); setPrice(range.price);
return true; return true;
} }
@@ -38,34 +48,56 @@ const ServiceWithPriceInput: FC<Props> = ({serviceProps, priceProps, quantity, c
setPrice(value); setPrice(value);
} }
useEffect(() => { useEffect(() => {
if (isFirstRender.current) return;
setPriceBasedOnQuantity(); setPriceBasedOnQuantity();
}, [quantity]); }, [quantity]);
useEffect(() => { useEffect(() => {
if (!priceProps.onChange || !price) return; if (isFirstRender.current) return;
if (!priceProps.onChange || typeof price === 'undefined') return;
priceProps.onChange(price); priceProps.onChange(price);
}, [price]); }, [price]);
useEffect(() => { useEffect(() => {
// if (!isFirstRender) setPrice(0);
// if (isFirstRender.current && price) return;
if (!serviceProps.onChange || !service) return; if (!serviceProps.onChange || !service) return;
if (price && isFirstRender.current) return;
setPriceBasedOnService(); setPriceBasedOnService();
serviceProps.onChange(service); serviceProps.onChange(service);
}, [service]); }, [service]);
useEffect(() => {
console.log('first render')
isFirstRender.current = false;
}, []);
const onReload = () => {
setPriceBasedOnService();
}
return ( return (
<Flex <Flex
{...containerProps} align={"center"}
gap={rem(10)} gap={rem(10)}
{...containerProps}
> >
<ActionIcon variant={"default"}>
<IconReload onClick={() => onReload()}/>
</ActionIcon>
<ServiceSelectNew <ServiceSelectNew
{...serviceProps} {...serviceProps}
value={service} value={service}
onChange={onServiceManualChange} onChange={onServiceManualChange}
filterType={filterType}
/> />
<NumberInput <NumberInput
{...priceProps} {...priceProps}
onChange={onPriceManualChange} onChange={onPriceManualChange}
value={price} value={price}
/> />
</Flex> </Flex>
) )
} }

View File

@@ -13,7 +13,6 @@ type Props = {
const BarcodeTemplateAdditionalFieldTable: FC<Props> = (props: Props) => { const BarcodeTemplateAdditionalFieldTable: FC<Props> = (props: Props) => {
const {value, onChange} = props; const {value, onChange} = props;
const [innerValue, setInnerValue] = useState<FieldType[]>(props.value || []); const [innerValue, setInnerValue] = useState<FieldType[]>(props.value || []);
console.log(innerValue);
const onNameChange = (field: FieldType, newName: string) => { const onNameChange = (field: FieldType, newName: string) => {
const newField = {...field, name: newName}; const newField = {...field, name: newName};
const newFields = innerValue.map(f => f === field ? newField : f); const newFields = innerValue.map(f => f === field ? newField : f);

View File

@@ -33,7 +33,6 @@ const BarcodeTemplateFormModal = ({
name: (name: string | undefined) => name && name.trim() !== '' ? null : "Необходимо ввести название шаблона", name: (name: string | undefined) => name && name.trim() !== '' ? null : "Необходимо ввести название шаблона",
} }
}) })
console.log(form.values.additionalAttributes);
return ( return (
<BaseFormModal <BaseFormModal
{...innerProps} {...innerProps}

View File

@@ -1,4 +1,4 @@
import {ActionIcon, Button, Flex, Input, rem} from "@mantine/core"; import {ActionIcon, Button, ComboboxItem, ComboboxItemGroup, Flex, Input, OptionsFilter, rem} from "@mantine/core";
import {BaseFormInputProps} from "../../../../types/utils.ts"; import {BaseFormInputProps} from "../../../../types/utils.ts";
import {DealProductServiceSchema, ServiceSchema} from "../../../../client"; import {DealProductServiceSchema, ServiceSchema} from "../../../../client";
import {FC, useEffect, useState} from "react"; import {FC, useEffect, useState} from "react";
@@ -32,6 +32,16 @@ const DealProductServiceTable: FC<Props> = (props: Props) => {
setInnerValue(oldValue => oldValue.filter((_, i) => i !== idx)); setInnerValue(oldValue => oldValue.filter((_, i) => i !== idx));
} }
const serviceOptionsFilter = ({options}: { options: ComboboxItemGroup[] }) => {
const productServiceIds = innerValue.map(service => service.service?.id);
return (options as ComboboxItemGroup[]).map(({items, group}) => {
return {
group,
items: items.filter(item => !productServiceIds.includes(parseInt((item as ComboboxItem).value)))
}
})
};
useEffect(() => { useEffect(() => {
onChange(innerValue as DealProductServiceSchema[]); onChange(innerValue as DealProductServiceSchema[]);
}, [innerValue]); }, [innerValue]);
@@ -39,7 +49,8 @@ const DealProductServiceTable: FC<Props> = (props: Props) => {
<Input.Wrapper error={error}> <Input.Wrapper error={error}>
<Flex direction={"column"} gap={rem(10)}> <Flex direction={"column"} gap={rem(10)}>
{innerValue.map((service, idx) => ( {innerValue.map((service, idx) => (
<Flex key={idx} direction={"row"} gap={rem(10)} align={"center"} justify={"stretch"}> <Flex key={service.service?.name || idx} direction={"row"} gap={rem(10)} align={"center"}
justify={"stretch"}>
<ActionIcon onClick={() => onDelete(idx)} variant={"default"}> <ActionIcon onClick={() => onDelete(idx)} variant={"default"}>
<IconTrash/> <IconTrash/>
</ActionIcon> </ActionIcon>
@@ -48,7 +59,8 @@ const DealProductServiceTable: FC<Props> = (props: Props) => {
onChange: (event) => onServiceChange(idx, event), onChange: (event) => onServiceChange(idx, event),
value: service.service, value: service.service,
placeholder: "Выберите услугу", placeholder: "Выберите услугу",
style: {width: "100%"} style: {width: "100%"},
filter: serviceOptionsFilter as OptionsFilter
}} }}
priceProps={{ priceProps={{
onChange: (event) => onQuantityChange(idx, event), onChange: (event) => onQuantityChange(idx, event),

View File

@@ -40,7 +40,7 @@ const DealProductsTable: FC<Props> = (props: Props) => {
onCreate: (product) => onCreate(product as DealProductSchema), onCreate: (product) => onCreate(product as DealProductSchema),
clientId clientId
}, },
size:"lg" size: "lg"
}) })
} }
const onPrintBarcodeClick = (product: DealProductSchema) => { const onPrintBarcodeClick = (product: DealProductSchema) => {
@@ -73,6 +73,7 @@ const DealProductsTable: FC<Props> = (props: Props) => {
}) })
} }
const onEditClick = (product: DealProductSchema) => { const onEditClick = (product: DealProductSchema) => {
if (!onChange) return;
modals.openContextModal({ modals.openContextModal({
modal: "addDealProduct", modal: "addDealProduct",
title: 'Создание товара', title: 'Создание товара',
@@ -80,10 +81,9 @@ const DealProductsTable: FC<Props> = (props: Props) => {
innerProps: { innerProps: {
clientId: clientId, clientId: clientId,
element: product, element: product,
onChange: () => { onChange: onChange
}
}, },
size:"lg" size: "lg"
}) })
} }

View File

@@ -1,7 +1,6 @@
import {useMemo} from "react"; import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table"; import {MRT_ColumnDef} from "mantine-react-table";
import {DealProductSchema} from "../../../../client"; import {DealProductSchema} from "../../../../client";
import PlusMinusInput from "../../../../components/PlusMinusInput/PlusMinusInput.tsx";
import {List} from "@mantine/core"; import {List} from "@mantine/core";
type Props = { type Props = {
@@ -11,7 +10,7 @@ type Props = {
const useDealProductsTableColumns = (props: Props) => { const useDealProductsTableColumns = (props: Props) => {
const {onChange, data} = props; const {onChange, data} = props;
const totalQuantity = useMemo(() => data.reduce((acc, row) => acc + row.quantity, 0), [data]); const totalQuantity = useMemo(() => data.reduce((acc, row) => acc + row.quantity, 0), [data]);
const totalPrice = useMemo(() => data.reduce((totalAcc, row) => totalAcc + row.services.reduce((singleAcc, service) => singleAcc + service.price * row.quantity, 0), 0), [data]);
return useMemo<MRT_ColumnDef<DealProductSchema>[]>(() => [ return useMemo<MRT_ColumnDef<DealProductSchema>[]>(() => [
{ {
accessorKey: "product.article", accessorKey: "product.article",
@@ -50,14 +49,24 @@ const useDealProductsTableColumns = (props: Props) => {
enableSorting: false, enableSorting: false,
enableColumnActions: false, enableColumnActions: false,
Footer: <>Всего товаров: {totalQuantity} </>, Footer: <>Всего товаров: {totalQuantity} </>,
Cell: ({row}) => { },
return ( {
<PlusMinusInput header: "Услуги",
value={row.original.quantity} Cell: ({row}) => <List size={"sm"}>{
onChange={(value) => onChange(row.original, value)} row.original.services.map(service => `${service.service.name} (${service.price}₽ за шт)`)
/> .map(serviceText => <List.Item key={serviceText}>
) {serviceText}
} </List.Item>
)}
</List>,
enableColumnActions: false,
},
{
header: "Итоговая стоимость услуг",
Cell: ({row}) => <>
{row.original.services.reduce((acc, service) => acc + row.original.quantity * service.price, 0)}</>,
enableColumnActions: false,
Footer: <>Всего стоимость услуг: {totalPrice}</>
} }
], [onChange, data]) ], [onChange, data])
} }

View File

@@ -6,7 +6,7 @@ import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {MRT_TableOptions} from "mantine-react-table"; import {MRT_TableOptions} from "mantine-react-table";
import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core"; import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
import {openContextModal} from "@mantine/modals"; import {openContextModal} from "@mantine/modals";
import {IconTrash} from "@tabler/icons-react"; import {IconEdit, IconTrash} from "@tabler/icons-react";
type RestProps = { type RestProps = {
onMultipleDelete?: (items: DealServiceSchema[]) => void; onMultipleDelete?: (items: DealServiceSchema[]) => void;
@@ -22,13 +22,15 @@ const DealServicesTable: FC<Props> = (
onMultipleDelete, onMultipleDelete,
tableRef tableRef
}) => { }) => {
const serviceIds = items.map(item => item.service.id);
const onQuantityChange = (service: DealServiceSchema, quantity: number) => { const onQuantityChange = (service: DealServiceSchema, quantity: number) => {
if (!onChange) return; return;
if (quantity <= 0 && onDelete) { // if (!onChange) return;
onDelete(service); // if (quantity <= 0 && onDelete) {
return; // onDelete(service);
} // return;
onChange({...service, quantity}); // }
// onChange({...service, quantity});
} }
const columns = useDealServicesTableColumns({ const columns = useDealServicesTableColumns({
onChange: onQuantityChange, onChange: onQuantityChange,
@@ -40,11 +42,24 @@ const DealServicesTable: FC<Props> = (
title: "Добавление услуги", title: "Добавление услуги",
modal: "addDealService", modal: "addDealService",
innerProps: { innerProps: {
onCreate: (event) => onCreate(event as DealServiceSchema) onCreate: (event) => onCreate(event as DealServiceSchema),
serviceIds
} }
}) })
} }
const onEditClick = (service: DealServiceSchema) => {
if (!onChange) return;
openContextModal({
title: "Добавление услуги",
modal: "addDealService",
innerProps: {
element: service,
onChange,
serviceIds
}
})
}
return ( return (
<BaseTable <BaseTable
ref={tableRef} ref={tableRef}
@@ -80,7 +95,6 @@ const DealServicesTable: FC<Props> = (
), ),
renderRowActions: ({row}) => ( renderRowActions: ({row}) => (
<Flex gap="md"> <Flex gap="md">
<Tooltip label="Удалить"> <Tooltip label="Удалить">
<ActionIcon onClick={() => { <ActionIcon onClick={() => {
if (onDelete) onDelete(row.original); if (onDelete) onDelete(row.original);
@@ -88,6 +102,13 @@ const DealServicesTable: FC<Props> = (
<IconTrash/> <IconTrash/>
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label="Редактировать">
<ActionIcon onClick={() => {
onEditClick(row.original);
}} variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
</Flex> </Flex>
) )
} as MRT_TableOptions<DealServiceSchema>} } as MRT_TableOptions<DealServiceSchema>}

View File

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

View File

@@ -29,7 +29,7 @@ export const useDealStatusChangeTableColumns = () => {
accessorKey: "comment", accessorKey: "comment",
header: "Комментарий", header: "Комментарий",
Cell: ({row}) => Cell: ({row}) =>
<Spoiler onDoubleClick={()=>{console.log("double click")}} maxHeight={80} showLabel={"Показать весь"} hideLabel={"Скрыть"}> <Spoiler maxHeight={80} showLabel={"Показать весь"} hideLabel={"Скрыть"}>
<Text style={{wordWrap: "break-word", wordBreak: "break-all", whiteSpace: "normal"}} span> <Text style={{wordWrap: "break-word", wordBreak: "break-all", whiteSpace: "normal"}} span>
{row.original.comment}<br/> {row.original.comment}<br/>
</Text> </Text>

View File

@@ -0,0 +1,10 @@
.bottom-panel {
padding: rem(10);
border-radius: rem(5);
@mixin light {
background-color: var(--mantine-color-gray-1);
}
@mixin dark {
background-color: var(--mantine-color-dark-5);
}
}

View File

@@ -11,6 +11,7 @@ import {IconBarcode, IconBox, IconCalendarUser, IconSettings} from "@tabler/icon
import DealStatusChangeTable from "../../components/DealStatusChangeTable/DealStatusChangeTable.tsx"; import DealStatusChangeTable from "../../components/DealStatusChangeTable/DealStatusChangeTable.tsx";
import DealEditDrawerGeneralTab from "./tabs/DealEditDrawerGeneralTab.tsx"; import DealEditDrawerGeneralTab from "./tabs/DealEditDrawerGeneralTab.tsx";
import {useQueryClient} from "@tanstack/react-query"; import {useQueryClient} from "@tanstack/react-query";
// import styles from './DealEditDrawer.module.css';
const useDealServicesTableState = () => { const useDealServicesTableState = () => {
const {selectedDeal, setSelectedDeal} = useDealPageContext(); const {selectedDeal, setSelectedDeal} = useDealPageContext();
@@ -18,11 +19,10 @@ const useDealServicesTableState = () => {
const onServiceUpdate = (service: DealServiceSchema) => { const onServiceUpdate = (service: DealServiceSchema) => {
if (!selectedDeal) return; if (!selectedDeal) return;
DealService.updateDealServiceQuantity({ DealService.updateDealService({
requestBody: { requestBody: {
dealId: selectedDeal.id, dealId: selectedDeal.id,
serviceId: service.service.id, service
quantity: service.quantity
} }
}).then(async ({ok, message}) => { }).then(async ({ok, message}) => {
@@ -72,14 +72,13 @@ const useDealServicesTableState = () => {
}) })
} }
const onServiceCreate = (service: DealServiceSchema) => { const onServiceCreate = (service: DealServiceSchema) => {
console.log('-------Drawer')
console.log(service);
if (!selectedDeal) return; if (!selectedDeal) return;
DealService.addDealService({ DealService.addDealService({
requestBody: { requestBody: {
dealId: selectedDeal.id, dealId: selectedDeal.id,
serviceId: service.service.id, serviceId: service.service.id,
quantity: service.quantity quantity: service.quantity,
price: service.price
} }
}).then(async ({ok, message}) => { }).then(async ({ok, message}) => {
if (!ok) { if (!ok) {
@@ -157,17 +156,14 @@ const useDealProductTableState = () => {
const onProductUpdate = (product: DealProductSchema) => { const onProductUpdate = (product: DealProductSchema) => {
if (!selectedDeal) return; if (!selectedDeal) return;
DealService.updateDealProductQuantity({ DealService.updateDealProduct({
requestBody: { requestBody: {
dealId: selectedDeal.id, dealId: selectedDeal.id,
productId: product.product.id, product: product
quantity: product.quantity
} }
}).then(async ({ok, message}) => { }).then(async ({ok, message}) => {
if (!ok) { notifications.guess(ok, {message});
notifications.guess(ok, {message}); if (!ok) return;
return;
}
await DealService.getDealById({dealId: selectedDeal.id}) await DealService.getDealById({dealId: selectedDeal.id})
.then(setSelectedDeal) .then(setSelectedDeal)
}) })
@@ -361,7 +357,6 @@ const DealEditDrawer: FC = () => {
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value={"services"}> <Tabs.Panel value={"services"}>
<Box p={rem(10)}> <Box p={rem(10)}>
<DealEditDrawerServicesTable/> <DealEditDrawerServicesTable/>
</Box> </Box>
</Tabs.Panel> </Tabs.Panel>
@@ -370,9 +365,15 @@ const DealEditDrawer: FC = () => {
<DealEditDrawerProductsTable/> <DealEditDrawerProductsTable/>
</Box> </Box>
</Tabs.Panel> </Tabs.Panel>
</Tabs> </Tabs>
{/*<Flex*/}
{/* h={"10%"}*/}
{/* align={'flex-end'}*/}
{/* justify={"flex-end"}*/}
{/*>*/}
{/*</Flex>*/}
</Drawer> </Drawer>
); );
} }

View File

@@ -23,8 +23,6 @@ const AddDealProductModal = ({
const validateServices = (services?: DealProductServiceSchema[]) => { const validateServices = (services?: DealProductServiceSchema[]) => {
if (!services || services.length == 0) return null; if (!services || services.length == 0) return null;
console.log("validating...");
console.log( services.filter(service => service.service === undefined))
return services.find(service => service.service === undefined) ? "Удалите пустые услуги" : null; return services.find(service => service.service === undefined) ? "Удалите пустые услуги" : null;
} }
const form = useForm<Partial<DealProductSchema>>({ const form = useForm<Partial<DealProductSchema>>({
@@ -39,11 +37,11 @@ const AddDealProductModal = ({
services: validateServices services: validateServices
} }
}); });
console.log(form.values);
const onClose = () => { const onClose = () => {
context.closeContextModal(id); context.closeContextModal(id);
} }
console.log(form.values)
return ( return (
<BaseFormModal <BaseFormModal

View File

@@ -2,19 +2,24 @@ import {ContextModalProps} from "@mantine/modals";
import BaseFormModal, {CreateEditFormProps} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx"; import BaseFormModal, {CreateEditFormProps} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import {DealServiceSchema} from "../../../client"; import {DealServiceSchema} from "../../../client";
import {useForm} from "@mantine/form"; import {useForm} from "@mantine/form";
import {NumberInput} from "@mantine/core"; import {ComboboxItem, ComboboxItemGroup, NumberInput, OptionsFilter} from "@mantine/core";
import ServiceSelect from "../../../components/ServiceSelect/ServiceSelect.tsx"; import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
import {ServiceType} from "../../../shared/enums/ServiceType.ts";
type Props = CreateEditFormProps<Partial<DealServiceSchema>>; type RestProps = {
serviceIds?: number[];
}
type Props = CreateEditFormProps<Partial<DealServiceSchema>> & RestProps;
const AddDealServiceModal = ({ const AddDealServiceModal = ({
context, context,
id, id,
innerProps innerProps
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const isEditing = 'element' in innerProps;
const form = useForm<Partial<DealServiceSchema>>({ const form = useForm<Partial<DealServiceSchema>>({
initialValues: { initialValues: isEditing ? innerProps.element : {
service: undefined, service: undefined,
quantity: 0, quantity: 1,
}, },
validate: { validate: {
service: (service?: DealServiceSchema['service']) => service !== undefined ? null : "Необходимо выбрать услугу", service: (service?: DealServiceSchema['service']) => service !== undefined ? null : "Необходимо выбрать услугу",
@@ -25,6 +30,16 @@ const AddDealServiceModal = ({
context.closeContextModal(id); context.closeContextModal(id);
} }
const serviceOptionsFilter = ({options}: { options: ComboboxItemGroup[] }) => {
if (!innerProps.serviceIds) return options;
const productServiceIds = innerProps.serviceIds;
return (options as ComboboxItemGroup[]).map(({items, group}) => {
return {
group,
items: items.filter(item => !productServiceIds.includes(parseInt((item as ComboboxItem).value)))
}
})
};
return ( return (
<BaseFormModal <BaseFormModal
@@ -34,10 +49,27 @@ const AddDealServiceModal = ({
onClose={onClose}> onClose={onClose}>
<BaseFormModal.Body> <BaseFormModal.Body>
<> <>
<ServiceSelect <ServiceWithPriceInput
placeholder={"Выберите услугу"} serviceProps={{
label={"Услуга"} ...form.getInputProps('service'),
{...form.getInputProps('service')} label: "Услуга",
placeholder: "Выберите услугу",
style: {width: '100%'},
disabled: isEditing,
filter: serviceOptionsFilter as OptionsFilter
}}
priceProps={{
...form.getInputProps('price'),
label: "Цена",
placeholder: "Введите цену",
style: {width: '100%'}
}}
quantity={form.values.quantity || 1}
containerProps={{
direction: "column",
style: {width: "100%"}
}}
filterType={ServiceType.DEAL_SERVICE}
/> />
<NumberInput <NumberInput
placeholder={"Введите количество"} placeholder={"Введите количество"}

View File

@@ -0,0 +1,26 @@
import {FC} from "react";
import {SegmentedControl, SegmentedControlProps} from "@mantine/core";
import {ServiceType} from "../../../../shared/enums/ServiceType.ts";
type Props = Omit<SegmentedControlProps, 'data'>;
const data = [
{
label: 'Для товара',
value: ServiceType.PRODUCT_SERVICE.toString()
},
{
label: 'Для сделки',
value: ServiceType.DEAL_SERVICE.toString()
}
]
const ServiceTypeSegmentedControl: FC<Props> = (props) => {
return (
<SegmentedControl
data={data}
{...props}
/>
)
}
export default ServiceTypeSegmentedControl

View File

@@ -44,7 +44,6 @@ const CreateServiceModal = ({
}) })
console.log(form.values)
const onCancelClick = () => { const onCancelClick = () => {
context.closeContextModal(id); context.closeContextModal(id);
} }

View File

@@ -9,4 +9,9 @@
padding: rem(5); padding: rem(5);
gap: rem(10); gap: rem(10);
display: flex; display: flex;
}
}
.top-panel-last-item {
margin-left: auto;
}

View File

@@ -1,4 +1,4 @@
import {FC} from "react"; import {FC, useState} from "react";
import ServicesTable from "../components/ServicesTable/ServicesTable.tsx"; import ServicesTable from "../components/ServicesTable/ServicesTable.tsx";
import useServicesList from "../hooks/useServicesList.tsx"; import useServicesList from "../hooks/useServicesList.tsx";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx"; import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
@@ -7,10 +7,12 @@ import {Button, Text} from "@mantine/core";
import {ServiceCategorySchema, ServiceSchema, ServiceService} from "../../../client"; import {ServiceCategorySchema, ServiceSchema, ServiceService} from "../../../client";
import {notifications} from "../../../shared/lib/notifications.ts"; import {notifications} from "../../../shared/lib/notifications.ts";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
import ServiceTypeSegmentedControl from "../components/ServiceTypeSegmentedControl/ServiceTypeSegmentedControl.tsx";
import {ServiceType} from "../../../shared/enums/ServiceType.ts";
export const ServicesPage: FC = () => { export const ServicesPage: FC = () => {
const {services, refetch} = useServicesList(); const {services, refetch} = useServicesList();
const [serviceType, setServiceType] = useState(ServiceType.DEAL_SERVICE)
// region Service create // region Service create
const onCreateClick = () => { const onCreateClick = () => {
modals.openContextModal({ modals.openContextModal({
@@ -90,13 +92,18 @@ export const ServicesPage: FC = () => {
<div className={styles['top-panel']}> <div className={styles['top-panel']}>
<Button onClick={onCreateClick} variant={"default"}>Создать услугу</Button> <Button onClick={onCreateClick} variant={"default"}>Создать услугу</Button>
<Button onClick={onCreateCategoryClick} variant={"default"}>Создать категорию</Button> <Button onClick={onCreateCategoryClick} variant={"default"}>Создать категорию</Button>
<ServiceTypeSegmentedControl
className={styles['top-panel-last-item']}
value={serviceType.toString()}
onChange={(event) => setServiceType(parseInt(event))}
/>
</div> </div>
</PageBlock> </PageBlock>
<PageBlock> <PageBlock>
<ServicesTable <ServicesTable
onDelete={onServiceDelete} onDelete={onServiceDelete}
onChange={onServiceUpdate} onChange={onServiceUpdate}
items={services} items={services.filter(service => service.serviceType == serviceType)}
/> />
</PageBlock> </PageBlock>
</div> </div>

View File

@@ -1,73 +1,14 @@
import {createLazyFileRoute} from "@tanstack/react-router"; import {createLazyFileRoute} from "@tanstack/react-router";
import ServiceSelectNew from "../components/Selects/ServiceSelectNew/ServiceSelectNew.tsx";
import {ServiceType} from "../shared/enums/ServiceType.ts";
import ServiceWithPriceInput from "../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
import {useEffect, useState} from "react";
import {ServiceSchema, ServiceService} from "../client";
import {NumberInput} from "@mantine/core";
import {isNumber} from "lodash";
import useServicesList from "../pages/ServicesPage/hooks/useServicesList.tsx";
export const Route = createLazyFileRoute('/test')({ export const Route = createLazyFileRoute('/test')({
component: TestPage component: TestPage
}) })
const data = [{
label: "test",
value: '0'
},
{
label: "test2",
value: '1'
}
]
function TestPage() { function TestPage() {
const [service, setService] = useState<ServiceSchema | undefined>({
"id": 96,
"name": "123",
"category": {"id": 1, "name": "Услуги по работе с товаром с учетом суммы всех сторон в см."},
"price": 0,
"serviceType": 1,
"priceRanges": [{"id": 4, "fromQuantity": 1, "toQuantity": 200, "price": 35}, {
"id": 3,
"fromQuantity": 201,
"toQuantity": 300,
"price": 24
}]
});
const [price, setPrice] = useState<number | string>();
const [q, setQ] = useState(1);
console.log('service:---------');
console.log(service);
console.log('price:---------');
console.log(price);
return ( return (
<> <>
<NumberInput
value={q}
onChange={event => {
if (!isNumber(event)) return;
setQ(event);
}}
/>
<ServiceWithPriceInput
priceProps={{
onChange: setPrice,
value: price,
label: "Цена"
}}
quantity={q}
serviceProps={{
onChange: setService,
value: service,
label: "Услуга"
}}
/>
</> </>
); );
} }