feat: split bills for one deal
This commit is contained in:
@@ -4,6 +4,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export type CancelCardBillRequest = {
|
export type CancelCardBillRequest = {
|
||||||
cardId: number;
|
cardId: number;
|
||||||
force?: (boolean | null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -3,19 +3,40 @@ 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)) {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (billRequestPdfUrl) {
|
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 (
|
return (
|
||||||
<ButtonCopy
|
<ButtonCopy
|
||||||
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
||||||
value={billRequestPdfUrl}
|
value={urls[0]}
|
||||||
>
|
>
|
||||||
Ссылка на оплату
|
Ссылка на оплату
|
||||||
</ButtonCopy>
|
</ButtonCopy>
|
||||||
@@ -23,21 +44,25 @@ const PaymentLinkButton = ({ card }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonCopyControlled
|
<Popover width={380} position="bottom" withArrow shadow="md">
|
||||||
onCopyClick={() => {
|
<Popover.Target>
|
||||||
const date =
|
<Button variant={"default"}>Ссылки на оплату</Button>
|
||||||
getCurrentDateTimeForFilename();
|
</Popover.Target>
|
||||||
FileSaver.saveAs(
|
<Popover.Dropdown>
|
||||||
`${import.meta.env.VITE_API_URL}/card/billing-document/${card.id}`,
|
<Stack gap={"md"}>
|
||||||
`bill_${card.id}_${date}.pdf`,
|
{urls.map((url, i) => (
|
||||||
);
|
<ButtonCopy
|
||||||
}}
|
key={i}
|
||||||
copied={false}
|
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
||||||
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
value={url}
|
||||||
>
|
>
|
||||||
Ссылка на оплату (PDF)
|
{`Ссылка на оплату (часть ${String(i + 1)})`}
|
||||||
</ButtonCopyControlled>
|
</ButtonCopy>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PaymentLinkButton;
|
export default PaymentLinkButton;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -20,16 +21,16 @@ type RestProps = {
|
|||||||
|
|
||||||
type Props = CRUDTableProps<CardProductServiceSchema> & RestProps;
|
type Props = CRUDTableProps<CardProductServiceSchema> & RestProps;
|
||||||
const ProductServicesTable: FC<Props> = ({
|
const ProductServicesTable: FC<Props> = ({
|
||||||
items,
|
items,
|
||||||
quantity,
|
quantity,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
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"}>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
32
src/pages/CardsPage/utils/isDealPaid.ts
Normal file
32
src/pages/CardsPage/utils/isDealPaid.ts
Normal 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)
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user