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