feat: deals viewer mode and links for viewers

This commit is contained in:
2025-07-04 15:12:37 +04:00
parent 82e2ef6db2
commit c3d135eba9
27 changed files with 935 additions and 734 deletions

File diff suppressed because it is too large Load Diff

View 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;
};

View File

@@ -11,6 +11,7 @@ import type { ClientGetResponse } from '../models/ClientGetResponse';
import type { ClientUpdateDetailsRequest } from '../models/ClientUpdateDetailsRequest'; import type { ClientUpdateDetailsRequest } from '../models/ClientUpdateDetailsRequest';
import type { ClientUpdateRequest } from '../models/ClientUpdateRequest'; import type { ClientUpdateRequest } from '../models/ClientUpdateRequest';
import type { ClientUpdateResponse } from '../models/ClientUpdateResponse'; import type { ClientUpdateResponse } from '../models/ClientUpdateResponse';
import type { CreateGuestUrlResponse } from '../models/CreateGuestUrlResponse';
import type { CancelablePromise } from '../core/CancelablePromise'; import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI'; import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request'; 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`,
},
});
}
} }

View File

@@ -8,9 +8,10 @@ import { CardGeneralFormType } from "../../pages/CardsPage/drawers/CardEditDrawe
type Props = { type Props = {
project: ProjectSchema; project: ProjectSchema;
form: UseFormReturnType<CardGeneralFormType>; form: UseFormReturnType<CardGeneralFormType>;
readOnly: boolean;
} }
const CardAttributeFields = ({ project, form }: Props) => { const CardAttributeFields = ({ project, form, readOnly }: Props) => {
const attributes: AttributeSchema[] = []; const attributes: AttributeSchema[] = [];
project.attributes.forEach(attribute => { project.attributes.forEach(attribute => {
@@ -27,6 +28,7 @@ const CardAttributeFields = ({ project, form }: Props) => {
key={attribute.id} key={attribute.id}
attribute={attribute} attribute={attribute}
form={form} form={form}
readOnly={readOnly}
/> />
); );
}); });

View File

@@ -8,9 +8,10 @@ import { IconInfoCircle } from "@tabler/icons-react";
type Props = { type Props = {
attribute: AttributeSchema; attribute: AttributeSchema;
form: UseFormReturnType<CardGeneralFormType>; form: UseFormReturnType<CardGeneralFormType>;
readOnly: boolean;
} }
const CardAttributeField = ({ attribute, form }: Props) => { const CardAttributeField = ({ attribute, form, readOnly }: Props) => {
const type = attribute.type.type; const type = attribute.type.type;
const getDateValue = (): Date | null => { const getDateValue = (): Date | null => {
@@ -35,6 +36,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
<Checkbox <Checkbox
label={label} label={label}
{...form.getInputProps(attribute.name, { type: "checkbox" })} {...form.getInputProps(attribute.name, { type: "checkbox" })}
readOnly={readOnly}
/> />
); );
} }
@@ -47,6 +49,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
clearable clearable
locale={"ru-RU"} locale={"ru-RU"}
valueFormat="DD.MM.YYYY" valueFormat="DD.MM.YYYY"
readOnly={readOnly}
/> />
); );
} }
@@ -59,6 +62,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
clearable clearable
locale={"ru-RU"} locale={"ru-RU"}
valueFormat="DD.MM.YYYY HH:mm" valueFormat="DD.MM.YYYY HH:mm"
readOnly={readOnly}
/> />
); );
} }
@@ -68,6 +72,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
label={label} label={label}
{...form.getInputProps(attribute.name)} {...form.getInputProps(attribute.name)}
value={form.getInputProps(attribute.name).value ?? ""} value={form.getInputProps(attribute.name).value ?? ""}
readOnly={readOnly}
/> />
); );
} }
@@ -77,6 +82,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
allowDecimal={type === "float"} allowDecimal={type === "float"}
label={label} label={label}
{...form.getInputProps(attribute.name)} {...form.getInputProps(attribute.name)}
readOnly={readOnly}
/> />
); );
} }

View File

@@ -8,6 +8,8 @@ import useStatus from "../../Statuses/Status/hooks/useStatus.tsx";
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx"; import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
import DragState from "../../../../pages/CardsPage/enums/DragState.ts"; import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx"; import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
type Props = { type Props = {
@@ -22,6 +24,7 @@ const Board = ({ board }: Props) => {
onDeleteBoardClick, onDeleteBoardClick,
} = useBoardsContext(); } = useBoardsContext();
const authState = useSelector((state: RootState) => state.auth);
const { dragState } = useDndContext(); const { dragState } = useDndContext();
const { showContextMenu } = useContextMenu(); const { showContextMenu } = useContextMenu();
@@ -65,6 +68,7 @@ const Board = ({ board }: Props) => {
<Draggable <Draggable
draggableId={"board-" + board.id.toString()} draggableId={"board-" + board.id.toString()}
index={board.ordinalNumber} index={board.ordinalNumber}
isDragDisabled={authState.isGuest}
> >
{(provided) => ( {(provided) => (
<div <div

View File

@@ -8,6 +8,8 @@ import PrefillCardsWithExcelDrawer
from "../../../../pages/CardsPage/drawers/PrefillCardWithExcelDrawer/PrefillCardsWithExcelDrawer.tsx"; from "../../../../pages/CardsPage/drawers/PrefillCardWithExcelDrawer/PrefillCardsWithExcelDrawer.tsx";
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx"; import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx"; import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
const Boards = () => { const Boards = () => {
@@ -16,6 +18,7 @@ const Boards = () => {
selectedBoard, selectedBoard,
onCreateBoardClick, onCreateBoardClick,
} = useBoardsContext(); } = useBoardsContext();
const authState = useSelector((state: RootState) => state.auth);
const { const {
onDragEnd, onDragEnd,
@@ -37,6 +40,7 @@ const Boards = () => {
board={board} board={board}
/> />
))} ))}
{!authState.isGuest && (
<Center <Center
px={"md"} px={"md"}
py={"xs"} py={"xs"}
@@ -45,6 +49,7 @@ const Boards = () => {
> >
<IconPlus /> <IconPlus />
</Center> </Center>
)}
<Box w={"100%"} style={{ borderBottom: "solid gray 1px" }}></Box> <Box w={"100%"} style={{ borderBottom: "solid gray 1px" }}></Box>
</Group> </Group>
); );

View File

@@ -9,6 +9,8 @@ import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts"; import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
import CardTags from "../CardTags/CardTags.tsx"; import CardTags from "../CardTags/CardTags.tsx";
import { ModuleNames } from "../../../../modules/modules.tsx"; import { ModuleNames } from "../../../../modules/modules.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
type Props = { type Props = {
cards: CardSummary[]; cards: CardSummary[];
@@ -21,6 +23,7 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
const [debouncedName] = useDebouncedValue(name, 200); const [debouncedName] = useDebouncedValue(name, 200);
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject); 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 totalPrice = useMemo(() => cards.reduce((acc, card) => acc + card.totalPrice, 0), [cards]);
const totalProducts = useMemo(() => cards.reduce((acc, card) => acc + card.totalProducts, 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> </Flex>
<CardTags groupId={group.id} tags={cards[0].tags}/> {!authState.isGuest && <CardTags groupId={group.id} tags={cards[0].tags}/>}
{isServicesAndProductsIncluded && ( {isServicesAndProductsIncluded && (
<Flex <Flex
p={rem(10)} p={rem(10)}

View File

@@ -15,6 +15,8 @@ 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"; import isDealPaid from "../../../../pages/CardsPage/utils/isDealPaid.ts";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
type Props = { type Props = {
cardSummary: CardSummary; cardSummary: CardSummary;
@@ -27,6 +29,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
const { setSelectedCard } = useCardPageContext(); const { setSelectedCard } = useCardPageContext();
const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState(); const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState();
const [isPaid, setIsPaid] = useState<boolean>(false); const [isPaid, setIsPaid] = useState<boolean>(false);
const authState = useSelector((state: RootState) => state.auth);
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);
@@ -36,8 +39,8 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
setSelectedCard(card); setSelectedCard(card);
}); });
}; };
const isLockedInsideGroup = () => { const canBeRemovedFromGroup = () => {
return !!(cardSummary?.group?.billRequests && cardSummary?.group?.billRequests?.length !== 0); return !!(cardSummary.group && (!cardSummary.group.billRequests || cardSummary.group.billRequests?.length === 0));
}; };
useEffect(() => { useEffect(() => {
@@ -46,8 +49,8 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
return ( return (
<div <div
onContextMenu={showContextMenu([ onContextMenu={!authState.isDealsViewer ? showContextMenu([
...isLockedInsideGroup() ? [] : [{ ...!canBeRemovedFromGroup() ? [] : [{
key: "removeFromGroup", key: "removeFromGroup",
onClick: () => onDeleteFromGroup(cardSummary), onClick: () => onDeleteFromGroup(cardSummary),
title: "Убрать из группы", title: "Убрать из группы",
@@ -65,7 +68,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
title: "Удалить", title: "Удалить",
icon: <IconTrash />, icon: <IconTrash />,
}, },
])} ]) : undefined}
onClick={() => onCardSummaryClick()} onClick={() => onCardSummaryClick()}
className={styles["container"]} className={styles["container"]}
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
@@ -108,7 +111,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
</Flex> </Flex>
)} )}
<CardAttributesInSummaryItem cardSummary={cardSummary} /> <CardAttributesInSummaryItem cardSummary={cardSummary} />
{!cardSummary.group?.id && ( {!authState.isGuest && !cardSummary.group?.id && (
<CardTags cardId={cardSummary.id} tags={cardSummary.tags} /> <CardTags cardId={cardSummary.id} tags={cardSummary.tags} />
)} )}
<Flex align={"center"} justify={"space-between"}> <Flex align={"center"} justify={"space-between"}>

View File

@@ -12,6 +12,8 @@ import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts"; import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx"; import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import { ModuleNames } from "../../../../modules/modules.tsx"; import { ModuleNames } from "../../../../modules/modules.tsx";
import { RootState } from "../../../../redux/store.ts";
import { useSelector } from "react-redux";
type Props = { type Props = {
status: StatusSchema; status: StatusSchema;
@@ -28,6 +30,7 @@ export const CardsDndColumn: FC<Props> = ({
dragState, dragState,
withCreateButton = false, withCreateButton = false,
}) => { }) => {
const authState = useSelector((state: RootState) => state.auth);
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const isCreatingDealFromFileEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject); const isCreatingDealFromFileEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
const isDropDisabled = dragState !== DragState.DRAG_CARD; const isDropDisabled = dragState !== DragState.DRAG_CARD;
@@ -77,7 +80,9 @@ export const CardsDndColumn: FC<Props> = ({
<Draggable <Draggable
draggableId={card.id.toString()} draggableId={card.id.toString()}
index={card.rank} index={card.rank}
key={card.id}> key={card.id}
isDragDisabled={authState.isGuest}
>
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
{...provided.draggableProps} {...provided.draggableProps}
@@ -104,6 +109,7 @@ export const CardsDndColumn: FC<Props> = ({
draggableId={"group-" + group.id} draggableId={"group-" + group.id}
index={cards[0].rank} index={cards[0].rank}
key={"group-" + group.id} key={"group-" + group.id}
isDragDisabled={authState.isGuest}
> >
{(provided) => ( {(provided) => (
<div <div
@@ -135,7 +141,7 @@ export const CardsDndColumn: FC<Props> = ({
styles["items-list-drag-over"], styles["items-list-drag-over"],
)} )}
{...provided.droppableProps}> {...provided.droppableProps}>
{withCreateButton && ( {withCreateButton && !authState.isGuest && (
<> <>
<CreateCardButton status={status} /> <CreateCardButton status={status} />
{ {

View File

@@ -10,6 +10,7 @@ import { usePrefillCardContext } from "../../../../pages/CardsPage/contexts/Pref
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts"; import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx"; import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import { ModuleNames } from "../../../../modules/modules.tsx"; import { ModuleNames } from "../../../../modules/modules.tsx";
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
type Props = { type Props = {
status: StatusSchema; status: StatusSchema;
@@ -20,6 +21,7 @@ const CreateCardButton = ({ status }: Props) => {
const [isTransitionEnded, setIsTransitionEnded] = useState(true); const [isTransitionEnded, setIsTransitionEnded] = useState(true);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { prefillCard, setPrefillCard } = usePrefillCardContext(); const { prefillCard, setPrefillCard } = usePrefillCardContext();
const { refetchSummaries } = useDndContext();
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const isPrefillingDealEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject); const isPrefillingDealEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
@@ -57,7 +59,7 @@ const CreateCardButton = ({ status }: Props) => {
}, },
}).then(async (result) => { }).then(async (result) => {
if (isPrefillingDealEnabled && prefillCard) { if (isPrefillingDealEnabled && prefillCard) {
CardService.prefillCard({ await CardService.prefillCard({
requestBody: { requestBody: {
oldCardId: prefillCard.id, oldCardId: prefillCard.id,
newCardId: result.cardId, newCardId: result.cardId,
@@ -67,6 +69,7 @@ const CreateCardButton = ({ status }: Props) => {
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["getCardSummaries"], queryKey: ["getCardSummaries"],
}); });
refetchSummaries();
setIsCreating(false); setIsCreating(false);
setPrefillCard(undefined); setPrefillCard(undefined);
}); });

View File

@@ -13,6 +13,8 @@ import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
import { useEqualHeightsContext } from "./contexts/EqualHeightContext.tsx"; import { useEqualHeightsContext } from "./contexts/EqualHeightContext.tsx";
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx"; import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
import { ModuleNames } from "../../../../modules/modules.tsx"; import { ModuleNames } from "../../../../modules/modules.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
type Props = { type Props = {
@@ -26,6 +28,7 @@ const Status = ({ summaries, status, dragState, index }: Props) => {
const { const {
selectedBoard, selectedBoard,
} = useBoardsContext(); } = useBoardsContext();
const authState = useSelector((state: RootState) => state.auth);
const isDropDisabled = dragState !== DragState.DRAG_STATUS; const isDropDisabled = dragState !== DragState.DRAG_STATUS;
const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedBoard?.project); const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedBoard?.project);
@@ -107,6 +110,7 @@ const Status = ({ summaries, status, dragState, index }: Props) => {
<Draggable <Draggable
draggableId={"status-" + status.id.toString()} draggableId={"status-" + status.id.toString()}
index={status.ordinalNumber} index={status.ordinalNumber}
isDragDisabled={authState.isGuest}
> >
{(provided) => ( {(provided) => (
<div <div

View File

@@ -4,6 +4,8 @@ import { jwtDecode, JwtPayload as JwtPayloadBase } from "jwt-decode";
interface AuthState { interface AuthState {
isAuthorized: boolean; isAuthorized: boolean;
accessToken: string; accessToken: string;
isDealEditor: boolean;
isDealsViewer: boolean;
isGuest: boolean; isGuest: boolean;
role: string; role: string;
} }
@@ -16,6 +18,8 @@ const initialState = (): AuthState => {
return { return {
accessToken: "", accessToken: "",
isAuthorized: false, isAuthorized: false,
isDealEditor: false,
isDealsViewer: false,
isGuest: false, isGuest: false,
role: "user", role: "user",
}; };
@@ -37,7 +41,9 @@ const authSlice = createSlice({
state.accessToken = action.payload.accessToken; state.accessToken = action.payload.accessToken;
state.isAuthorized = true; state.isAuthorized = true;
state.role = role; 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 (_) { } catch (_) {
const url = window.location.href; const url = window.location.href;
const urlObj = new URL(url); const urlObj = new URL(url);

View File

@@ -21,11 +21,16 @@ import PrintDealBarcodesButton from "./components/PrintDealBarcodesButton/PrintD
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"; 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 ProductAndServiceTab: FC = () => {
const { cardState, cardServicesState, cardProductsState } = useCardProductAndServiceTabState(); const { cardState, cardServicesState, cardProductsState } = useCardProductAndServiceTabState();
const { selectedCard: card } = useCardPageContext();
const isLocked = isDealLocked(cardState.card); const isLocked = isDealLocked(cardState.card);
const [paid, setPaid] = useState<boolean>(false); const [paid, setPaid] = useState<boolean>(false);
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
useEffect(() => { useEffect(() => {
setPaid(isDealPaid(cardState.card)); setPaid(isDealPaid(cardState.card));
@@ -128,7 +133,9 @@ const ProductAndServiceTab: FC = () => {
ProductService.createProduct({ ProductService.createProduct({
requestBody: newProduct, requestBody: newProduct,
}).then(({ ok, message }) => { }).then(({ ok, message }) => {
notifications.guess(ok, { message: message }); notifications.guess(ok, { message });
}).catch(err => {
notifications.error({ message: err.toString() });
}); });
}; };
const onCreateProductClick = () => { const onCreateProductClick = () => {
@@ -256,18 +263,18 @@ const ProductAndServiceTab: FC = () => {
mr={"xs"} mr={"xs"}
> >
<Group wrap={"nowrap"}> <Group wrap={"nowrap"}>
<PrintDealBarcodesButton card={cardState.card} /> {!isDealsViewer && <PrintDealBarcodesButton card={cardState.card} />}
<Checkbox <Checkbox
label={"Оплачен"} label={"Оплачен"}
checked={paid} checked={paid}
disabled disabled
/> />
</Group> </Group>
<PaymentLinkButton card={cardState.card} /> {!isDealsViewer && <PaymentLinkButton card={cardState.card} />}
</Group> </Group>
)} )}
<Stack className={styles["card-container-wrapper"]} mr={"xs"}> <Stack className={styles["card-container-wrapper"]} mr={"xs"}>
<GeneralDataForm /> {card && <GeneralDataForm />}
</Stack> </Stack>
<ScrollArea offsetScrollbars> <ScrollArea offsetScrollbars>
<Flex <Flex
@@ -278,6 +285,8 @@ const ProductAndServiceTab: FC = () => {
{...cardServicesState} {...cardServicesState}
/> />
{!isDealsViewer && (
<>
<Divider my={rem(15)} /> <Divider my={rem(15)} />
<div className={styles["card-container-buttons"]}> <div className={styles["card-container-buttons"]}>
<Button <Button
@@ -313,6 +322,8 @@ const ProductAndServiceTab: FC = () => {
</Button> </Button>
)} )}
</div> </div>
</>
)}
</Flex> </Flex>
<Flex <Flex
direction={"column"} direction={"column"}

View File

@@ -138,6 +138,7 @@ const CardServicesTable: FC<Props> = ({
w={"100%"} w={"100%"}
gap={rem(10)} gap={rem(10)}
align={"center"}> align={"center"}>
{!authState.isDealsViewer && (
<Tooltip <Tooltip
onClick={() => onDeleteClick(service)} onClick={() => onDeleteClick(service)}
label="Удалить услугу"> label="Удалить услугу">
@@ -147,6 +148,7 @@ const CardServicesTable: FC<Props> = ({
<IconTrash /> <IconTrash />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
)}
{!authState.isGuest && ( {!authState.isGuest && (
<Tooltip label="Сотрудники"> <Tooltip label="Сотрудники">
<ActionIcon <ActionIcon
@@ -167,9 +169,8 @@ const CardServicesTable: FC<Props> = ({
isNumber(event) && isNumber(event) &&
onQuantityChange(service, event) onQuantityChange(service, event)
} }
value={service.quantity} value={service.quantity}
readOnly={authState.isDealsViewer}
/> />
<NumberInput <NumberInput
flex={1} flex={1}
@@ -180,6 +181,7 @@ const CardServicesTable: FC<Props> = ({
suffix={"₽"} suffix={"₽"}
value={service.price} value={service.price}
disabled={authState.isGuest || isLocked || service.isFixedPrice} disabled={authState.isGuest || isLocked || service.isFixedPrice}
readOnly={authState.isDealsViewer}
rightSectionProps={{ rightSectionProps={{
style: { style: {
display: "flex", display: "flex",
@@ -188,12 +190,14 @@ const CardServicesTable: FC<Props> = ({
}, },
}} }}
rightSection={ rightSection={
!authState.isDealsViewer && (
<LockCheckbox <LockCheckbox
label={"Зафиксировать цену"} label={"Зафиксировать цену"}
variant={"default"} variant={"default"}
value={service.isFixedPrice} value={service.isFixedPrice}
onChange={value => onLockChange(service, value)} onChange={value => onLockChange(service, value)}
/> />
)
} }
/> />
</Flex> </Flex>
@@ -212,6 +216,7 @@ const CardServicesTable: FC<Props> = ({
</Title> </Title>
</Flex> </Flex>
{!authState.isDealsViewer && (
<Flex <Flex
direction={"column"} direction={"column"}
gap={rem(10)} gap={rem(10)}
@@ -231,6 +236,7 @@ const CardServicesTable: FC<Props> = ({
Добавить набор услуг Добавить набор услуг
</Button> </Button>
</Flex> </Flex>
)}
</Flex> </Flex>
<Modal <Modal
title={"Добавление сотрудника к услуге"} title={"Добавление сотрудника к услуге"}

View File

@@ -7,6 +7,8 @@ import { Button, Checkbox, Stack } from "@mantine/core";
import { notifications } from "../../../../../../shared/lib/notifications.ts"; import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts";
type GeneralDataFormType = { type GeneralDataFormType = {
@@ -16,7 +18,8 @@ type GeneralDataFormType = {
const GeneralDataForm = () => { const GeneralDataForm = () => {
const { selectedCard: card, refetchCard } = useCardPageContext(); const { selectedCard: card, refetchCard } = useCardPageContext();
if (!card) return; const { isDealsViewer } = useSelector((state: RootState) => state.auth);
if (!card) return <></>;
const [initialValues, setInitialValues] = useState<GeneralDataFormType>(card); const [initialValues, setInitialValues] = useState<GeneralDataFormType>(card);
@@ -67,7 +70,7 @@ const GeneralDataForm = () => {
<form onSubmit={form.onSubmit(values => onSubmit(values))}> <form onSubmit={form.onSubmit(values => onSubmit(values))}>
<Stack> <Stack>
<ShippingWarehouseAutocomplete <ShippingWarehouseAutocomplete
placeholder={"Введите склад отгрузки"} placeholder={isDealsViewer ? "" : "Введите склад отгрузки"}
label={"Склад отгрузки"} label={"Склад отгрузки"}
value={ value={
isShippingWarehouse( isShippingWarehouse(
@@ -87,7 +90,10 @@ const GeneralDataForm = () => {
"shippingWarehouse", "shippingWarehouse",
).onChange(event); ).onChange(event);
}} }}
readOnly={isDealsViewer}
/> />
{!isDealsViewer && (
<>
<Checkbox <Checkbox
label={"Учет выручки в статистике"} label={"Учет выручки в статистике"}
{...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })} {...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
@@ -99,6 +105,8 @@ const GeneralDataForm = () => {
> >
Сохранить Сохранить
</Button> </Button>
</>
)}
</Stack> </Stack>
</form> </form>
); );

View File

@@ -103,8 +103,8 @@ const ProductServicesTable: FC<Props> = ({
{ {
enableColumnActions: false, enableColumnActions: false,
enableSorting: false, enableSorting: false,
enableRowActions: true, enableRowActions: !authState.isDealsViewer,
enableBottomToolbar: true, enableBottomToolbar: !authState.isDealsViewer,
renderBottomToolbar: ( renderBottomToolbar: (
<Flex <Flex
justify={"flex-end"} justify={"flex-end"}

View File

@@ -15,6 +15,8 @@ import { ServiceType } from "../../../../../../shared/enums/ServiceType.ts";
import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx"; import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
import { useDebouncedCallback } from "@mantine/hooks"; import { useDebouncedCallback } from "@mantine/hooks";
import { isDealLocked } from "../../../../../../pages/CardsPage/utils/isDealPaid.ts"; import { isDealLocked } from "../../../../../../pages/CardsPage/utils/isDealPaid.ts";
import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts";
type Props = { type Props = {
product: CardProductSchema; product: CardProductSchema;
@@ -54,6 +56,8 @@ const ProductView: FC<Props> = ({
onDelete(product); onDelete(product);
}; };
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
const onServiceDelete = (item: CardProductServiceSchema) => { const onServiceDelete = (item: CardProductServiceSchema) => {
if (!onChange) return; if (!onChange) return;
onChange({ onChange({
@@ -162,7 +166,9 @@ const ProductView: FC<Props> = ({
isNumber(event) && onQuantityChange(event) isNumber(event) && onQuantityChange(event)
} }
placeholder={"Введите количество товара"} placeholder={"Введите количество товара"}
readOnly={isDealsViewer}
/> />
{!(isDealsViewer && product.comment?.length === 0) && (
<Textarea <Textarea
mih={rem(140)} mih={rem(140)}
styles={{ styles={{
@@ -179,8 +185,10 @@ const ProductView: FC<Props> = ({
comment: event.currentTarget.value, comment: event.currentTarget.value,
}); });
}} }}
placeholder={"Введите комментарий для товара"} placeholder={isDealsViewer ? "" : "Введите комментарий для товара"}
readOnly={isDealsViewer}
/> />
)}
</div> </div>
<div className={styles["services-container"]}> <div className={styles["services-container"]}>
@@ -195,6 +203,7 @@ const ProductView: FC<Props> = ({
onDelete={onServiceDelete} onDelete={onServiceDelete}
onChange={onServiceChange} onChange={onServiceChange}
/> />
{!isDealsViewer && (
<Flex <Flex
mt={"auto"} mt={"auto"}
ml={"auto"} ml={"auto"}
@@ -223,6 +232,7 @@ const ProductView: FC<Props> = ({
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Flex> </Flex>
)}
</div> </div>
</div> </div>
); );

View File

@@ -1,7 +1,9 @@
import React, { createContext, FC, useContext, useEffect, useState } from "react"; import React, { createContext, FC, useContext, useEffect, useState } from "react";
import { useProjectsContext } from "../../contexts/ProjectsContext.tsx"; import { useProjectsContext } from "../../contexts/ProjectsContext.tsx";
import { MODULES } from "../modules.tsx"; import { ModuleNames, MODULES } from "../modules.tsx";
import { Module } from "../types.tsx"; import { Module } from "../types.tsx";
import { RootState } from "../../redux/store.ts";
import { useSelector } from "react-redux";
type ModulesContextState = { type ModulesContextState = {
modules: Module[]; modules: Module[];
@@ -13,15 +15,24 @@ const ModulesContext = createContext<ModulesContextState | undefined>(
const useModulesContextState = () => { const useModulesContextState = () => {
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
const [modules, setModules] = useState<Module[]>([]); 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(() => { useEffect(() => {
const modules = selectedProject?.modules ?? []; const modules = selectedProject?.modules ?? [];
const projectModules = modules.map(module => { const projectModules = modules.map(module => {
return MODULES[module.key]; return MODULES[module.key];
}) ?? []; }) ?? [];
setModules(projectModules); setModules(filterModules(projectModules));
}, [selectedProject?.id]); }, [selectedProject?.id]);
return { return {

View File

@@ -22,42 +22,42 @@ const modules: ModulesType = {
[ModuleNames.CLIENTS]: { [ModuleNames.CLIENTS]: {
info: { info: {
label: "Клиенты", label: "Клиенты",
key: "clients", key: ModuleNames.CLIENTS,
icon: <IconUser />, icon: <IconUser />,
} }
}, },
[ModuleNames.SERVICES_AND_PRODUCTS]: { [ModuleNames.SERVICES_AND_PRODUCTS]: {
info: { info: {
label: "Товары и услуги", label: "Товары и услуги",
key: "servicesAndProducts", key: ModuleNames.SERVICES_AND_PRODUCTS,
icon: <IconBox />, icon: <IconBox />,
} }
}, },
[ModuleNames.SHIPMENT]: { [ModuleNames.SHIPMENT]: {
info: { info: {
label: "Отгрузка", label: "Отгрузка",
key: "shipment", key: ModuleNames.SHIPMENT,
icon: <IconCubeSend />, icon: <IconCubeSend />,
} }
}, },
[ModuleNames.EMPLOYEES]: { [ModuleNames.EMPLOYEES]: {
info: { info: {
label: "Сотрудники", label: "Сотрудники",
key: "employees", key: ModuleNames.EMPLOYEES,
icon: <IconUsersGroup />, icon: <IconUsersGroup />,
} }
}, },
[ModuleNames.MANAGERS]: { [ModuleNames.MANAGERS]: {
info: { info: {
label: "Менеджер", label: "Менеджер",
key: "managers", key: ModuleNames.MANAGERS,
icon: <IconUserCog />, icon: <IconUserCog />,
} }
}, },
[ModuleNames.CHAT]: { [ModuleNames.CHAT]: {
info: { info: {
label: "Чат", label: "Чат",
key: "chat", key: ModuleNames.CHAT,
icon: <IconMessage />, icon: <IconMessage />,
} }
}, },

View File

@@ -11,6 +11,7 @@ type DndContextState = {
dragState: DragState, dragState: DragState,
onDragStart: (start: DragStart) => void, onDragStart: (start: DragStart) => void,
onDragEnd: (result: DropResult) => Promise<void>, onDragEnd: (result: DropResult) => Promise<void>,
refetchSummaries: () => void;
}; };
const DndContext = createContext<DndContextState | undefined>(undefined); const DndContext = createContext<DndContextState | undefined>(undefined);
@@ -64,6 +65,7 @@ const useDndContextState = ({
dragState, dragState,
onDragStart, onDragStart,
onDragEnd, onDragEnd,
refetchSummaries,
}; };
}; };

View File

@@ -26,6 +26,8 @@ import CardStatusSelect from "../../../../../../components/CardStatusSelect/Card
import CardAttributeFields from "../../../../../../components/CardAttributeFields/CardAttributeFields.tsx"; import CardAttributeFields from "../../../../../../components/CardAttributeFields/CardAttributeFields.tsx";
import getAttributesFromCard from "../../../../../../components/CardAttributeFields/utils/getAttributesFromCard.ts"; import getAttributesFromCard from "../../../../../../components/CardAttributeFields/utils/getAttributesFromCard.ts";
import CardTagsInput from "../../../../components/CardTagsInput/CardTagsInput.tsx"; import CardTagsInput from "../../../../components/CardTagsInput/CardTagsInput.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts";
type Props = { type Props = {
card: CardSchema; card: CardSchema;
@@ -43,6 +45,7 @@ const Content: FC<Props> = ({ card }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [project, setProject] = useState<ProjectSchema | null>(card.board.project); const [project, setProject] = useState<ProjectSchema | null>(card.board.project);
const [cardTags, setCardTags] = useState<string[]>(card.tags?.map(tag => tag.name) ?? []); const [cardTags, setCardTags] = useState<string[]>(card.tags?.map(tag => tag.name) ?? []);
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
const getInitialValues = (card: CardSchema): CardGeneralFormType => { const getInitialValues = (card: CardSchema): CardGeneralFormType => {
return { return {
@@ -159,6 +162,7 @@ const Content: FC<Props> = ({ card }) => {
placeholder={"Название сделки"} placeholder={"Название сделки"}
label={"Название сделки"} label={"Название сделки"}
{...form.getInputProps("name")} {...form.getInputProps("name")}
readOnly={isDealsViewer}
/> />
<TextInput <TextInput
disabled disabled
@@ -178,11 +182,13 @@ const Content: FC<Props> = ({ card }) => {
project={project} project={project}
{...form.getInputProps("board")} {...form.getInputProps("board")}
label={"Доска"} label={"Доска"}
readOnly={isDealsViewer}
/> />
<CardStatusSelect <CardStatusSelect
board={form.values.board} board={form.values.board}
{...form.getInputProps("status")} {...form.getInputProps("status")}
label={"Статус"} label={"Статус"}
readOnly={isDealsViewer}
/> />
<Textarea <Textarea
h={rem(120)} h={rem(120)}
@@ -191,25 +197,29 @@ const Content: FC<Props> = ({ card }) => {
input: { height: "90%" }, input: { height: "90%" },
}} }}
label={"Коментарий"} label={"Коментарий"}
placeholder={"Введите коментарий"} placeholder={isDealsViewer ? "" : "Введите коментарий"}
{...form.getInputProps("comment")} {...form.getInputProps("comment")}
readOnly={isDealsViewer}
/> />
{project && project?.tags.length > 0 && ( {project && project?.tags.length > 0 && (
<CardTagsInput <CardTagsInput
value={cardTags} value={cardTags}
onChange={setCardTags} onChange={setCardTags}
readOnly={isDealsViewer}
/> />
)} )}
{project && ( {project && (
<CardAttributeFields <CardAttributeFields
project={project} project={project}
form={form} form={form}
readOnly={isDealsViewer}
/> />
)} )}
</Flex> </Flex>
</Fieldset> </Fieldset>
</Stack> </Stack>
</ScrollArea> </ScrollArea>
{!isDealsViewer && (
<Flex <Flex
mt={"md"} mt={"md"}
gap={rem(10)} gap={rem(10)}
@@ -258,6 +268,7 @@ const Content: FC<Props> = ({ card }) => {
</Button> </Button>
</Group> </Group>
</Flex> </Flex>
)}
</Flex> </Flex>
</form> </form>
); );

View File

@@ -10,6 +10,8 @@ import { useDisclosure } from "@mantine/hooks";
import InlineButton from "../../../components/InlineButton/InlineButton.tsx"; import InlineButton from "../../../components/InlineButton/InlineButton.tsx";
import { IconFilter } from "@tabler/icons-react"; import { IconFilter } from "@tabler/icons-react";
import BoardSelect from "../../../components/BoardSelect/BoardSelect.tsx"; import BoardSelect from "../../../components/BoardSelect/BoardSelect.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../redux/store.ts";
type Props = { type Props = {
form: UseFormReturnType<CardsPageState>; form: UseFormReturnType<CardsPageState>;
@@ -18,6 +20,7 @@ type Props = {
const CardsTableFiltersModal = ({ form, projects }: Props) => { const CardsTableFiltersModal = ({ form, projects }: Props) => {
const [opened, { open, close }] = useDisclosure(); const [opened, { open, close }] = useDisclosure();
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
return ( return (
<> <>
@@ -60,6 +63,7 @@ const CardsTableFiltersModal = ({ form, projects }: Props) => {
placeholder={"Выберите маркетплейс"} placeholder={"Выберите маркетплейс"}
{...form.getInputProps("marketplace")} {...form.getInputProps("marketplace")}
/> />
{!isDealsViewer && (
<ClientSelectNew <ClientSelectNew
onClear={() => onClear={() =>
form.setFieldValue("client", null) form.setFieldValue("client", null)
@@ -69,6 +73,7 @@ const CardsTableFiltersModal = ({ form, projects }: Props) => {
placeholder={"Выберите клиента"} placeholder={"Выберите клиента"}
{...form.getInputProps("client")} {...form.getInputProps("client")}
/> />
)}
</Flex> </Flex>
</Modal> </Modal>
</> </>

View File

@@ -1,5 +1,4 @@
import { FC } from "react"; import { FC } from "react";
import ClientsTable from "./components/ClientsTable/ClientsTable.tsx";
import useClientsList from "./hooks/useClientsList.tsx"; import useClientsList from "./hooks/useClientsList.tsx";
import PageBlock from "../../components/PageBlock/PageBlock.tsx"; import PageBlock from "../../components/PageBlock/PageBlock.tsx";
import styles from "./ClientsPage.module.css"; import styles from "./ClientsPage.module.css";
@@ -7,9 +6,10 @@ import { Button, TextInput } from "@mantine/core";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { ClientSchema, ClientService } from "../../client"; import { ClientSchema, ClientService } from "../../client";
import { notifications } from "../../shared/lib/notifications.ts"; 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 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 ClientsPage: FC = () => {
const { clients, refetch } = useClientsList(); const { clients, refetch } = useClientsList();

View File

@@ -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;

View File

@@ -8,6 +8,7 @@ import { IconEdit, IconTrash } from "@tabler/icons-react";
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx"; import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import ClientChatButton from "../ClientChatButton/ClientChatButton.tsx"; import ClientChatButton from "../ClientChatButton/ClientChatButton.tsx";
import ClientLinkActionIcon from "../ClientLinkActionIcon/ClientLinkActionIcon.tsx";
type RefetchProps = { type RefetchProps = {
refetch: () => void; refetch: () => void;
@@ -46,7 +47,8 @@ const ClientsTable: FC<CRUDTableProps<ClientSchema> & RefetchProps> = ({
enableRowActions: true, enableRowActions: true,
renderRowActions: ({ row }) => ( renderRowActions: ({ row }) => (
<Flex gap="md"> <Flex gap="md">
<ClientChatButton client={row.original} refetch={refetch}/> <ClientChatButton client={row.original} refetch={refetch} />
<ClientLinkActionIcon client={row.original} />
<Tooltip label="Редактировать"> <Tooltip label="Редактировать">
<ActionIcon <ActionIcon
onClick={() => onClick={() =>

View File

@@ -8,11 +8,16 @@ type Props = {
const useClientsFilter = ({ clients }: Props) => { const useClientsFilter = ({ clients }: Props) => {
const [search, setSearch] = useState<string>(""); const [search, setSearch] = useState<string>("");
const debouncedSearch = useDebouncedValue(search, 400); const [debouncedSearch] = useDebouncedValue(search, 400);
const [filteredClients, setFilteredClients] = useState<ClientSchema[]>([]); const [filteredClients, setFilteredClients] = useState<ClientSchema[]>([]);
const filterClients = () => { const filterClients = () => {
const loweredSearch = search.toLowerCase(); if (debouncedSearch.length === 0) {
setFilteredClients(clients);
return;
}
const loweredSearch = debouncedSearch.toLowerCase();
const filtered = clients.filter( const filtered = clients.filter(
client => ( client => (
client.name.toLowerCase().includes(loweredSearch) || client.name.toLowerCase().includes(loweredSearch) ||
@@ -28,7 +33,7 @@ const useClientsFilter = ({ clients }: Props) => {
useEffect(() => { useEffect(() => {
filterClients(); filterClients();
}, [debouncedSearch]); }, [debouncedSearch, clients]);
return { return {
search, search,