feat: deal create, update, delete

This commit is contained in:
2025-08-24 12:49:19 +04:00
parent 10f50ac254
commit d5be9ce61a
23 changed files with 741 additions and 76 deletions

View File

@ -0,0 +1,10 @@
.create-button {
cursor: pointer;
@mixin light {
background-color: var(--color-light-white-blue);
}
@mixin dark {
background-color: var(--mantine-color-dark-7);
}
}

View File

@ -0,0 +1,51 @@
import { useState } from "react";
import { IconPlus } from "@tabler/icons-react";
import { Card, Center, Group, Text, Transition } from "@mantine/core";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import CreateCardForm, { CreateDealForm } from "./components/CreateCardForm";
import styles from "./CreateDealButton.module.css";
const CreateCardButton = () => {
const [isCreating, setIsCreating] = useState(false);
const [isTransitionEnded, setIsTransitionEnded] = useState(true);
const { dealsCrud } = useDealsContext();
const onSubmit = (values: CreateDealForm) => {
dealsCrud.onCreate(values.name);
setIsCreating(prevState => !prevState);
setIsTransitionEnded(false);
};
return (
<Card
className={styles["create-button"]}
onClick={() => {
if (isCreating) return;
setIsCreating(prevState => !prevState);
setIsTransitionEnded(false);
}}>
{!isCreating && isTransitionEnded && (
<Center>
<Group gap={"xs"}>
<IconPlus />
<Text>Добавить</Text>
</Group>
</Center>
)}
<Transition
mounted={isCreating}
transition={"scale-y"}
onExited={() => setIsTransitionEnded(true)}>
{styles => (
<div style={styles}>
<CreateCardForm
onCancel={() => setIsCreating(false)}
onSubmit={onSubmit}
/>
</div>
)}
</Transition>
</Card>
);
};
export default CreateCardButton;

View File

@ -0,0 +1,54 @@
import { FC } from "react";
import { IconCheck, IconX } from "@tabler/icons-react";
import { Button, Group, Stack, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
export type CreateDealForm = {
name: string;
};
type Props = {
onSubmit: (values: CreateDealForm) => void;
onCancel: () => void;
};
const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => {
const form = useForm<CreateDealForm>({
initialValues: {
name: "",
},
validate: {
name: value => !value && "Введите название",
},
});
return (
<form onSubmit={form.onSubmit(values => {
onSubmit(values);
form.reset();
})}>
<Stack>
<TextInput
placeholder={"Название"}
{...form.getInputProps("name")}
/>
<Group wrap={"nowrap"}>
<Button
variant={"default"}
w={"100%"}
onClick={onCancel}>
<IconX />
</Button>
<Button
variant={"default"}
w={"100%"}
type={"submit"}>
<IconCheck />
</Button>
</Group>
</Stack>
</form>
);
};
export default CreateCardForm;

View File

@ -1,4 +1,6 @@
import { Card, Group, Pill, Stack, Text } from "@mantine/core";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useDrawersContext } from "@/drawers/DrawersContext";
import { DealSchema } from "@/lib/client";
import styles from "./DealCard.module.css";
@ -7,8 +9,17 @@ type Props = {
};
const DealCard = ({ deal }: Props) => {
const { dealsCrud } = useDealsContext();
const { openDrawer } = useDrawersContext();
const onClick = () => {
openDrawer({ key: "dealEditorDrawer", props: { deal, dealsCrud } });
};
return (
<Card className={styles.container}>
<Card
onClick={onClick}
className={styles.container}>
<Text c={"dodgerblue"}>{deal.name}</Text>
<Stack gap={0}>
<Text>Wb электросталь</Text>

View File

@ -45,11 +45,13 @@ const Funnel: FC = () => {
renderContainer={(
status: StatusSchema,
funnelColumnComponent: ReactNode,
renderDraggable
renderDraggable,
index
) => (
<StatusColumnWrapper
status={status}
renderHeader={renderDraggable}>
renderHeader={renderDraggable}
createFormEnabled={index === 0}>
{funnelColumnComponent}
</StatusColumnWrapper>
)}

View File

@ -1,5 +1,6 @@
import React, { ReactNode } from "react";
import { Box, ScrollArea, Stack } from "@mantine/core";
import CreateCardButton from "@/app/deals/components/shared/CreateDealButton/CreateDealButton";
import { StatusSchema } from "@/lib/client";
import styles from "./StatusColumnWrapper.module.css";
@ -7,9 +8,14 @@ type Props = {
status: StatusSchema;
renderHeader: () => ReactNode;
children: ReactNode;
createFormEnabled?: boolean;
};
const StatusColumnWrapper = ({ renderHeader, children }: Props) => {
const StatusColumnWrapper = ({
renderHeader,
children,
createFormEnabled = false,
}: Props) => {
return (
<Box className={styles.container}>
<Stack
@ -22,7 +28,12 @@ const StatusColumnWrapper = ({ renderHeader, children }: Props) => {
scrollbarSize={10}
type={"always"}
scrollbars={"y"}>
<Stack mah={"calc(100vh - 220px)"}>{children}</Stack>
<Stack
gap={"xs"}
mah={"calc(100vh - 220px)"}>
{createFormEnabled && <CreateCardButton />}
{children}
</Stack>
</ScrollArea>
</Stack>
</Box>

View File

@ -13,8 +13,6 @@ type BoardsContextState = {
setSelectedBoardId: React.Dispatch<React.SetStateAction<number | null>>;
refetchBoards: () => void;
boardsCrud: BoardsCrud;
isEditorDrawerOpened: boolean;
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
};
const BoardsContext = createContext<BoardsContextState | undefined>(undefined);
@ -26,8 +24,6 @@ const useBoardsContextState = () => {
setBoards,
refetch: refetchBoards,
} = useBoardsList({ projectId: project?.id });
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
useState<boolean>(false);
const [selectedBoardId, setSelectedBoardId] = useState<number | null>(null);
const selectedBoard =
@ -51,8 +47,6 @@ const useBoardsContextState = () => {
setSelectedBoardId,
refetchBoards,
boardsCrud,
isEditorDrawerOpened,
setIsEditorDrawerOpened,
};
};

View File

@ -1,57 +1,43 @@
"use client";
import React, { createContext, FC, useContext } from "react";
import { useMutation, UseMutationResult } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import { DealsCrud, useDealsCrud } from "@/hooks/cruds/useDealsCrud";
import useDealsList from "@/hooks/lists/useDealsList";
import {
DealSchema,
HttpValidationError,
Options,
UpdateDealData,
UpdateDealResponse,
} from "@/lib/client";
import { updateDealMutation } from "@/lib/client/@tanstack/react-query.gen";
import { notifications } from "@/lib/notifications";
import { DealSchema } from "@/lib/client";
type DealsContextState = {
deals: DealSchema[];
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
updateDeal: UseMutationResult<
UpdateDealResponse,
AxiosError<HttpValidationError>,
Options<UpdateDealData>
>;
refetchDeals: () => void;
dealsCrud: DealsCrud;
};
const DealsContext = createContext<DealsContextState | undefined>(undefined);
const useDealsContextState = () => {
const { selectedBoard } = useBoardsContext();
const { statuses } = useStatusesContext();
const {
deals,
setDeals,
refetch: refetchDeals,
} = useDealsList({ boardId: selectedBoard?.id });
const updateDeal = useMutation({
...updateDealMutation(),
onError: error => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
refetchDeals();
},
const dealsCrud = useDealsCrud({
deals,
setDeals,
refetchDeals,
boardId: selectedBoard?.id,
statuses,
});
return {
deals,
setDeals,
updateDeal,
refetchDeals,
dealsCrud,
};
};

View File

@ -0,0 +1,4 @@
.tab {
border-bottom-width: 3px;
}

View File

@ -0,0 +1,48 @@
"use client";
import React, { FC } from "react";
import { Drawer } from "@mantine/core";
import DealEditorBody from "@/app/deals/drawers/DealEditorDrawer/components/DealEditorBody";
import { DrawerProps } from "@/drawers/types";
import { DealsCrud } from "@/hooks/cruds/useDealsCrud";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { DealSchema } from "@/lib/client";
type Props = {
deal: DealSchema;
dealsCrud: DealsCrud;
};
const DealEditorDrawer: FC<DrawerProps<Props>> = ({
opened,
onClose,
props,
}) => {
const isMobile = useIsMobile();
return (
<Drawer
size={isMobile ? "100%" : "60%"}
position={"right"}
onClose={onClose}
removeScrollProps={{ allowPinchZoom: true }}
withCloseButton={false}
opened={opened}
trapFocus={false}
styles={{
body: {
height: "100%",
display: "flex",
flexDirection: "column",
padding: 0,
},
}}>
<DealEditorBody
{...props}
onClose={onClose}
/>
</Drawer>
);
};
export default DealEditorDrawer;

View File

@ -0,0 +1,47 @@
import { FC } from "react";
import { IconCircleDotted, IconEdit } from "@tabler/icons-react";
import { Tabs, Text } from "@mantine/core";
import GeneralTab from "@/app/deals/drawers/DealEditorDrawer/components/GeneralTab";
import { DealsCrud } from "@/hooks/cruds/useDealsCrud";
import { DealSchema } from "@/lib/client";
import styles from "../DealEditorDrawer.module.css";
type Props = {
dealsCrud: DealsCrud;
deal: DealSchema;
onClose: () => void;
};
const DealEditorBody: FC<Props> = ({ dealsCrud, deal, onClose }) => {
return (
<Tabs
defaultValue="general"
classNames={{ tab: styles.tab }}>
<Tabs.List>
<Tabs.Tab
value="general"
leftSection={<IconEdit />}>
Общая информация
</Tabs.Tab>
<Tabs.Tab
value="mock"
leftSection={<IconCircleDotted />}>
Mock
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="general">
<GeneralTab
dealsCrud={dealsCrud}
deal={deal}
onClose={onClose}
/>
</Tabs.Panel>
<Tabs.Panel value="mock">
<Text>mock</Text>
</Tabs.Panel>
</Tabs>
);
};
export default DealEditorBody;

View File

@ -0,0 +1,67 @@
import { FC, useState } from "react";
import { isEqual } from "lodash";
import { Button, Group, Stack, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { DealsCrud } from "@/hooks/cruds/useDealsCrud";
import { DealSchema } from "@/lib/client";
type Props = {
dealsCrud: DealsCrud;
deal: DealSchema;
onClose: () => void;
};
const GeneralTab: FC<Props> = ({ deal, dealsCrud, onClose }) => {
const [initialValues, setInitialValues] = useState(deal);
const form = useForm<DealSchema>({
initialValues,
validate: {
name: value => !value && "Введите название",
},
});
const onSubmit = (values: DealSchema) => {
dealsCrud.onUpdate(deal.id, values);
setInitialValues(values);
};
const onDelete = () => {
dealsCrud.onDelete(deal, onClose);
};
return (
<form onSubmit={form.onSubmit(onSubmit)}>
<Stack p={"md"}>
<TextInput
label={"Название"}
{...form.getInputProps("name")}
/>
<Group justify={"space-between"}>
<Group>
<Button
type={"submit"}
disabled={isEqual(form.values, initialValues)}
variant={"filled"}>
Сохранить
</Button>
<Button
type={"reset"}
onClick={() => form.reset()}
disabled={isEqual(form.values, initialValues)}
variant={"default"}>
Отменить
</Button>
</Group>
<Button
onClick={onDelete}
color={"red"}
variant={"outline"}>
Удалить
</Button>
</Group>
</Stack>
</form>
);
};
export default GeneralTab;

View File

@ -0,0 +1,3 @@
import ProjectsEditorDrawer from "@/app/deals/drawers/ProjectsEditorDrawer/ProjectsEditorDrawer";
export default ProjectsEditorDrawer;

View File

@ -25,7 +25,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
const { statuses, setStatuses, statusesCrud } = useStatusesContext();
const { deals, setDeals, updateDeal } = useDealsContext();
const { deals, setDeals, dealsCrud } = useDealsContext();
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
const isMobile = useIsMobile();
@ -253,17 +253,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
statusId: number,
lexorank?: string
) => {
updateDeal.mutate({
path: {
dealId,
},
body: {
deal: {
statusId,
lexorank,
},
},
});
dealsCrud.onUpdate(dealId, { statusId, lexorank, name: null });
};
const handleDragStart = ({ active }: DragStartEvent) => {