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 { 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`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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"}>
|
||||||
|
|||||||
@@ -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} />
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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={"Добавление сотрудника к услуге"}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 />,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 { 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={() =>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user