feat: split bills for one deal

This commit is contained in:
2025-05-09 13:33:17 +04:00
parent e4f8e90ae6
commit d03ba66ebb
11 changed files with 112 additions and 45 deletions

View File

@@ -4,6 +4,5 @@
/* eslint-disable */ /* eslint-disable */
export type CancelCardBillRequest = { export type CancelCardBillRequest = {
cardId: number; cardId: number;
force?: (boolean | null);
}; };

View File

@@ -7,6 +7,6 @@ export type CardGroupSchema = {
id: number; id: number;
name?: (string | null); name?: (string | null);
lexorank: string; lexorank: string;
billRequest?: (GroupBillRequestSchema | null); billRequests?: Array<GroupBillRequestSchema>;
}; };

View File

@@ -35,7 +35,7 @@ export type CardSchema = {
clientId: (number | null); clientId: (number | null);
client: (ClientSchema | null); client: (ClientSchema | null);
shippingWarehouse?: (ShippingWarehouseSchema | string | null); shippingWarehouse?: (ShippingWarehouseSchema | string | null);
billRequest?: (CardBillRequestSchema | null); billRequests?: Array<CardBillRequestSchema>;
group?: (CardGroupSchema | null); group?: (CardGroupSchema | null);
manager?: (UserSchema | null); manager?: (UserSchema | null);
pallets?: Array<PalletSchema>; pallets?: Array<PalletSchema>;

View File

@@ -24,7 +24,7 @@ export type CardSummary = {
attributes: Array<CardAttributeSchema>; attributes: Array<CardAttributeSchema>;
shipmentWarehouseId: (number | null); shipmentWarehouseId: (number | null);
shipmentWarehouseName: (string | null); shipmentWarehouseName: (string | null);
billRequest?: (CardBillRequestSchema | null); billRequests?: Array<CardBillRequestSchema>;
group?: (CardGroupSchema | null); group?: (CardGroupSchema | null);
}; };

View File

@@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, useEffect, useState } from "react";
import { CardService, CardSummary } from "../../../../client"; import { CardService, CardSummary } from "../../../../client";
import styles from "./CardSummaryItem.module.css"; import styles from "./CardSummaryItem.module.css";
@@ -14,6 +14,7 @@ import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import CardTags from "../CardTags/CardTags.tsx"; import CardTags from "../CardTags/CardTags.tsx";
import CardAttributesInSummaryItem from "../CardAttributesInSummaryItem/CardAttributesInSummaryItem.tsx"; import CardAttributesInSummaryItem from "../CardAttributesInSummaryItem/CardAttributesInSummaryItem.tsx";
import { ModuleNames } from "../../../../modules/modules.tsx"; import { ModuleNames } from "../../../../modules/modules.tsx";
import isDealPaid from "../../../../pages/CardsPage/utils/isDealPaid.ts";
type Props = { type Props = {
cardSummary: CardSummary; cardSummary: CardSummary;
@@ -25,6 +26,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const { setSelectedCard } = useCardPageContext(); const { setSelectedCard } = useCardPageContext();
const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState(); const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState();
const [isPaid, setIsPaid] = useState<boolean>(false);
const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject); const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
const isClientIncluded = isModuleInProject(ModuleNames.CLIENTS, selectedProject); const isClientIncluded = isModuleInProject(ModuleNames.CLIENTS, selectedProject);
@@ -34,13 +36,14 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
setSelectedCard(card); setSelectedCard(card);
}); });
}; };
const isPaid = () => {
return cardSummary.billRequest?.paid || cardSummary.group?.billRequest?.paid;
};
const isLockedInsideGroup = () => { const isLockedInsideGroup = () => {
return cardSummary.group && !cardSummary.group.billRequest; return cardSummary.group && !cardSummary.group.billRequests;
}; };
useEffect(() => {
setIsPaid(isDealPaid(cardSummary));
}, []);
return ( return (
<div <div
onContextMenu={showContextMenu([ onContextMenu={showContextMenu([
@@ -147,7 +150,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
</Popover> </Popover>
)} )}
</CopyButton> </CopyButton>
{isPaid() && ( {isPaid && (
<Tooltip label={"Оплачен"}> <Tooltip label={"Оплачен"}>
<ThemeIcon variant={"transparent"}> <ThemeIcon variant={"transparent"}>
<IconCheck /> <IconCheck />

View File

@@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, useEffect, useState } from "react";
import styles from "./ProductAndServiceTab.module.css"; import styles from "./ProductAndServiceTab.module.css";
import ProductView from "./components/ProductView/ProductView.tsx"; import ProductView from "./components/ProductView/ProductView.tsx";
import { Button, Checkbox, Divider, Flex, Group, rem, ScrollArea, Stack, Text, Title } from "@mantine/core"; import { Button, Checkbox, Divider, Flex, Group, rem, ScrollArea, Stack, Text, Title } from "@mantine/core";
@@ -20,10 +20,17 @@ import GeneralDataForm from "./components/GeneralDataForm/GeneralDataForm.tsx";
import PrintDealBarcodesButton from "./components/PrintDealBarcodesButton/PrintDealBarcodesButton.tsx"; import PrintDealBarcodesButton from "./components/PrintDealBarcodesButton/PrintDealBarcodesButton.tsx";
import PaymentLinkButton from "./components/PaymentLinkButton/PaymentLinkButton.tsx"; import PaymentLinkButton from "./components/PaymentLinkButton/PaymentLinkButton.tsx";
import isValidInn from "../../../../pages/ClientsPage/utils/isValidInn.ts"; import isValidInn from "../../../../pages/ClientsPage/utils/isValidInn.ts";
import isDealPaid, { isDealLocked } from "../../../../pages/CardsPage/utils/isDealPaid.ts";
const ProductAndServiceTab: FC = () => { const ProductAndServiceTab: FC = () => {
const { cardState, cardServicesState, cardProductsState } = useCardProductAndServiceTabState(); const { cardState, cardServicesState, cardProductsState } = useCardProductAndServiceTabState();
const isLocked = Boolean(cardState.card?.billRequest || cardState.card?.group?.billRequest); const isLocked = isDealLocked(cardState.card);
const [paid, setPaid] = useState<boolean>(false);
useEffect(() => {
setPaid(isDealPaid(cardState.card));
}, []);
const onAddProductClick = () => { const onAddProductClick = () => {
if (!cardProductsState.onCreate || !cardState.card || !cardState.card.clientId) return; if (!cardProductsState.onCreate || !cardState.card || !cardState.card.clientId) return;
const productIds = cardState.card.products.map( const productIds = cardState.card.products.map(
@@ -222,7 +229,7 @@ const ProductAndServiceTab: FC = () => {
<div <div
className={classNames( className={classNames(
styles["container"], styles["container"],
cardState.card?.billRequest && styles["container-disabled"], cardState.card?.billRequests?.length && styles["container-disabled"],
)}> )}>
<div className={styles["products-list"]}> <div className={styles["products-list"]}>
<ScrollArea offsetScrollbars> <ScrollArea offsetScrollbars>
@@ -252,7 +259,7 @@ const ProductAndServiceTab: FC = () => {
<PrintDealBarcodesButton card={cardState.card} /> <PrintDealBarcodesButton card={cardState.card} />
<Checkbox <Checkbox
label={"Оплачен"} label={"Оплачен"}
checked={cardState.card.billRequest?.paid || cardState.card.group?.billRequest?.paid || false} checked={paid}
disabled disabled
/> />
</Group> </Group>

View File

@@ -31,7 +31,7 @@ const CardServicesTable: FC<Props> = ({
const authState = useSelector((state: RootState) => state.auth); const authState = useSelector((state: RootState) => state.auth);
const { cardState } = useCardProductAndServiceTabState(); const { cardState } = useCardProductAndServiceTabState();
const isLocked = Boolean(cardState.card?.billRequest); const isLocked = Boolean(cardState.card?.billRequests) || Boolean(cardState.card?.group?.billRequests);
const [currentService, setCurrentService] = useState< const [currentService, setCurrentService] = useState<
CardServiceSchema | undefined CardServiceSchema | undefined

View File

@@ -3,25 +3,14 @@ import ButtonCopy from "../../../../../../components/ButtonCopy/ButtonCopy.tsx";
import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx"; import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
import { getCurrentDateTimeForFilename } from "../../../../../../shared/lib/date.ts"; import { getCurrentDateTimeForFilename } from "../../../../../../shared/lib/date.ts";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import { Button, Popover, Stack } from "@mantine/core";
type Props = { type Props = {
card: CardSchema; card: CardSchema;
} }
const PaymentLinkButton = ({ card }: Props) => { const PaymentLinkButton = ({ card }: Props) => {
const billRequestPdfUrl = card?.billRequest?.pdfUrl || card?.group?.billRequest?.pdfUrl; if ((!card.billRequests || card.billRequests.length === 0) && (!card?.group?.billRequests || card?.group?.billRequests.length === 0)) {
if (billRequestPdfUrl) {
return (
<ButtonCopy
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
value={billRequestPdfUrl}
>
Ссылка на оплату
</ButtonCopy>
);
}
return ( return (
<ButtonCopyControlled <ButtonCopyControlled
onCopyClick={() => { onCopyClick={() => {
@@ -38,6 +27,42 @@ const PaymentLinkButton = ({ card }: Props) => {
Ссылка на оплату (PDF) Ссылка на оплату (PDF)
</ButtonCopyControlled> </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; export default PaymentLinkButton;

View File

@@ -11,6 +11,7 @@ import SimpleUsersTable from "../../../../../../pages/CardsPage/components/Simpl
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts"; import { RootState } from "../../../../../../redux/store.ts";
import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx"; import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
import { isDealLocked } from "../../../../../../pages/CardsPage/utils/isDealPaid.ts";
type RestProps = { type RestProps = {
quantity: number; quantity: number;
@@ -27,9 +28,9 @@ const ProductServicesTable: FC<Props> = ({
onChange, onChange,
onCopyServices, onCopyServices,
onKitAdd, onKitAdd,
}) => { }) => {
const { cardState } = useCardProductAndServiceTabState(); const { cardState } = useCardProductAndServiceTabState();
const isLocked = Boolean(cardState.card?.billRequest); const isLocked = isDealLocked(cardState.card);
const authState = useSelector((state: RootState) => state.auth); const authState = useSelector((state: RootState) => state.auth);
const columns = useProductServicesTableColumns({ data: items, quantity }); const columns = useProductServicesTableColumns({ data: items, quantity });
@@ -78,7 +79,7 @@ const ProductServicesTable: 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;
@@ -158,7 +159,7 @@ const ProductServicesTable: FC<Props> = ({
<ActionIcon <ActionIcon
onClick={() => onClick={() =>
onEmployeeClick( onEmployeeClick(
row.original row.original,
) )
} }
variant={"default"}> variant={"default"}>

View File

@@ -47,7 +47,7 @@ const ProductView: FC<Props> = ({
if (!onChange) return; if (!onChange) return;
onChange(item); onChange(item);
}, 200); }, 200);
const isLocked = Boolean(cardState.card?.billRequest); const isLocked = Boolean(cardState.card?.billRequests) || Boolean(cardState.card?.group?.billRequests);
const onDeleteClick = () => { const onDeleteClick = () => {
if (!onDelete) return; if (!onDelete) return;
onDelete(product); onDelete(product);

View File

@@ -0,0 +1,32 @@
import { CardSchema, CardSummary } from "../../../client";
const isDealPaid = (deal: CardSummary | CardSchema | undefined): boolean => {
if ((!deal?.billRequests || deal?.billRequests?.length === 0) && (!deal?.group?.billRequests || deal?.group?.billRequests?.length === 0)) {
return false;
}
if (deal.billRequests && deal.billRequests?.length !== 0) {
for (let i = 0; i < deal.billRequests.length; i++) {
if (!deal.billRequests[i].paid) {
return false;
}
}
}
if (deal.group?.billRequests && deal.group?.billRequests.length !== 0) {
for (let i = 0; i < deal.group.billRequests.length; i++) {
if (!deal.group.billRequests[i].paid) {
return false;
}
}
}
return true;
};
export default isDealPaid;
export const isDealLocked = (deal: CardSummary | CardSchema | undefined): boolean => {
return !!(
(deal?.billRequests && deal?.billRequests?.length !== 0) ||
(deal?.group?.billRequests && deal?.group?.billRequests?.length !== 0)
);
};