feat: total services amount and recalculating

This commit is contained in:
2024-10-22 19:31:51 +03:00
parent aa6f0364b5
commit bb34f12274
10 changed files with 153 additions and 43 deletions

View File

@@ -98,6 +98,8 @@ export type { DealProductSchema } from './models/DealProductSchema';
export type { DealProductServiceSchema } from './models/DealProductServiceSchema'; export type { DealProductServiceSchema } from './models/DealProductServiceSchema';
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 { DealRecalculatePriceRequest } from './models/DealRecalculatePriceRequest';
export type { DealRecalculatePriceResponse } from './models/DealRecalculatePriceResponse';
export type { DealSchema } from './models/DealSchema'; export type { DealSchema } from './models/DealSchema';
export type { DealServiceSchema } from './models/DealServiceSchema'; export type { DealServiceSchema } from './models/DealServiceSchema';
export type { DealServicesCopyRequest } from './models/DealServicesCopyRequest'; export type { DealServicesCopyRequest } from './models/DealServicesCopyRequest';

View File

@@ -8,5 +8,6 @@ export type DealProductServiceSchema = {
service: ServiceSchema; service: ServiceSchema;
price: number; price: number;
employees: Array<UserSchema>; employees: Array<UserSchema>;
isFixedPrice: boolean;
}; };

View File

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

View File

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

View File

@@ -9,5 +9,6 @@ export type DealServiceSchema = {
quantity: number; quantity: number;
price: number; price: number;
employees: Array<UserSchema>; employees: Array<UserSchema>;
isFixedPrice: boolean;
}; };

View File

@@ -34,6 +34,8 @@ import type { DealProductAddKitRequest } from '../models/DealProductAddKitReques
import type { DealProductAddKitResponse } from '../models/DealProductAddKitResponse'; import type { DealProductAddKitResponse } from '../models/DealProductAddKitResponse';
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 { DealRecalculatePriceRequest } from '../models/DealRecalculatePriceRequest';
import type { DealRecalculatePriceResponse } from '../models/DealRecalculatePriceResponse';
import type { DealSchema } from '../models/DealSchema'; import type { DealSchema } from '../models/DealSchema';
import type { DealServicesCopyRequest } from '../models/DealServicesCopyRequest'; import type { DealServicesCopyRequest } from '../models/DealServicesCopyRequest';
import type { DealServicesCopyResponse } from '../models/DealServicesCopyResponse'; import type { DealServicesCopyResponse } from '../models/DealServicesCopyResponse';
@@ -350,6 +352,26 @@ export class DealService {
}, },
}); });
} }
/**
* Recalculate Deal Price
* @returns DealRecalculatePriceResponse Successful Response
* @throws ApiError
*/
public static recalculateDealPrice({
requestBody,
}: {
requestBody: DealRecalculatePriceRequest,
}): CancelablePromise<DealRecalculatePriceResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/recalculate-price',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/** /**
* Services Add * Services Add
* @returns DealAddServicesResponse Successful Response * @returns DealAddServicesResponse Successful Response

View File

@@ -0,0 +1,34 @@
import { ActionIcon, ActionIconVariant, CheckboxProps, Tooltip } from "@mantine/core";
import { FC } from "react";
import { IconLock, IconLockOpen } from "@tabler/icons-react";
type RestProps = {
value?: boolean;
onChange?: (value: boolean) => void;
variant: ActionIconVariant
}
type Props = Omit<CheckboxProps, "onChange" | "value" | "variant"> & RestProps;
const LockCheckbox: FC<Props> = (props) => {
const { value = false } = props;
const getIcon = () => {
return value ? <IconLock /> : <IconLockOpen />;
};
const handleChange = () => {
if (props.onChange) {
props.onChange(!value);
}
};
return (
<Tooltip label={props.label}>
<ActionIcon
onClick={handleChange}
variant={props.variant}>
{getIcon()}
</ActionIcon>
</Tooltip>
);
};
export default LockCheckbox;

View File

@@ -10,7 +10,7 @@ import { ContextModalProps } from "@mantine/modals";
import { useForm, UseFormReturnType } from "@mantine/form"; import { useForm, UseFormReturnType } from "@mantine/form";
import { isNil, isNumber } from "lodash"; import { isNil, isNumber } from "lodash";
import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx"; import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
import { Flex } from "@mantine/core"; import { Checkbox, Flex, rem } from "@mantine/core";
import { ServiceType } from "../../../shared/enums/ServiceType.ts"; import { ServiceType } from "../../../shared/enums/ServiceType.ts";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../../redux/store.ts"; import { RootState } from "../../../redux/store.ts";
@@ -23,20 +23,21 @@ type RestProps = {
type Props = CreateEditFormProps<DealProductServiceSchema> & RestProps; type Props = CreateEditFormProps<DealProductServiceSchema> & RestProps;
const ProductServiceFormModal = ({ const ProductServiceFormModal = ({
context, context,
id, id,
innerProps, innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const authState = useSelector((state: RootState) => state.auth); const authState = useSelector((state: RootState) => state.auth);
const isEditing = "onChange" in innerProps; const isEditing = "onChange" in innerProps;
const initialValues: Partial<DealProductServiceSchema> = isEditing const initialValues: Partial<DealProductServiceSchema> = isEditing
? innerProps.element ? innerProps.element
: { : {
service: undefined, service: undefined,
price: undefined, price: undefined,
employees: [], employees: [],
}; isFixedPrice: false,
};
const form = useForm<Partial<DealProductServiceSchema>>({ const form = useForm<Partial<DealProductServiceSchema>>({
initialValues, initialValues,
validate: { validate: {
@@ -57,7 +58,7 @@ const ProductServiceFormModal = ({
closeOnSubmit> closeOnSubmit>
<BaseFormModal.Body> <BaseFormModal.Body>
<> <>
<Flex w={"100%"}> <Flex w={"100%"} direction={"column"} gap={rem(10)}>
<ServiceWithPriceInput <ServiceWithPriceInput
category={innerProps.category} category={innerProps.category}
serviceProps={{ serviceProps={{
@@ -85,6 +86,11 @@ const ProductServiceFormModal = ({
lockOnEdit={isEditing} lockOnEdit={isEditing}
quantity={innerProps.quantity} quantity={innerProps.quantity}
/> />
<Checkbox
{...form.getInputProps("isFixedPrice", { type: "checkbox" })}
label={"Зафиксировать цену"}
placeholder={"Зафиксировать цену"}
/>
</Flex> </Flex>
</> </>
</BaseFormModal.Body> </BaseFormModal.Body>

View File

@@ -1,21 +1,7 @@
import { CRUDTableProps } from "../../../../../../types/CRUDTable.tsx"; import { CRUDTableProps } from "../../../../../../types/CRUDTable.tsx";
import { import { DealServiceSchema, GetServiceKitSchema, UserSchema } from "../../../../../../client";
DealServiceSchema,
GetServiceKitSchema,
UserSchema,
} from "../../../../../../client";
import { FC, useState } from "react"; import { FC, useState } from "react";
import { import { ActionIcon, Button, Flex, Modal, NumberInput, rem, Text, Title, Tooltip } from "@mantine/core";
ActionIcon,
Button,
Flex,
Modal,
NumberInput,
rem,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { IconTrash, IconUsersGroup } from "@tabler/icons-react"; import { IconTrash, IconUsersGroup } from "@tabler/icons-react";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { isNumber } from "lodash"; import { isNumber } from "lodash";
@@ -24,18 +10,19 @@ import { ServiceType } from "../../../../../../shared/enums/ServiceType.ts";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts"; import { RootState } from "../../../../../../redux/store.ts";
import useDealProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx"; import useDealProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
import LockCheckbox from "../../../../../../components/LockCheckbox/LockCheckbox.tsx";
type RestProps = { type RestProps = {
onKitAdd?: (kit: GetServiceKitSchema) => void; onKitAdd?: (kit: GetServiceKitSchema) => void;
}; };
type Props = CRUDTableProps<DealServiceSchema> & RestProps; type Props = CRUDTableProps<DealServiceSchema> & RestProps;
const DealServicesTable: FC<Props> = ({ const DealServicesTable: FC<Props> = ({
items, items,
onDelete, onDelete,
onCreate, onCreate,
onChange, onChange,
onKitAdd, onKitAdd,
}) => { }) => {
const authState = useSelector((state: RootState) => state.auth); const authState = useSelector((state: RootState) => state.auth);
const { dealState } = useDealProductAndServiceTabState(); const { dealState } = useDealProductAndServiceTabState();
@@ -78,7 +65,13 @@ const DealServicesTable: FC<Props> = ({
price, price,
}); });
}; };
const onLockChange = (item: DealServiceSchema, isLocked: boolean) => {
if (!onChange) return;
onChange({
...item,
isFixedPrice: isLocked,
});
};
const onEmployeeClick = (item: DealServiceSchema) => { const onEmployeeClick = (item: DealServiceSchema) => {
if (!onChange) return; if (!onChange) return;
setCurrentService(item); setCurrentService(item);
@@ -91,7 +84,7 @@ const DealServicesTable: FC<Props> = ({
const getCurrentEmployees = (): UserSchema[] => { const getCurrentEmployees = (): UserSchema[] => {
if (!currentService) return []; if (!currentService) return [];
const item = items.find( const item = items.find(
i => i.service.id === currentService.service.id i => i.service.id === currentService.service.id,
); );
if (!item) return []; if (!item) return [];
return item.employees; return item.employees;
@@ -169,7 +162,9 @@ const DealServicesTable: FC<Props> = ({
isNumber(event) && isNumber(event) &&
onQuantityChange(service, event) onQuantityChange(service, event)
} }
value={service.quantity} value={service.quantity}
/> />
<NumberInput <NumberInput
flex={1} flex={1}
@@ -179,7 +174,22 @@ const DealServicesTable: FC<Props> = ({
} }
suffix={"₽"} suffix={"₽"}
value={service.price} value={service.price}
disabled={authState.isGuest || isLocked} disabled={authState.isGuest || isLocked || service.isFixedPrice}
rightSectionProps={{
style: {
display: "flex",
cursor: "pointer",
pointerEvents: "auto",
},
}}
rightSection={
<LockCheckbox
label={"Зафиксировать цену"}
variant={"default"}
value={service.isFixedPrice}
onChange={value => onLockChange(service, value)}
/>
}
/> />
</Flex> </Flex>
))} ))}
@@ -192,7 +202,7 @@ const DealServicesTable: FC<Props> = ({
Итог:{" "} Итог:{" "}
{items.reduce( {items.reduce(
(acc, item) => acc + item.price * item.quantity, (acc, item) => acc + item.price * item.quantity,
0 0,
)} )}
</Title> </Title>

View File

@@ -8,13 +8,24 @@ import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
import { notifications } from "../../../../../shared/lib/notifications.ts"; import { notifications } from "../../../../../shared/lib/notifications.ts";
const useDealState = () => { const useDealState = () => {
const { selectedDeal, setSelectedDeal } = useDealPageContext(); const { selectedDeal, setSelectedDeal } = useDealPageContext();
const recalculate = async () => {
return DealService.recalculateDealPrice({
requestBody: {
dealId: selectedDeal?.id || -1,
},
});
};
const refetch = async () => { const refetch = async () => {
if (!selectedDeal) return; if (!selectedDeal) return;
const { ok, message } = await recalculate();
if (!ok) notifications.guess(ok, { message });
return DealService.getDealById({ dealId: selectedDeal.id }).then( return DealService.getDealById({ dealId: selectedDeal.id }).then(
deal => { deal => {
setSelectedDeal(deal); setSelectedDeal(deal);
} },
); );
}; };
return { return {
@@ -25,6 +36,9 @@ const useDealState = () => {
const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => { const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
const { deal, refetch } = useDealState(); const { deal, refetch } = useDealState();
const refetchAndRecalculate = async () => {
await refetch();
};
const onCreate = (item: DealServiceSchema) => { const onCreate = (item: DealServiceSchema) => {
if (!deal) return; if (!deal) return;
DealService.addDealService({ DealService.addDealService({
@@ -36,7 +50,7 @@ const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
}, },
}).then(async ({ ok, message }) => { }).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message }); if (!ok) notifications.guess(ok, { message });
if (ok) await refetch(); if (ok) await refetchAndRecalculate();
}); });
}; };
const onDelete = (item: DealServiceSchema) => { const onDelete = (item: DealServiceSchema) => {
@@ -48,7 +62,7 @@ const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
}, },
}).then(async ({ ok, message }) => { }).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message }); if (!ok) notifications.guess(ok, { message });
if (ok) await refetch(); if (ok) await refetchAndRecalculate();
}); });
}; };
const onChange = (item: DealServiceSchema) => { const onChange = (item: DealServiceSchema) => {
@@ -60,7 +74,7 @@ const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
}, },
}).then(async ({ ok, message }) => { }).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message }); if (!ok) notifications.guess(ok, { message });
if (ok) await refetch(); if (ok) await refetchAndRecalculate();
}); });
}; };
return { return {
@@ -73,6 +87,9 @@ const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
const useDealProductsState = (): CRUDTableProps<DealProductSchema> => { const useDealProductsState = (): CRUDTableProps<DealProductSchema> => {
const { deal, refetch } = useDealState(); const { deal, refetch } = useDealState();
const refetchAndRecalculate = async () => {
await refetch();
};
const onCreate = (item: DealProductSchema) => { const onCreate = (item: DealProductSchema) => {
if (!deal) return; if (!deal) return;
DealService.addDealProduct({ DealService.addDealProduct({
@@ -82,7 +99,7 @@ const useDealProductsState = (): CRUDTableProps<DealProductSchema> => {
}, },
}).then(async ({ ok, message }) => { }).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message }); if (!ok) notifications.guess(ok, { message });
if (ok) await refetch(); if (ok) await refetchAndRecalculate();
}); });
}; };
const onDelete = (item: DealProductSchema) => { const onDelete = (item: DealProductSchema) => {
@@ -94,7 +111,7 @@ const useDealProductsState = (): CRUDTableProps<DealProductSchema> => {
}, },
}).then(async ({ ok, message }) => { }).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message }); if (!ok) notifications.guess(ok, { message });
if (ok) await refetch(); if (ok) await refetchAndRecalculate();
}); });
}; };
const onChange = (item: DealProductSchema) => { const onChange = (item: DealProductSchema) => {
@@ -106,7 +123,7 @@ const useDealProductsState = (): CRUDTableProps<DealProductSchema> => {
}, },
}).then(async ({ ok, message }) => { }).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message }); if (!ok) notifications.guess(ok, { message });
if (ok) await refetch(); if (ok) await refetchAndRecalculate();
}); });
}; };
return { return {