feat: deals viewer mode and links for viewers
This commit is contained in:
		
							
								
								
									
										1009
									
								
								src/client/index.ts
									
									
									
									
									
								
							
							
						
						
									
										1009
									
								
								src/client/index.ts
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										10
									
								
								src/client/models/CreateGuestUrlResponse.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/client/models/CreateGuestUrlResponse.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
/* generated using openapi-typescript-codegen -- do not edit */
 | 
			
		||||
/* istanbul ignore file */
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
export type CreateGuestUrlResponse = {
 | 
			
		||||
    ok: boolean;
 | 
			
		||||
    message: string;
 | 
			
		||||
    url: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -11,6 +11,7 @@ import type { ClientGetResponse } from '../models/ClientGetResponse';
 | 
			
		||||
import type { ClientUpdateDetailsRequest } from '../models/ClientUpdateDetailsRequest';
 | 
			
		||||
import type { ClientUpdateRequest } from '../models/ClientUpdateRequest';
 | 
			
		||||
import type { ClientUpdateResponse } from '../models/ClientUpdateResponse';
 | 
			
		||||
import type { CreateGuestUrlResponse } from '../models/CreateGuestUrlResponse';
 | 
			
		||||
import type { CancelablePromise } from '../core/CancelablePromise';
 | 
			
		||||
import { OpenAPI } from '../core/OpenAPI';
 | 
			
		||||
import { request as __request } from '../core/request';
 | 
			
		||||
@@ -148,4 +149,25 @@ export class ClientService {
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * Client Link
 | 
			
		||||
     * @returns CreateGuestUrlResponse Successful Response
 | 
			
		||||
     * @throws ApiError
 | 
			
		||||
     */
 | 
			
		||||
    public static clientLink({
 | 
			
		||||
        clientId,
 | 
			
		||||
    }: {
 | 
			
		||||
        clientId: number,
 | 
			
		||||
    }): CancelablePromise<CreateGuestUrlResponse> {
 | 
			
		||||
        return __request(OpenAPI, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            url: '/client/link/{client_id}',
 | 
			
		||||
            path: {
 | 
			
		||||
                'client_id': clientId,
 | 
			
		||||
            },
 | 
			
		||||
            errors: {
 | 
			
		||||
                422: `Validation Error`,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,10 @@ import { CardGeneralFormType } from "../../pages/CardsPage/drawers/CardEditDrawe
 | 
			
		||||
type Props = {
 | 
			
		||||
    project: ProjectSchema;
 | 
			
		||||
    form: UseFormReturnType<CardGeneralFormType>;
 | 
			
		||||
    readOnly: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CardAttributeFields = ({ project, form }: Props) => {
 | 
			
		||||
const CardAttributeFields = ({ project, form, readOnly }: Props) => {
 | 
			
		||||
    const attributes: AttributeSchema[] = [];
 | 
			
		||||
 | 
			
		||||
    project.attributes.forEach(attribute => {
 | 
			
		||||
@@ -27,6 +28,7 @@ const CardAttributeFields = ({ project, form }: Props) => {
 | 
			
		||||
                key={attribute.id}
 | 
			
		||||
                attribute={attribute}
 | 
			
		||||
                form={form}
 | 
			
		||||
                readOnly={readOnly}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,10 @@ import { IconInfoCircle } from "@tabler/icons-react";
 | 
			
		||||
type Props = {
 | 
			
		||||
    attribute: AttributeSchema;
 | 
			
		||||
    form: UseFormReturnType<CardGeneralFormType>;
 | 
			
		||||
    readOnly: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CardAttributeField = ({ attribute, form }: Props) => {
 | 
			
		||||
const CardAttributeField = ({ attribute, form, readOnly }: Props) => {
 | 
			
		||||
    const type = attribute.type.type;
 | 
			
		||||
 | 
			
		||||
    const getDateValue = (): Date | null => {
 | 
			
		||||
@@ -35,6 +36,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
 | 
			
		||||
            <Checkbox
 | 
			
		||||
                label={label}
 | 
			
		||||
                {...form.getInputProps(attribute.name, { type: "checkbox" })}
 | 
			
		||||
                readOnly={readOnly}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
@@ -47,6 +49,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
 | 
			
		||||
                clearable
 | 
			
		||||
                locale={"ru-RU"}
 | 
			
		||||
                valueFormat="DD.MM.YYYY"
 | 
			
		||||
                readOnly={readOnly}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
@@ -59,6 +62,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
 | 
			
		||||
                clearable
 | 
			
		||||
                locale={"ru-RU"}
 | 
			
		||||
                valueFormat="DD.MM.YYYY HH:mm"
 | 
			
		||||
                readOnly={readOnly}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
@@ -68,6 +72,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
 | 
			
		||||
                label={label}
 | 
			
		||||
                {...form.getInputProps(attribute.name)}
 | 
			
		||||
                value={form.getInputProps(attribute.name).value ?? ""}
 | 
			
		||||
                readOnly={readOnly}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
@@ -77,6 +82,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
 | 
			
		||||
                allowDecimal={type === "float"}
 | 
			
		||||
                label={label}
 | 
			
		||||
                {...form.getInputProps(attribute.name)}
 | 
			
		||||
                readOnly={readOnly}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ import useStatus from "../../Statuses/Status/hooks/useStatus.tsx";
 | 
			
		||||
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
 | 
			
		||||
import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
 | 
			
		||||
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../../redux/store.ts";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
@@ -22,6 +24,7 @@ const Board = ({ board }: Props) => {
 | 
			
		||||
        onDeleteBoardClick,
 | 
			
		||||
    } = useBoardsContext();
 | 
			
		||||
 | 
			
		||||
    const authState = useSelector((state: RootState) => state.auth);
 | 
			
		||||
    const { dragState } = useDndContext();
 | 
			
		||||
 | 
			
		||||
    const { showContextMenu } = useContextMenu();
 | 
			
		||||
@@ -65,6 +68,7 @@ const Board = ({ board }: Props) => {
 | 
			
		||||
                    <Draggable
 | 
			
		||||
                        draggableId={"board-" + board.id.toString()}
 | 
			
		||||
                        index={board.ordinalNumber}
 | 
			
		||||
                        isDragDisabled={authState.isGuest}
 | 
			
		||||
                    >
 | 
			
		||||
                        {(provided) => (
 | 
			
		||||
                            <div
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ import PrefillCardsWithExcelDrawer
 | 
			
		||||
    from "../../../../pages/CardsPage/drawers/PrefillCardWithExcelDrawer/PrefillCardsWithExcelDrawer.tsx";
 | 
			
		||||
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
 | 
			
		||||
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../../redux/store.ts";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const Boards = () => {
 | 
			
		||||
@@ -16,6 +18,7 @@ const Boards = () => {
 | 
			
		||||
        selectedBoard,
 | 
			
		||||
        onCreateBoardClick,
 | 
			
		||||
    } = useBoardsContext();
 | 
			
		||||
    const authState = useSelector((state: RootState) => state.auth);
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
        onDragEnd,
 | 
			
		||||
@@ -37,14 +40,16 @@ const Boards = () => {
 | 
			
		||||
                        board={board}
 | 
			
		||||
                    />
 | 
			
		||||
                ))}
 | 
			
		||||
                <Center
 | 
			
		||||
                    px={"md"}
 | 
			
		||||
                    py={"xs"}
 | 
			
		||||
                    style={{ cursor: "pointer", borderBottom: "solid gray 1px" }}
 | 
			
		||||
                    onClick={onCreateBoardClick}
 | 
			
		||||
                >
 | 
			
		||||
                    <IconPlus />
 | 
			
		||||
                </Center>
 | 
			
		||||
                {!authState.isGuest && (
 | 
			
		||||
                    <Center
 | 
			
		||||
                        px={"md"}
 | 
			
		||||
                        py={"xs"}
 | 
			
		||||
                        style={{ cursor: "pointer", borderBottom: "solid gray 1px" }}
 | 
			
		||||
                        onClick={onCreateBoardClick}
 | 
			
		||||
                    >
 | 
			
		||||
                        <IconPlus />
 | 
			
		||||
                    </Center>
 | 
			
		||||
                )}
 | 
			
		||||
                <Box w={"100%"} style={{ borderBottom: "solid gray 1px" }}></Box>
 | 
			
		||||
            </Group>
 | 
			
		||||
        );
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,8 @@ import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
 | 
			
		||||
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
 | 
			
		||||
import CardTags from "../CardTags/CardTags.tsx";
 | 
			
		||||
import { ModuleNames } from "../../../../modules/modules.tsx";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../../redux/store.ts";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    cards: CardSummary[];
 | 
			
		||||
@@ -21,6 +23,7 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
 | 
			
		||||
    const [debouncedName] = useDebouncedValue(name, 200);
 | 
			
		||||
    const { selectedProject } = useProjectsContext();
 | 
			
		||||
    const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
 | 
			
		||||
    const authState = useSelector((state: RootState) => state.auth);
 | 
			
		||||
 | 
			
		||||
    const totalPrice = useMemo(() => cards.reduce((acc, card) => acc + card.totalPrice, 0), [cards]);
 | 
			
		||||
    const totalProducts = useMemo(() => cards.reduce((acc, card) => acc + card.totalProducts, 0), [cards]);
 | 
			
		||||
@@ -77,7 +80,7 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
 | 
			
		||||
                    />
 | 
			
		||||
                ))}
 | 
			
		||||
            </Flex>
 | 
			
		||||
            <CardTags groupId={group.id} tags={cards[0].tags}/>
 | 
			
		||||
            {!authState.isGuest && <CardTags groupId={group.id} tags={cards[0].tags}/>}
 | 
			
		||||
            {isServicesAndProductsIncluded && (
 | 
			
		||||
                <Flex
 | 
			
		||||
                    p={rem(10)}
 | 
			
		||||
@@ -95,4 +98,4 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
 | 
			
		||||
            )}
 | 
			
		||||
        </Flex>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,8 @@ import CardTags from "../CardTags/CardTags.tsx";
 | 
			
		||||
import CardAttributesInSummaryItem from "../CardAttributesInSummaryItem/CardAttributesInSummaryItem.tsx";
 | 
			
		||||
import { ModuleNames } from "../../../../modules/modules.tsx";
 | 
			
		||||
import isDealPaid from "../../../../pages/CardsPage/utils/isDealPaid.ts";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../../redux/store.ts";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    cardSummary: CardSummary;
 | 
			
		||||
@@ -27,6 +29,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
 | 
			
		||||
    const { setSelectedCard } = useCardPageContext();
 | 
			
		||||
    const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState();
 | 
			
		||||
    const [isPaid, setIsPaid] = useState<boolean>(false);
 | 
			
		||||
    const authState = useSelector((state: RootState) => state.auth);
 | 
			
		||||
 | 
			
		||||
    const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
 | 
			
		||||
    const isClientIncluded = isModuleInProject(ModuleNames.CLIENTS, selectedProject);
 | 
			
		||||
@@ -36,8 +39,8 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
 | 
			
		||||
            setSelectedCard(card);
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
    const isLockedInsideGroup = () => {
 | 
			
		||||
        return !!(cardSummary?.group?.billRequests && cardSummary?.group?.billRequests?.length !== 0);
 | 
			
		||||
    const canBeRemovedFromGroup = () => {
 | 
			
		||||
        return !!(cardSummary.group && (!cardSummary.group.billRequests || cardSummary.group.billRequests?.length === 0));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
@@ -46,8 +49,8 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div
 | 
			
		||||
            onContextMenu={showContextMenu([
 | 
			
		||||
                ...isLockedInsideGroup() ? [] : [{
 | 
			
		||||
            onContextMenu={!authState.isDealsViewer ? showContextMenu([
 | 
			
		||||
                ...!canBeRemovedFromGroup() ? [] : [{
 | 
			
		||||
                    key: "removeFromGroup",
 | 
			
		||||
                    onClick: () => onDeleteFromGroup(cardSummary),
 | 
			
		||||
                    title: "Убрать из группы",
 | 
			
		||||
@@ -65,7 +68,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
 | 
			
		||||
                    title: "Удалить",
 | 
			
		||||
                    icon: <IconTrash />,
 | 
			
		||||
                },
 | 
			
		||||
            ])}
 | 
			
		||||
            ]) : undefined}
 | 
			
		||||
            onClick={() => onCardSummaryClick()}
 | 
			
		||||
            className={styles["container"]}
 | 
			
		||||
            style={{ backgroundColor: color }}
 | 
			
		||||
@@ -108,7 +111,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                )}
 | 
			
		||||
                <CardAttributesInSummaryItem cardSummary={cardSummary} />
 | 
			
		||||
                {!cardSummary.group?.id && (
 | 
			
		||||
                {!authState.isGuest && !cardSummary.group?.id && (
 | 
			
		||||
                    <CardTags cardId={cardSummary.id} tags={cardSummary.tags} />
 | 
			
		||||
                )}
 | 
			
		||||
                <Flex align={"center"} justify={"space-between"}>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,8 @@ import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
 | 
			
		||||
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
 | 
			
		||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
 | 
			
		||||
import { ModuleNames } from "../../../../modules/modules.tsx";
 | 
			
		||||
import { RootState } from "../../../../redux/store.ts";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    status: StatusSchema;
 | 
			
		||||
@@ -28,6 +30,7 @@ export const CardsDndColumn: FC<Props> = ({
 | 
			
		||||
                                              dragState,
 | 
			
		||||
                                              withCreateButton = false,
 | 
			
		||||
                                          }) => {
 | 
			
		||||
    const authState = useSelector((state: RootState) => state.auth);
 | 
			
		||||
    const { selectedProject } = useProjectsContext();
 | 
			
		||||
    const isCreatingDealFromFileEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
 | 
			
		||||
    const isDropDisabled = dragState !== DragState.DRAG_CARD;
 | 
			
		||||
@@ -77,7 +80,9 @@ export const CardsDndColumn: FC<Props> = ({
 | 
			
		||||
            <Draggable
 | 
			
		||||
                draggableId={card.id.toString()}
 | 
			
		||||
                index={card.rank}
 | 
			
		||||
                key={card.id}>
 | 
			
		||||
                key={card.id}
 | 
			
		||||
                isDragDisabled={authState.isGuest}
 | 
			
		||||
            >
 | 
			
		||||
                {(provided, snapshot) => (
 | 
			
		||||
                    <div
 | 
			
		||||
                        {...provided.draggableProps}
 | 
			
		||||
@@ -104,6 +109,7 @@ export const CardsDndColumn: FC<Props> = ({
 | 
			
		||||
                draggableId={"group-" + group.id}
 | 
			
		||||
                index={cards[0].rank}
 | 
			
		||||
                key={"group-" + group.id}
 | 
			
		||||
                isDragDisabled={authState.isGuest}
 | 
			
		||||
            >
 | 
			
		||||
                {(provided) => (
 | 
			
		||||
                    <div
 | 
			
		||||
@@ -135,7 +141,7 @@ export const CardsDndColumn: FC<Props> = ({
 | 
			
		||||
                        styles["items-list-drag-over"],
 | 
			
		||||
                    )}
 | 
			
		||||
                    {...provided.droppableProps}>
 | 
			
		||||
                    {withCreateButton && (
 | 
			
		||||
                    {withCreateButton && !authState.isGuest && (
 | 
			
		||||
                        <>
 | 
			
		||||
                            <CreateCardButton status={status} />
 | 
			
		||||
                            {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import { usePrefillCardContext } from "../../../../pages/CardsPage/contexts/Pref
 | 
			
		||||
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
 | 
			
		||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
 | 
			
		||||
import { ModuleNames } from "../../../../modules/modules.tsx";
 | 
			
		||||
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    status: StatusSchema;
 | 
			
		||||
@@ -20,6 +21,7 @@ const CreateCardButton = ({ status }: Props) => {
 | 
			
		||||
    const [isTransitionEnded, setIsTransitionEnded] = useState(true);
 | 
			
		||||
    const queryClient = useQueryClient();
 | 
			
		||||
    const { prefillCard, setPrefillCard } = usePrefillCardContext();
 | 
			
		||||
    const { refetchSummaries } = useDndContext();
 | 
			
		||||
 | 
			
		||||
    const { selectedProject } = useProjectsContext();
 | 
			
		||||
    const isPrefillingDealEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
 | 
			
		||||
@@ -57,7 +59,7 @@ const CreateCardButton = ({ status }: Props) => {
 | 
			
		||||
                                    },
 | 
			
		||||
                                }).then(async (result) => {
 | 
			
		||||
                                    if (isPrefillingDealEnabled && prefillCard) {
 | 
			
		||||
                                        CardService.prefillCard({
 | 
			
		||||
                                        await CardService.prefillCard({
 | 
			
		||||
                                            requestBody: {
 | 
			
		||||
                                                oldCardId: prefillCard.id,
 | 
			
		||||
                                                newCardId: result.cardId,
 | 
			
		||||
@@ -67,6 +69,7 @@ const CreateCardButton = ({ status }: Props) => {
 | 
			
		||||
                                    await queryClient.invalidateQueries({
 | 
			
		||||
                                        queryKey: ["getCardSummaries"],
 | 
			
		||||
                                    });
 | 
			
		||||
                                    refetchSummaries();
 | 
			
		||||
                                    setIsCreating(false);
 | 
			
		||||
                                    setPrefillCard(undefined);
 | 
			
		||||
                                });
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
 | 
			
		||||
import { useEqualHeightsContext } from "./contexts/EqualHeightContext.tsx";
 | 
			
		||||
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
 | 
			
		||||
import { ModuleNames } from "../../../../modules/modules.tsx";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../../redux/store.ts";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
@@ -26,6 +28,7 @@ const Status = ({ summaries, status, dragState, index }: Props) => {
 | 
			
		||||
    const {
 | 
			
		||||
        selectedBoard,
 | 
			
		||||
    } = useBoardsContext();
 | 
			
		||||
    const authState = useSelector((state: RootState) => state.auth);
 | 
			
		||||
 | 
			
		||||
    const isDropDisabled = dragState !== DragState.DRAG_STATUS;
 | 
			
		||||
    const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedBoard?.project);
 | 
			
		||||
@@ -107,6 +110,7 @@ const Status = ({ summaries, status, dragState, index }: Props) => {
 | 
			
		||||
                        <Draggable
 | 
			
		||||
                            draggableId={"status-" + status.id.toString()}
 | 
			
		||||
                            index={status.ordinalNumber}
 | 
			
		||||
                            isDragDisabled={authState.isGuest}
 | 
			
		||||
                        >
 | 
			
		||||
                            {(provided) => (
 | 
			
		||||
                                <div
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ import { jwtDecode, JwtPayload as JwtPayloadBase } from "jwt-decode";
 | 
			
		||||
interface AuthState {
 | 
			
		||||
    isAuthorized: boolean;
 | 
			
		||||
    accessToken: string;
 | 
			
		||||
    isDealEditor: boolean;
 | 
			
		||||
    isDealsViewer: boolean;
 | 
			
		||||
    isGuest: boolean;
 | 
			
		||||
    role: string;
 | 
			
		||||
}
 | 
			
		||||
@@ -16,6 +18,8 @@ const initialState = (): AuthState => {
 | 
			
		||||
    return {
 | 
			
		||||
        accessToken: "",
 | 
			
		||||
        isAuthorized: false,
 | 
			
		||||
        isDealEditor: false,
 | 
			
		||||
        isDealsViewer: false,
 | 
			
		||||
        isGuest: false,
 | 
			
		||||
        role: "user",
 | 
			
		||||
    };
 | 
			
		||||
@@ -37,7 +41,9 @@ const authSlice = createSlice({
 | 
			
		||||
                state.accessToken = action.payload.accessToken;
 | 
			
		||||
                state.isAuthorized = true;
 | 
			
		||||
                state.role = role;
 | 
			
		||||
                if (sub === "guest") state.isGuest = true;
 | 
			
		||||
                state.isDealEditor = sub === "deal_editor";
 | 
			
		||||
                state.isDealsViewer = sub === "deals_viewer";
 | 
			
		||||
                state.isGuest = state.isDealEditor || state.isDealsViewer;
 | 
			
		||||
            } catch (_) {
 | 
			
		||||
                const url = window.location.href;
 | 
			
		||||
                const urlObj = new URL(url);
 | 
			
		||||
 
 | 
			
		||||
@@ -21,11 +21,16 @@ import PrintDealBarcodesButton from "./components/PrintDealBarcodesButton/PrintD
 | 
			
		||||
import PaymentLinkButton from "./components/PaymentLinkButton/PaymentLinkButton.tsx";
 | 
			
		||||
import isValidInn from "../../../../pages/ClientsPage/utils/isValidInn.ts";
 | 
			
		||||
import isDealPaid, { isDealLocked } from "../../../../pages/CardsPage/utils/isDealPaid.ts";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../../redux/store.ts";
 | 
			
		||||
import { useCardPageContext } from "../../../../pages/CardsPage/contexts/CardPageContext.tsx";
 | 
			
		||||
 | 
			
		||||
const ProductAndServiceTab: FC = () => {
 | 
			
		||||
    const { cardState, cardServicesState, cardProductsState } = useCardProductAndServiceTabState();
 | 
			
		||||
    const { selectedCard: card } = useCardPageContext();
 | 
			
		||||
    const isLocked = isDealLocked(cardState.card);
 | 
			
		||||
    const [paid, setPaid] = useState<boolean>(false);
 | 
			
		||||
    const { isDealsViewer } = useSelector((state: RootState) => state.auth);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setPaid(isDealPaid(cardState.card));
 | 
			
		||||
@@ -128,7 +133,9 @@ const ProductAndServiceTab: FC = () => {
 | 
			
		||||
        ProductService.createProduct({
 | 
			
		||||
            requestBody: newProduct,
 | 
			
		||||
        }).then(({ ok, message }) => {
 | 
			
		||||
            notifications.guess(ok, { message: message });
 | 
			
		||||
            notifications.guess(ok, { message });
 | 
			
		||||
        }).catch(err => {
 | 
			
		||||
            notifications.error({ message: err.toString() });
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
    const onCreateProductClick = () => {
 | 
			
		||||
@@ -256,18 +263,18 @@ const ProductAndServiceTab: FC = () => {
 | 
			
		||||
                        mr={"xs"}
 | 
			
		||||
                    >
 | 
			
		||||
                        <Group wrap={"nowrap"}>
 | 
			
		||||
                            <PrintDealBarcodesButton card={cardState.card} />
 | 
			
		||||
                            {!isDealsViewer && <PrintDealBarcodesButton card={cardState.card} />}
 | 
			
		||||
                            <Checkbox
 | 
			
		||||
                                label={"Оплачен"}
 | 
			
		||||
                                checked={paid}
 | 
			
		||||
                                disabled
 | 
			
		||||
                            />
 | 
			
		||||
                        </Group>
 | 
			
		||||
                        <PaymentLinkButton card={cardState.card} />
 | 
			
		||||
                        {!isDealsViewer && <PaymentLinkButton card={cardState.card} />}
 | 
			
		||||
                    </Group>
 | 
			
		||||
                )}
 | 
			
		||||
                <Stack className={styles["card-container-wrapper"]} mr={"xs"}>
 | 
			
		||||
                    <GeneralDataForm />
 | 
			
		||||
                    {card && <GeneralDataForm />}
 | 
			
		||||
                </Stack>
 | 
			
		||||
                <ScrollArea offsetScrollbars>
 | 
			
		||||
                    <Flex
 | 
			
		||||
@@ -278,41 +285,45 @@ const ProductAndServiceTab: FC = () => {
 | 
			
		||||
                            {...cardServicesState}
 | 
			
		||||
                        />
 | 
			
		||||
 | 
			
		||||
                        <Divider my={rem(15)} />
 | 
			
		||||
                        <div className={styles["card-container-buttons"]}>
 | 
			
		||||
                            <Button
 | 
			
		||||
                                disabled={isLocked}
 | 
			
		||||
                                variant={"default"}
 | 
			
		||||
                                fullWidth
 | 
			
		||||
                                onClick={onCreateProductClick}>
 | 
			
		||||
                                Создать товар
 | 
			
		||||
                            </Button>
 | 
			
		||||
                            <Button
 | 
			
		||||
                                disabled={isLocked}
 | 
			
		||||
                                onClick={onAddProductClick}
 | 
			
		||||
                                variant={"default"}
 | 
			
		||||
                                fullWidth>
 | 
			
		||||
                                Добавить товар
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <Divider my={rem(15)} />
 | 
			
		||||
                        <div className={styles["card-container-buttons"]}>
 | 
			
		||||
                            {isLocked ? (
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    onClick={onCancelBillClick}
 | 
			
		||||
                                    color={"red"}>
 | 
			
		||||
                                    Отозвать счет
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    disabled={isLocked}
 | 
			
		||||
                                    onClick={onCreateBillClick}
 | 
			
		||||
                                    variant={"default"}
 | 
			
		||||
                                    fullWidth>
 | 
			
		||||
                                    Выставить счет
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {!isDealsViewer && (
 | 
			
		||||
                            <>
 | 
			
		||||
                                <Divider my={rem(15)} />
 | 
			
		||||
                                <div className={styles["card-container-buttons"]}>
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        disabled={isLocked}
 | 
			
		||||
                                        variant={"default"}
 | 
			
		||||
                                        fullWidth
 | 
			
		||||
                                        onClick={onCreateProductClick}>
 | 
			
		||||
                                        Создать товар
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        disabled={isLocked}
 | 
			
		||||
                                        onClick={onAddProductClick}
 | 
			
		||||
                                        variant={"default"}
 | 
			
		||||
                                        fullWidth>
 | 
			
		||||
                                        Добавить товар
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <Divider my={rem(15)} />
 | 
			
		||||
                                <div className={styles["card-container-buttons"]}>
 | 
			
		||||
                                    {isLocked ? (
 | 
			
		||||
                                        <Button
 | 
			
		||||
                                            onClick={onCancelBillClick}
 | 
			
		||||
                                            color={"red"}>
 | 
			
		||||
                                            Отозвать счет
 | 
			
		||||
                                        </Button>
 | 
			
		||||
                                    ) : (
 | 
			
		||||
                                        <Button
 | 
			
		||||
                                            disabled={isLocked}
 | 
			
		||||
                                            onClick={onCreateBillClick}
 | 
			
		||||
                                            variant={"default"}
 | 
			
		||||
                                            fullWidth>
 | 
			
		||||
                                            Выставить счет
 | 
			
		||||
                                        </Button>
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </>
 | 
			
		||||
                        )}
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                    <Flex
 | 
			
		||||
                        direction={"column"}
 | 
			
		||||
 
 | 
			
		||||
@@ -138,15 +138,17 @@ const CardServicesTable: FC<Props> = ({
 | 
			
		||||
                                w={"100%"}
 | 
			
		||||
                                gap={rem(10)}
 | 
			
		||||
                                align={"center"}>
 | 
			
		||||
                                <Tooltip
 | 
			
		||||
                                    onClick={() => onDeleteClick(service)}
 | 
			
		||||
                                    label="Удалить услугу">
 | 
			
		||||
                                    <ActionIcon
 | 
			
		||||
                                        disabled={isLocked}
 | 
			
		||||
                                        variant={"default"}>
 | 
			
		||||
                                        <IconTrash />
 | 
			
		||||
                                    </ActionIcon>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                                {!authState.isDealsViewer && (
 | 
			
		||||
                                    <Tooltip
 | 
			
		||||
                                        onClick={() => onDeleteClick(service)}
 | 
			
		||||
                                        label="Удалить услугу">
 | 
			
		||||
                                        <ActionIcon
 | 
			
		||||
                                            disabled={isLocked}
 | 
			
		||||
                                            variant={"default"}>
 | 
			
		||||
                                            <IconTrash />
 | 
			
		||||
                                        </ActionIcon>
 | 
			
		||||
                                    </Tooltip>
 | 
			
		||||
                                )}
 | 
			
		||||
                                {!authState.isGuest && (
 | 
			
		||||
                                    <Tooltip label="Сотрудники">
 | 
			
		||||
                                        <ActionIcon
 | 
			
		||||
@@ -167,9 +169,8 @@ const CardServicesTable: FC<Props> = ({
 | 
			
		||||
                                        isNumber(event) &&
 | 
			
		||||
                                        onQuantityChange(service, event)
 | 
			
		||||
                                    }
 | 
			
		||||
 | 
			
		||||
                                    value={service.quantity}
 | 
			
		||||
 | 
			
		||||
                                    readOnly={authState.isDealsViewer}
 | 
			
		||||
                                />
 | 
			
		||||
                                <NumberInput
 | 
			
		||||
                                    flex={1}
 | 
			
		||||
@@ -180,6 +181,7 @@ const CardServicesTable: FC<Props> = ({
 | 
			
		||||
                                    suffix={"₽"}
 | 
			
		||||
                                    value={service.price}
 | 
			
		||||
                                    disabled={authState.isGuest || isLocked || service.isFixedPrice}
 | 
			
		||||
                                    readOnly={authState.isDealsViewer}
 | 
			
		||||
                                    rightSectionProps={{
 | 
			
		||||
                                        style: {
 | 
			
		||||
                                            display: "flex",
 | 
			
		||||
@@ -188,12 +190,14 @@ const CardServicesTable: FC<Props> = ({
 | 
			
		||||
                                        },
 | 
			
		||||
                                    }}
 | 
			
		||||
                                    rightSection={
 | 
			
		||||
                                        <LockCheckbox
 | 
			
		||||
                                            label={"Зафиксировать цену"}
 | 
			
		||||
                                            variant={"default"}
 | 
			
		||||
                                            value={service.isFixedPrice}
 | 
			
		||||
                                            onChange={value => onLockChange(service, value)}
 | 
			
		||||
                                        />
 | 
			
		||||
                                        !authState.isDealsViewer && (
 | 
			
		||||
                                            <LockCheckbox
 | 
			
		||||
                                                label={"Зафиксировать цену"}
 | 
			
		||||
                                                variant={"default"}
 | 
			
		||||
                                                value={service.isFixedPrice}
 | 
			
		||||
                                                onChange={value => onLockChange(service, value)}
 | 
			
		||||
                                            />
 | 
			
		||||
                                        )
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                            </Flex>
 | 
			
		||||
@@ -212,25 +216,27 @@ const CardServicesTable: FC<Props> = ({
 | 
			
		||||
                        ₽
 | 
			
		||||
                    </Title>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                <Flex
 | 
			
		||||
                    direction={"column"}
 | 
			
		||||
                    gap={rem(10)}
 | 
			
		||||
                    mt={"auto"}>
 | 
			
		||||
                    <Button
 | 
			
		||||
                        disabled={isLocked}
 | 
			
		||||
                        onClick={onCreateClick}
 | 
			
		||||
                        fullWidth
 | 
			
		||||
                        variant={"default"}>
 | 
			
		||||
                        Добавить услугу
 | 
			
		||||
                    </Button>
 | 
			
		||||
                    <Button
 | 
			
		||||
                        disabled={isLocked}
 | 
			
		||||
                        onClick={onAddKitClick}
 | 
			
		||||
                        fullWidth
 | 
			
		||||
                        variant={"default"}>
 | 
			
		||||
                        Добавить набор услуг
 | 
			
		||||
                    </Button>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                {!authState.isDealsViewer && (
 | 
			
		||||
                    <Flex
 | 
			
		||||
                        direction={"column"}
 | 
			
		||||
                        gap={rem(10)}
 | 
			
		||||
                        mt={"auto"}>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            disabled={isLocked}
 | 
			
		||||
                            onClick={onCreateClick}
 | 
			
		||||
                            fullWidth
 | 
			
		||||
                            variant={"default"}>
 | 
			
		||||
                            Добавить услугу
 | 
			
		||||
                        </Button>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            disabled={isLocked}
 | 
			
		||||
                            onClick={onAddKitClick}
 | 
			
		||||
                            fullWidth
 | 
			
		||||
                            variant={"default"}>
 | 
			
		||||
                            Добавить набор услуг
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                )}
 | 
			
		||||
            </Flex>
 | 
			
		||||
            <Modal
 | 
			
		||||
                title={"Добавление сотрудника к услуге"}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,8 @@ 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 = {
 | 
			
		||||
@@ -16,7 +18,8 @@ type GeneralDataFormType = {
 | 
			
		||||
 | 
			
		||||
const GeneralDataForm = () => {
 | 
			
		||||
    const { selectedCard: card, refetchCard } = useCardPageContext();
 | 
			
		||||
    if (!card) return;
 | 
			
		||||
    const { isDealsViewer } = useSelector((state: RootState) => state.auth);
 | 
			
		||||
    if (!card) return <></>;
 | 
			
		||||
 | 
			
		||||
    const [initialValues, setInitialValues] = useState<GeneralDataFormType>(card);
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +70,7 @@ const GeneralDataForm = () => {
 | 
			
		||||
        <form onSubmit={form.onSubmit(values => onSubmit(values))}>
 | 
			
		||||
            <Stack>
 | 
			
		||||
                <ShippingWarehouseAutocomplete
 | 
			
		||||
                    placeholder={"Введите склад отгрузки"}
 | 
			
		||||
                    placeholder={isDealsViewer ? "" : "Введите склад отгрузки"}
 | 
			
		||||
                    label={"Склад отгрузки"}
 | 
			
		||||
                    value={
 | 
			
		||||
                        isShippingWarehouse(
 | 
			
		||||
@@ -87,18 +90,23 @@ const GeneralDataForm = () => {
 | 
			
		||||
                            "shippingWarehouse",
 | 
			
		||||
                        ).onChange(event);
 | 
			
		||||
                    }}
 | 
			
		||||
                    readOnly={isDealsViewer}
 | 
			
		||||
                />
 | 
			
		||||
                <Checkbox
 | 
			
		||||
                    label={"Учет выручки в статистике"}
 | 
			
		||||
                    {...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
 | 
			
		||||
                />
 | 
			
		||||
                <Button
 | 
			
		||||
                    type={"submit"}
 | 
			
		||||
                    variant={"default"}
 | 
			
		||||
                    disabled={isEqual(initialValues, form.values)}
 | 
			
		||||
                >
 | 
			
		||||
                    Сохранить
 | 
			
		||||
                </Button>
 | 
			
		||||
                {!isDealsViewer && (
 | 
			
		||||
                    <>
 | 
			
		||||
                        <Checkbox
 | 
			
		||||
                            label={"Учет выручки в статистике"}
 | 
			
		||||
                            {...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
 | 
			
		||||
                        />
 | 
			
		||||
                        <Button
 | 
			
		||||
                            type={"submit"}
 | 
			
		||||
                            variant={"default"}
 | 
			
		||||
                            disabled={isEqual(initialValues, form.values)}
 | 
			
		||||
                        >
 | 
			
		||||
                            Сохранить
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </>
 | 
			
		||||
                )}
 | 
			
		||||
            </Stack>
 | 
			
		||||
        </form>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -103,8 +103,8 @@ const ProductServicesTable: FC<Props> = ({
 | 
			
		||||
                        {
 | 
			
		||||
                            enableColumnActions: false,
 | 
			
		||||
                            enableSorting: false,
 | 
			
		||||
                            enableRowActions: true,
 | 
			
		||||
                            enableBottomToolbar: true,
 | 
			
		||||
                            enableRowActions: !authState.isDealsViewer,
 | 
			
		||||
                            enableBottomToolbar: !authState.isDealsViewer,
 | 
			
		||||
                            renderBottomToolbar: (
 | 
			
		||||
                                <Flex
 | 
			
		||||
                                    justify={"flex-end"}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,8 @@ import { ServiceType } from "../../../../../../shared/enums/ServiceType.ts";
 | 
			
		||||
import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
 | 
			
		||||
import { useDebouncedCallback } from "@mantine/hooks";
 | 
			
		||||
import { isDealLocked } from "../../../../../../pages/CardsPage/utils/isDealPaid.ts";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../../../../redux/store.ts";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    product: CardProductSchema;
 | 
			
		||||
@@ -54,6 +56,8 @@ const ProductView: FC<Props> = ({
 | 
			
		||||
        onDelete(product);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const { isDealsViewer } = useSelector((state: RootState) => state.auth);
 | 
			
		||||
 | 
			
		||||
    const onServiceDelete = (item: CardProductServiceSchema) => {
 | 
			
		||||
        if (!onChange) return;
 | 
			
		||||
        onChange({
 | 
			
		||||
@@ -162,25 +166,29 @@ const ProductView: FC<Props> = ({
 | 
			
		||||
                        isNumber(event) && onQuantityChange(event)
 | 
			
		||||
                    }
 | 
			
		||||
                    placeholder={"Введите количество товара"}
 | 
			
		||||
                    readOnly={isDealsViewer}
 | 
			
		||||
                />
 | 
			
		||||
                <Textarea
 | 
			
		||||
                    mih={rem(140)}
 | 
			
		||||
                    styles={{
 | 
			
		||||
                        wrapper: { height: "90%" },
 | 
			
		||||
                        input: { height: "90%" },
 | 
			
		||||
                    }}
 | 
			
		||||
                    my={rem(10)}
 | 
			
		||||
                    disabled={isLocked}
 | 
			
		||||
                    defaultValue={product.comment}
 | 
			
		||||
                    onChange={event => {
 | 
			
		||||
                        if (!onChange) return;
 | 
			
		||||
                        debouncedOnChange({
 | 
			
		||||
                            ...product,
 | 
			
		||||
                            comment: event.currentTarget.value,
 | 
			
		||||
                        });
 | 
			
		||||
                    }}
 | 
			
		||||
                    placeholder={"Введите комментарий для товара"}
 | 
			
		||||
                />
 | 
			
		||||
                {!(isDealsViewer && product.comment?.length === 0) && (
 | 
			
		||||
                    <Textarea
 | 
			
		||||
                        mih={rem(140)}
 | 
			
		||||
                        styles={{
 | 
			
		||||
                            wrapper: { height: "90%" },
 | 
			
		||||
                            input: { height: "90%" },
 | 
			
		||||
                        }}
 | 
			
		||||
                        my={rem(10)}
 | 
			
		||||
                        disabled={isLocked}
 | 
			
		||||
                        defaultValue={product.comment}
 | 
			
		||||
                        onChange={event => {
 | 
			
		||||
                            if (!onChange) return;
 | 
			
		||||
                            debouncedOnChange({
 | 
			
		||||
                                ...product,
 | 
			
		||||
                                comment: event.currentTarget.value,
 | 
			
		||||
                            });
 | 
			
		||||
                        }}
 | 
			
		||||
                        placeholder={isDealsViewer ? "" : "Введите комментарий для товара"}
 | 
			
		||||
                        readOnly={isDealsViewer}
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className={styles["services-container"]}>
 | 
			
		||||
@@ -195,34 +203,36 @@ const ProductView: FC<Props> = ({
 | 
			
		||||
                    onDelete={onServiceDelete}
 | 
			
		||||
                    onChange={onServiceChange}
 | 
			
		||||
                />
 | 
			
		||||
                <Flex
 | 
			
		||||
                    mt={"auto"}
 | 
			
		||||
                    ml={"auto"}
 | 
			
		||||
                    gap={rem(10)}>
 | 
			
		||||
                    <Tooltip
 | 
			
		||||
                        onClick={onPrintBarcodeClick}
 | 
			
		||||
                        label="Печать штрихкода">
 | 
			
		||||
                        <ActionIcon variant={"default"}>
 | 
			
		||||
                            <IconBarcode />
 | 
			
		||||
                        </ActionIcon>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                    <Tooltip
 | 
			
		||||
                        onClick={onProductEditClick}
 | 
			
		||||
                        label="Редактировать товар">
 | 
			
		||||
                        <ActionIcon variant={"default"}>
 | 
			
		||||
                            <IconEdit />
 | 
			
		||||
                        </ActionIcon>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                    <Tooltip
 | 
			
		||||
                        onClick={onDeleteClick}
 | 
			
		||||
                        label="Удалить товар">
 | 
			
		||||
                        <ActionIcon
 | 
			
		||||
                            disabled={isLocked}
 | 
			
		||||
                            variant={"default"}>
 | 
			
		||||
                            <IconTrash />
 | 
			
		||||
                        </ActionIcon>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                {!isDealsViewer && (
 | 
			
		||||
                    <Flex
 | 
			
		||||
                        mt={"auto"}
 | 
			
		||||
                        ml={"auto"}
 | 
			
		||||
                        gap={rem(10)}>
 | 
			
		||||
                        <Tooltip
 | 
			
		||||
                            onClick={onPrintBarcodeClick}
 | 
			
		||||
                            label="Печать штрихкода">
 | 
			
		||||
                            <ActionIcon variant={"default"}>
 | 
			
		||||
                                <IconBarcode />
 | 
			
		||||
                            </ActionIcon>
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
                        <Tooltip
 | 
			
		||||
                            onClick={onProductEditClick}
 | 
			
		||||
                            label="Редактировать товар">
 | 
			
		||||
                            <ActionIcon variant={"default"}>
 | 
			
		||||
                                <IconEdit />
 | 
			
		||||
                            </ActionIcon>
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
                        <Tooltip
 | 
			
		||||
                            onClick={onDeleteClick}
 | 
			
		||||
                            label="Удалить товар">
 | 
			
		||||
                            <ActionIcon
 | 
			
		||||
                                disabled={isLocked}
 | 
			
		||||
                                variant={"default"}>
 | 
			
		||||
                                <IconTrash />
 | 
			
		||||
                            </ActionIcon>
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import React, { createContext, FC, useContext, useEffect, useState } from "react";
 | 
			
		||||
import { useProjectsContext } from "../../contexts/ProjectsContext.tsx";
 | 
			
		||||
import { MODULES } from "../modules.tsx";
 | 
			
		||||
import { ModuleNames, MODULES } from "../modules.tsx";
 | 
			
		||||
import { Module } from "../types.tsx";
 | 
			
		||||
import { RootState } from "../../redux/store.ts";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
 | 
			
		||||
type ModulesContextState = {
 | 
			
		||||
    modules: Module[];
 | 
			
		||||
@@ -13,15 +15,24 @@ const ModulesContext = createContext<ModulesContextState | undefined>(
 | 
			
		||||
 | 
			
		||||
const useModulesContextState = () => {
 | 
			
		||||
    const { selectedProject } = useProjectsContext();
 | 
			
		||||
 | 
			
		||||
    const { isDealsViewer } = useSelector((state: RootState) => state.auth);
 | 
			
		||||
    const [modules, setModules] = useState<Module[]>([]);
 | 
			
		||||
 | 
			
		||||
    const filterModules = (modulesToFilter: Module[]): Module[] => {
 | 
			
		||||
        if (isDealsViewer) {
 | 
			
		||||
            const modulesForDealsViewer: string[] = [ModuleNames.SERVICES_AND_PRODUCTS];
 | 
			
		||||
            return modulesToFilter.filter(module => modulesForDealsViewer.includes(module.info.key));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return modulesToFilter;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const modules = selectedProject?.modules ?? [];
 | 
			
		||||
        const projectModules = modules.map(module => {
 | 
			
		||||
            return MODULES[module.key];
 | 
			
		||||
        }) ?? [];
 | 
			
		||||
        setModules(projectModules);
 | 
			
		||||
        setModules(filterModules(projectModules));
 | 
			
		||||
    }, [selectedProject?.id]);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,42 +22,42 @@ const modules: ModulesType = {
 | 
			
		||||
    [ModuleNames.CLIENTS]: {
 | 
			
		||||
        info: {
 | 
			
		||||
            label: "Клиенты",
 | 
			
		||||
            key: "clients",
 | 
			
		||||
            key: ModuleNames.CLIENTS,
 | 
			
		||||
            icon: <IconUser />,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    [ModuleNames.SERVICES_AND_PRODUCTS]: {
 | 
			
		||||
        info: {
 | 
			
		||||
            label: "Товары и услуги",
 | 
			
		||||
            key: "servicesAndProducts",
 | 
			
		||||
            key: ModuleNames.SERVICES_AND_PRODUCTS,
 | 
			
		||||
            icon: <IconBox />,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    [ModuleNames.SHIPMENT]: {
 | 
			
		||||
        info: {
 | 
			
		||||
            label: "Отгрузка",
 | 
			
		||||
            key: "shipment",
 | 
			
		||||
            key: ModuleNames.SHIPMENT,
 | 
			
		||||
            icon: <IconCubeSend />,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    [ModuleNames.EMPLOYEES]: {
 | 
			
		||||
        info: {
 | 
			
		||||
            label: "Сотрудники",
 | 
			
		||||
            key: "employees",
 | 
			
		||||
            key: ModuleNames.EMPLOYEES,
 | 
			
		||||
            icon: <IconUsersGroup />,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    [ModuleNames.MANAGERS]: {
 | 
			
		||||
        info: {
 | 
			
		||||
            label: "Менеджер",
 | 
			
		||||
            key: "managers",
 | 
			
		||||
            key: ModuleNames.MANAGERS,
 | 
			
		||||
            icon: <IconUserCog />,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    [ModuleNames.CHAT]: {
 | 
			
		||||
        info: {
 | 
			
		||||
            label: "Чат",
 | 
			
		||||
            key: "chat",
 | 
			
		||||
            key: ModuleNames.CHAT,
 | 
			
		||||
            icon: <IconMessage />,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ type DndContextState = {
 | 
			
		||||
    dragState: DragState,
 | 
			
		||||
    onDragStart: (start: DragStart) => void,
 | 
			
		||||
    onDragEnd: (result: DropResult) => Promise<void>,
 | 
			
		||||
    refetchSummaries: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const DndContext = createContext<DndContextState | undefined>(undefined);
 | 
			
		||||
@@ -64,6 +65,7 @@ const useDndContextState = ({
 | 
			
		||||
        dragState,
 | 
			
		||||
        onDragStart,
 | 
			
		||||
        onDragEnd,
 | 
			
		||||
        refetchSummaries,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ import CardStatusSelect from "../../../../../../components/CardStatusSelect/Card
 | 
			
		||||
import CardAttributeFields from "../../../../../../components/CardAttributeFields/CardAttributeFields.tsx";
 | 
			
		||||
import getAttributesFromCard from "../../../../../../components/CardAttributeFields/utils/getAttributesFromCard.ts";
 | 
			
		||||
import CardTagsInput from "../../../../components/CardTagsInput/CardTagsInput.tsx";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../../../../redux/store.ts";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    card: CardSchema;
 | 
			
		||||
@@ -43,6 +45,7 @@ const Content: FC<Props> = ({ card }) => {
 | 
			
		||||
    const queryClient = useQueryClient();
 | 
			
		||||
    const [project, setProject] = useState<ProjectSchema | null>(card.board.project);
 | 
			
		||||
    const [cardTags, setCardTags] = useState<string[]>(card.tags?.map(tag => tag.name) ?? []);
 | 
			
		||||
    const { isDealsViewer } = useSelector((state: RootState) => state.auth);
 | 
			
		||||
 | 
			
		||||
    const getInitialValues = (card: CardSchema): CardGeneralFormType => {
 | 
			
		||||
        return {
 | 
			
		||||
@@ -159,6 +162,7 @@ const Content: FC<Props> = ({ card }) => {
 | 
			
		||||
                                    placeholder={"Название сделки"}
 | 
			
		||||
                                    label={"Название сделки"}
 | 
			
		||||
                                    {...form.getInputProps("name")}
 | 
			
		||||
                                    readOnly={isDealsViewer}
 | 
			
		||||
                                />
 | 
			
		||||
                                <TextInput
 | 
			
		||||
                                    disabled
 | 
			
		||||
@@ -178,11 +182,13 @@ const Content: FC<Props> = ({ card }) => {
 | 
			
		||||
                                    project={project}
 | 
			
		||||
                                    {...form.getInputProps("board")}
 | 
			
		||||
                                    label={"Доска"}
 | 
			
		||||
                                    readOnly={isDealsViewer}
 | 
			
		||||
                                />
 | 
			
		||||
                                <CardStatusSelect
 | 
			
		||||
                                    board={form.values.board}
 | 
			
		||||
                                    {...form.getInputProps("status")}
 | 
			
		||||
                                    label={"Статус"}
 | 
			
		||||
                                    readOnly={isDealsViewer}
 | 
			
		||||
                                />
 | 
			
		||||
                                <Textarea
 | 
			
		||||
                                    h={rem(120)}
 | 
			
		||||
@@ -191,73 +197,78 @@ const Content: FC<Props> = ({ card }) => {
 | 
			
		||||
                                        input: { height: "90%" },
 | 
			
		||||
                                    }}
 | 
			
		||||
                                    label={"Коментарий"}
 | 
			
		||||
                                    placeholder={"Введите коментарий"}
 | 
			
		||||
                                    placeholder={isDealsViewer ? "" : "Введите коментарий"}
 | 
			
		||||
                                    {...form.getInputProps("comment")}
 | 
			
		||||
                                    readOnly={isDealsViewer}
 | 
			
		||||
                                />
 | 
			
		||||
                                {project && project?.tags.length > 0 && (
 | 
			
		||||
                                    <CardTagsInput
 | 
			
		||||
                                        value={cardTags}
 | 
			
		||||
                                        onChange={setCardTags}
 | 
			
		||||
                                        readOnly={isDealsViewer}
 | 
			
		||||
                                    />
 | 
			
		||||
                                )}
 | 
			
		||||
                                {project && (
 | 
			
		||||
                                    <CardAttributeFields
 | 
			
		||||
                                        project={project}
 | 
			
		||||
                                        form={form}
 | 
			
		||||
                                        readOnly={isDealsViewer}
 | 
			
		||||
                                    />
 | 
			
		||||
                                )}
 | 
			
		||||
                            </Flex>
 | 
			
		||||
                        </Fieldset>
 | 
			
		||||
                    </Stack>
 | 
			
		||||
                </ScrollArea>
 | 
			
		||||
                <Flex
 | 
			
		||||
                    mt={"md"}
 | 
			
		||||
                    gap={rem(10)}
 | 
			
		||||
                    align={"center"}
 | 
			
		||||
                    justify={"flex-end"}>
 | 
			
		||||
                {!isDealsViewer && (
 | 
			
		||||
                    <Flex
 | 
			
		||||
                        align={"center"}
 | 
			
		||||
                        mt={"md"}
 | 
			
		||||
                        gap={rem(10)}
 | 
			
		||||
                        justify={"center"}>
 | 
			
		||||
                        <ButtonCopyControlled
 | 
			
		||||
                            onCopyClick={onCopyGuestUrlClick}
 | 
			
		||||
                            onCopiedLabel={
 | 
			
		||||
                                "Ссылка скопирована в буфер обмена"
 | 
			
		||||
                            }
 | 
			
		||||
                            copied={clipboard.copied}
 | 
			
		||||
                        >
 | 
			
		||||
                            Ссылка на редактирование
 | 
			
		||||
                        </ButtonCopyControlled>
 | 
			
		||||
                        <Flex gap={rem(10)}>
 | 
			
		||||
                            <Checkbox
 | 
			
		||||
                                label={"Завершена"}
 | 
			
		||||
                                {...form.getInputProps("isCompleted", { type: "checkbox" })}
 | 
			
		||||
                            />
 | 
			
		||||
                            <Checkbox
 | 
			
		||||
                                label={"Удалена"}
 | 
			
		||||
                                {...form.getInputProps("isDeleted", { type: "checkbox" })}
 | 
			
		||||
                            />
 | 
			
		||||
                        </Flex>
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                    <Divider orientation={"vertical"} />
 | 
			
		||||
                    <Group
 | 
			
		||||
                        align={"center"}
 | 
			
		||||
                        justify={"center"}>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            color={"red"}
 | 
			
		||||
                            type={"reset"}
 | 
			
		||||
                            disabled={isEqualValues()}
 | 
			
		||||
                            onClick={cancelChanges}>
 | 
			
		||||
                            Отменить изменения
 | 
			
		||||
                        </Button>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            variant={"default"}
 | 
			
		||||
                            type={"submit"}
 | 
			
		||||
                            disabled={isEqualValues()}>
 | 
			
		||||
                            Сохранить изменения
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </Group>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                        justify={"flex-end"}>
 | 
			
		||||
                        <Flex
 | 
			
		||||
                            align={"center"}
 | 
			
		||||
                            gap={rem(10)}
 | 
			
		||||
                            justify={"center"}>
 | 
			
		||||
                            <ButtonCopyControlled
 | 
			
		||||
                                onCopyClick={onCopyGuestUrlClick}
 | 
			
		||||
                                onCopiedLabel={
 | 
			
		||||
                                    "Ссылка скопирована в буфер обмена"
 | 
			
		||||
                                }
 | 
			
		||||
                                copied={clipboard.copied}
 | 
			
		||||
                            >
 | 
			
		||||
                                Ссылка на редактирование
 | 
			
		||||
                            </ButtonCopyControlled>
 | 
			
		||||
                            <Flex gap={rem(10)}>
 | 
			
		||||
                                <Checkbox
 | 
			
		||||
                                    label={"Завершена"}
 | 
			
		||||
                                    {...form.getInputProps("isCompleted", { type: "checkbox" })}
 | 
			
		||||
                                />
 | 
			
		||||
                                <Checkbox
 | 
			
		||||
                                    label={"Удалена"}
 | 
			
		||||
                                    {...form.getInputProps("isDeleted", { type: "checkbox" })}
 | 
			
		||||
                                />
 | 
			
		||||
                            </Flex>
 | 
			
		||||
                        </Flex>
 | 
			
		||||
                        <Divider orientation={"vertical"} />
 | 
			
		||||
                        <Group
 | 
			
		||||
                            align={"center"}
 | 
			
		||||
                            justify={"center"}>
 | 
			
		||||
                            <Button
 | 
			
		||||
                                color={"red"}
 | 
			
		||||
                                type={"reset"}
 | 
			
		||||
                                disabled={isEqualValues()}
 | 
			
		||||
                                onClick={cancelChanges}>
 | 
			
		||||
                                Отменить изменения
 | 
			
		||||
                            </Button>
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant={"default"}
 | 
			
		||||
                                type={"submit"}
 | 
			
		||||
                                disabled={isEqualValues()}>
 | 
			
		||||
                                Сохранить изменения
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </Group>
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                )}
 | 
			
		||||
            </Flex>
 | 
			
		||||
        </form>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,8 @@ import { useDisclosure } from "@mantine/hooks";
 | 
			
		||||
import InlineButton from "../../../components/InlineButton/InlineButton.tsx";
 | 
			
		||||
import { IconFilter } from "@tabler/icons-react";
 | 
			
		||||
import BoardSelect from "../../../components/BoardSelect/BoardSelect.tsx";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../redux/store.ts";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    form: UseFormReturnType<CardsPageState>;
 | 
			
		||||
@@ -18,6 +20,7 @@ type Props = {
 | 
			
		||||
 | 
			
		||||
const CardsTableFiltersModal = ({ form, projects }: Props) => {
 | 
			
		||||
    const [opened, { open, close }] = useDisclosure();
 | 
			
		||||
    const { isDealsViewer } = useSelector((state: RootState) => state.auth);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
@@ -60,15 +63,17 @@ const CardsTableFiltersModal = ({ form, projects }: Props) => {
 | 
			
		||||
                        placeholder={"Выберите маркетплейс"}
 | 
			
		||||
                        {...form.getInputProps("marketplace")}
 | 
			
		||||
                    />
 | 
			
		||||
                    <ClientSelectNew
 | 
			
		||||
                        onClear={() =>
 | 
			
		||||
                            form.setFieldValue("client", null)
 | 
			
		||||
                        }
 | 
			
		||||
                        clearable
 | 
			
		||||
                        searchable
 | 
			
		||||
                        placeholder={"Выберите клиента"}
 | 
			
		||||
                        {...form.getInputProps("client")}
 | 
			
		||||
                    />
 | 
			
		||||
                    {!isDealsViewer && (
 | 
			
		||||
                        <ClientSelectNew
 | 
			
		||||
                            onClear={() =>
 | 
			
		||||
                                form.setFieldValue("client", null)
 | 
			
		||||
                            }
 | 
			
		||||
                            clearable
 | 
			
		||||
                            searchable
 | 
			
		||||
                            placeholder={"Выберите клиента"}
 | 
			
		||||
                            {...form.getInputProps("client")}
 | 
			
		||||
                        />
 | 
			
		||||
                    )}
 | 
			
		||||
                </Flex>
 | 
			
		||||
            </Modal>
 | 
			
		||||
        </>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { FC } from "react";
 | 
			
		||||
import ClientsTable from "./components/ClientsTable/ClientsTable.tsx";
 | 
			
		||||
import useClientsList from "./hooks/useClientsList.tsx";
 | 
			
		||||
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
 | 
			
		||||
import styles from "./ClientsPage.module.css";
 | 
			
		||||
@@ -7,9 +6,10 @@ import { Button, TextInput } from "@mantine/core";
 | 
			
		||||
import { modals } from "@mantine/modals";
 | 
			
		||||
import { ClientSchema, ClientService } from "../../client";
 | 
			
		||||
import { notifications } from "../../shared/lib/notifications.ts";
 | 
			
		||||
import { ChatContextProvider } from "./contexts/ChatContext.tsx";
 | 
			
		||||
import ClientChatDrawer from "./drawers/ClientChatDrawer/ClientChatDrawer.tsx";
 | 
			
		||||
import useClientsFilter from "./hooks/useClientsFilter.tsx";
 | 
			
		||||
import { ChatContextProvider } from "./contexts/ChatContext.tsx";
 | 
			
		||||
import ClientsTable from "./components/ClientsTable/ClientsTable.tsx";
 | 
			
		||||
import ClientChatDrawer from "./drawers/ClientChatDrawer/ClientChatDrawer.tsx";
 | 
			
		||||
 | 
			
		||||
const ClientsPage: FC = () => {
 | 
			
		||||
    const { clients, refetch } = useClientsList();
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
import { ActionIcon, rem, Tooltip } from "@mantine/core";
 | 
			
		||||
import { IconCheck, IconLink } from "@tabler/icons-react";
 | 
			
		||||
import { FC } from "react";
 | 
			
		||||
import { useClipboard } from "@mantine/hooks";
 | 
			
		||||
import { ClientSchema, ClientService } from "../../../../client";
 | 
			
		||||
import { notifications } from "../../../../shared/lib/notifications.ts";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    client: ClientSchema;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ClientLinkActionIcon: FC<Props> = ({ client }) => {
 | 
			
		||||
    const clipboard = useClipboard();
 | 
			
		||||
 | 
			
		||||
    const onGetLinkClick = () => {
 | 
			
		||||
        ClientService.clientLink({
 | 
			
		||||
            clientId: client.id,
 | 
			
		||||
        })
 | 
			
		||||
            .then(({ ok, message, url }) => {
 | 
			
		||||
                if (ok) {
 | 
			
		||||
                    clipboard.copy(`${window.location.origin}/${url}`);
 | 
			
		||||
                } else {
 | 
			
		||||
                    notifications.error({ message });
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .catch(err => console.log(err));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Tooltip
 | 
			
		||||
            label={"Ссылка скопирована"}
 | 
			
		||||
            offset={5}
 | 
			
		||||
            radius="xl"
 | 
			
		||||
            transitionProps={{ duration: 100, transition: "slide-down" }}
 | 
			
		||||
            opened={clipboard.copied}>
 | 
			
		||||
            <ActionIcon
 | 
			
		||||
                variant={"default"}
 | 
			
		||||
                size="md"
 | 
			
		||||
                onClick={() => onGetLinkClick()}
 | 
			
		||||
            >
 | 
			
		||||
                {clipboard.copied ? (
 | 
			
		||||
                    <IconCheck
 | 
			
		||||
                        style={{ width: rem(20), height: rem(20) }}
 | 
			
		||||
                        stroke={1.5}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <IconLink
 | 
			
		||||
                        style={{ width: rem(20), height: rem(20) }}
 | 
			
		||||
                        stroke={1.5}
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
            </ActionIcon>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ClientLinkActionIcon;
 | 
			
		||||
@@ -8,6 +8,7 @@ import { IconEdit, IconTrash } from "@tabler/icons-react";
 | 
			
		||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
 | 
			
		||||
import { modals } from "@mantine/modals";
 | 
			
		||||
import ClientChatButton from "../ClientChatButton/ClientChatButton.tsx";
 | 
			
		||||
import ClientLinkActionIcon from "../ClientLinkActionIcon/ClientLinkActionIcon.tsx";
 | 
			
		||||
 | 
			
		||||
type RefetchProps = {
 | 
			
		||||
    refetch: () => void;
 | 
			
		||||
@@ -46,7 +47,8 @@ const ClientsTable: FC<CRUDTableProps<ClientSchema> & RefetchProps> = ({
 | 
			
		||||
                        enableRowActions: true,
 | 
			
		||||
                        renderRowActions: ({ row }) => (
 | 
			
		||||
                            <Flex gap="md">
 | 
			
		||||
                                <ClientChatButton client={row.original} refetch={refetch}/>
 | 
			
		||||
                                <ClientChatButton client={row.original} refetch={refetch} />
 | 
			
		||||
                                <ClientLinkActionIcon client={row.original} />
 | 
			
		||||
                                <Tooltip label="Редактировать">
 | 
			
		||||
                                    <ActionIcon
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,11 +8,16 @@ type Props = {
 | 
			
		||||
 | 
			
		||||
const useClientsFilter = ({ clients }: Props) => {
 | 
			
		||||
    const [search, setSearch] = useState<string>("");
 | 
			
		||||
    const debouncedSearch = useDebouncedValue(search, 400);
 | 
			
		||||
    const [debouncedSearch] = useDebouncedValue(search, 400);
 | 
			
		||||
    const [filteredClients, setFilteredClients] = useState<ClientSchema[]>([]);
 | 
			
		||||
 | 
			
		||||
    const filterClients = () => {
 | 
			
		||||
        const loweredSearch = search.toLowerCase();
 | 
			
		||||
        if (debouncedSearch.length === 0) {
 | 
			
		||||
            setFilteredClients(clients);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const loweredSearch = debouncedSearch.toLowerCase();
 | 
			
		||||
        const filtered = clients.filter(
 | 
			
		||||
            client => (
 | 
			
		||||
                client.name.toLowerCase().includes(loweredSearch) ||
 | 
			
		||||
@@ -28,7 +33,7 @@ const useClientsFilter = ({ clients }: Props) => {
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        filterClients();
 | 
			
		||||
    }, [debouncedSearch]);
 | 
			
		||||
    }, [debouncedSearch, clients]);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        search,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user