fix: total price for deal

This commit is contained in:
2025-09-19 18:15:55 +04:00
parent 05edac23f1
commit e7416155be
12 changed files with 69 additions and 261 deletions

View File

@ -0,0 +1,22 @@
import { Flex, Stack } from "@mantine/core";
import DealServicesTable from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealInfoView/components/DealServicesTable/DealServicesTable";
import ProductsActions from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealInfoView/components/ProductsActions/ProductsActions";
import TotalPriceLabel from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealInfoView/components/TotalPriceLabel/TotalPriceLabel";
import styles from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/FulfillmentBaseTab.module.css";
const DealInfoView = () => (
<Stack
flex={2}
gap={"sm"}>
<Flex
gap={"sm"}
direction={"column"}
className={styles.container}>
<DealServicesTable />
<ProductsActions />
</Flex>
<TotalPriceLabel />
</Stack>
);
export default DealInfoView;

View File

@ -1,10 +1,10 @@
import { FC } from "react"; import { FC } from "react";
import { Flex, ScrollArea } from "@mantine/core"; import { Flex, ScrollArea } from "@mantine/core";
import DealServiceRow from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/components/DealServiceRow"; import DealServicesTitle from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealInfoView/components/DealServicesTable/components/DealServicesTitle";
import DealServicesTitle from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/components/DealServicesTitle"; import DealServicesTotalLabel from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealInfoView/components/DealServicesTable/components/DealServicesTotalLabel";
import DealServicesTotalLabel from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/components/DealServicesTotalLabel"; import ServicesActions from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealInfoView/components/DealServicesTable/components/ServicesActions";
import ServicesActions from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/components/ServicesActions";
import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext"; import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
import DealServiceRow from "./components/DealServiceRow";
const DealServicesTable: FC = () => { const DealServicesTable: FC = () => {
const { dealServicesList, dealServicesCrud } = useFulfillmentBaseContext(); const { dealServicesList, dealServicesCrud } = useFulfillmentBaseContext();
@ -15,7 +15,7 @@ const DealServicesTable: FC = () => {
<Flex <Flex
direction={"column"} direction={"column"}
gap={"sm"} gap={"sm"}
h={"88vh"}> h={"78vh"}>
<DealServicesTitle /> <DealServicesTitle />
<ScrollArea <ScrollArea
flex={1} flex={1}

View File

@ -0,0 +1,40 @@
import { useMemo } from "react";
import { Flex, Title } from "@mantine/core";
import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
import styles from "../../../../FulfillmentBaseTab.module.css";
const TotalPriceLabel = () => {
const {
dealServicesList: { dealServices },
dealProductsList: { dealProducts },
} = useFulfillmentBaseContext();
const totalPrice = useMemo(() => {
const productServicesPrice = dealProducts.reduce(
(acc, row) =>
acc +
row.productServices.reduce(
(acc2, row2) => acc2 + row2.price * row.quantity,
0
),
0
);
const cardServicesPrice = dealServices.reduce(
(acc, row) => acc + row.price * row.quantity,
0
);
return cardServicesPrice + productServicesPrice;
}, [dealServices, dealProducts]);
return (
<Flex
direction={"column"}
className={styles.container}>
<Title order={3}>
Общая стоимость всех услуг: {totalPrice.toLocaleString("ru")}
</Title>
</Flex>
);
};
export default TotalPriceLabel;

View File

@ -1,9 +1,7 @@
import { Flex, ScrollArea, Stack } from "@mantine/core"; import { Flex, ScrollArea, Stack } from "@mantine/core";
import DealServicesTable from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/DealServicesTable"; import DealInfoView from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealInfoView/DealInfoView";
import ProductsActions from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductsActions/ProductsActions";
import ProductView from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductView/ProductView"; import ProductView from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductView/ProductView";
import { useFulfillmentBaseContext } from "../../contexts/FulfillmentBaseContext"; import { useFulfillmentBaseContext } from "../../contexts/FulfillmentBaseContext";
import styles from "../../FulfillmentBaseTab.module.css";
const FulfillmentBaseTabBody = () => { const FulfillmentBaseTabBody = () => {
const { dealProductsList } = useFulfillmentBaseContext(); const { dealProductsList } = useFulfillmentBaseContext();
@ -25,13 +23,7 @@ const FulfillmentBaseTabBody = () => {
))} ))}
</Stack> </Stack>
</ScrollArea> </ScrollArea>
<Stack <DealInfoView />
flex={2}
gap={"sm"}
className={styles.container}>
<DealServicesTable />
<ProductsActions />
</Stack>
</Flex> </Flex>
); );
}; };

View File

@ -1,115 +0,0 @@
import ShippingWarehouseAutocomplete
from "../../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
import { CardService, ShippingWarehouseSchema } from "../../../../../../client";
import { useForm } from "@mantine/form";
import { useCardPageContext } from "../../../../../../pages/CardsPage/contexts/CardPageContext.tsx";
import { Button, Checkbox, Stack } from "@mantine/core";
import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { useEffect, useState } from "react";
import { isEqual } from "lodash";
import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts";
type GeneralDataFormType = {
shippingWarehouse?: ShippingWarehouseSchema | null | string;
isServicesProfitAccounted: boolean;
}
const GeneralDataForm = () => {
const { selectedCard: card, refetchCard } = useCardPageContext();
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
if (!card) return <></>;
const [initialValues, setInitialValues] = useState<GeneralDataFormType>(card);
const form = useForm<GeneralDataFormType>({
initialValues,
});
useEffect(() => {
const data = card ?? {};
setInitialValues(data);
form.setValues(data);
}, [card]);
const isShippingWarehouse = (
value: ShippingWarehouseSchema | string | null | undefined,
): value is ShippingWarehouseSchema => {
return !!value && !["string"].includes(typeof value);
};
const onSubmit = (values: GeneralDataFormType) => {
if (!card) return;
const shippingWarehouse = isShippingWarehouse(values.shippingWarehouse)
? values.shippingWarehouse.name
: values.shippingWarehouse;
CardService.updateProductsAndServicesGeneralInfo({
requestBody: {
cardId: card.id,
data: {
...values,
shippingWarehouse,
},
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
refetchCard();
})
.catch(err => console.log(err));
};
return (
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
<Stack>
<ShippingWarehouseAutocomplete
placeholder={isDealsViewer ? "" : "Введите склад отгрузки"}
label={"Склад отгрузки"}
value={
isShippingWarehouse(
form.values.shippingWarehouse,
)
? form.values.shippingWarehouse
: undefined
}
onChange={event => {
if (isShippingWarehouse(event)) {
form.getInputProps(
"shippingWarehouse",
).onChange(event.name);
return;
}
form.getInputProps(
"shippingWarehouse",
).onChange(event);
}}
readOnly={isDealsViewer}
/>
{!isDealsViewer && (
<>
<Checkbox
label={"Учет выручки в статистике"}
{...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
/>
<Button
type={"submit"}
variant={"default"}
disabled={isEqual(initialValues, form.values)}
>
Сохранить
</Button>
</>
)}
</Stack>
</form>
);
};
export default GeneralDataForm;

View File

@ -1,68 +0,0 @@
import { CardSchema } from "../../../../../../client";
import ButtonCopy from "../../../../../../components/ButtonCopy/ButtonCopy.tsx";
import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
import { getCurrentDateTimeForFilename } from "../../../../../../shared/lib/date.ts";
import FileSaver from "file-saver";
import { Button, Popover, Stack } from "@mantine/core";
type Props = {
card: CardSchema;
}
const PaymentLinkButton = ({ card }: Props) => {
if ((!card.billRequests || card.billRequests.length === 0) && (!card?.group?.billRequests || card?.group?.billRequests.length === 0)) {
return (
<ButtonCopyControlled
onCopyClick={() => {
const date =
getCurrentDateTimeForFilename();
FileSaver.saveAs(
`${import.meta.env.VITE_API_URL}/card/billing-document/${card.id}`,
`bill_${card.id}_${date}.pdf`,
);
}}
copied={false}
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
>
Ссылка на оплату (PDF)
</ButtonCopyControlled>
);
}
const requests = (card?.group ? card?.group?.billRequests : card.billRequests) ?? [];
const urls = requests.map(request => request.pdfUrl).filter(url => url !== null);
if (urls.length === 1) {
return (
<ButtonCopy
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
value={urls[0]}
>
Ссылка на оплату
</ButtonCopy>
);
}
return (
<Popover width={380} position="bottom" withArrow shadow="md">
<Popover.Target>
<Button variant={"default"}>Ссылки на оплату</Button>
</Popover.Target>
<Popover.Dropdown>
<Stack gap={"md"}>
{urls.map((url, i) => (
<ButtonCopy
key={i}
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
value={url}
>
{`Ссылка на оплату (часть ${String(i + 1)})`}
</ButtonCopy>
))}
</Stack>
</Popover.Dropdown>
</Popover>
);
};
export default PaymentLinkButton;

View File

@ -1,63 +0,0 @@
import { ActionIcon, Group, Tooltip } from "@mantine/core";
import styles from "../../../../../../pages/CardsPage/ui/CardsPage.module.css";
import { CardSchema, CardService } from "../../../../../../client";
import { base64ToBlob } from "../../../../../../shared/lib/utils.ts";
import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { IconBarcode, IconPrinter } from "@tabler/icons-react";
type Props = {
card: CardSchema;
}
const PrintDealBarcodesButton = ({ card }: Props) => {
return (
<Group wrap={"nowrap"}>
<Tooltip
className={styles["print-deals-button"]}
label={"Распечатать штрихкоды сделки"}
>
<ActionIcon
onClick={async () => {
const response =
await CardService.getCardProductsBarcodesPdf({
requestBody: {
cardId: card.id,
},
});
const pdfBlob = base64ToBlob(
response.base64String,
response.mimeType,
);
const pdfUrl = URL.createObjectURL(pdfBlob);
const pdfWindow = window.open(pdfUrl);
if (!pdfWindow) {
notifications.error({ message: "Ошибка" });
return;
}
pdfWindow.onload = () => {
pdfWindow.print();
};
}}
variant={"default"}>
<IconBarcode />
</ActionIcon>
</Tooltip>
<Tooltip label={"Распечатать сделку"}>
<ActionIcon
onClick={() => {
const pdfWindow = window.open(
`${import.meta.env.VITE_API_URL}/card/tech-spec/${card.id}`,
);
if (!pdfWindow) return;
pdfWindow.print();
}}
variant={"default"}>
<IconPrinter />
</ActionIcon>
</Tooltip>
</Group>
);
};
export default PrintDealBarcodesButton;