feat: board and status selects in deal editor

This commit is contained in:
2025-09-02 14:41:28 +04:00
parent a6d8948e9d
commit 72ed69db24
13 changed files with 192 additions and 121 deletions

View File

@ -3,21 +3,25 @@ import { IconEdit } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable";
import { ActionIcon, Tooltip } from "@mantine/core";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import { useDrawersContext } from "@/drawers/DrawersContext";
import { DealSchema } from "@/lib/client";
import { utcDateTimeToLocalString } from "@/utils/datetime";
const useDealsTableColumns = () => {
const { selectedProject } = useProjectsContext();
const { dealsCrud } = useDealsContext();
const { openDrawer } = useDrawersContext();
const onEditDeal = useCallback(
(deal: DealSchema) => {
if (!selectedProject) return;
openDrawer({
key: "dealEditorDrawer",
props: {
deal,
dealsCrud,
project: selectedProject,
},
});
},

View File

@ -1,5 +1,6 @@
import { Box, Card, Group, Pill, Stack, Text } from "@mantine/core";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import { useDrawersContext } from "@/drawers/DrawersContext";
import { DealSchema } from "@/lib/client";
import styles from "./DealCard.module.css";
@ -9,11 +10,16 @@ type Props = {
};
const DealCard = ({ deal }: Props) => {
const { selectedProject } = useProjectsContext();
const { dealsCrud } = useDealsContext();
const { openDrawer } = useDrawersContext();
const onClick = () => {
openDrawer({ key: "dealEditorDrawer", props: { deal, dealsCrud } });
if (!selectedProject) return;
openDrawer({
key: "dealEditorDrawer",
props: { project: selectedProject, deal, dealsCrud },
});
};
return (
@ -22,7 +28,9 @@ const DealCard = ({ deal }: Props) => {
className={styles.container}>
<Group
justify={"space-between"}
ml={"xs"}
wrap={"nowrap"}
pl={"xs"}
gap={"xs"}
align={"start"}>
<Text
c={"dodgerblue"}
@ -30,7 +38,7 @@ const DealCard = ({ deal }: Props) => {
{deal.name}
</Text>
<Box className={styles["deal-id"]}>
<Text>ID: {deal.id}</Text>
<Text style={{ textWrap: "nowrap" }}>ID: {deal.id}</Text>
</Box>
</Group>
<Stack className={styles["deal-data"]}>

View File

@ -39,7 +39,7 @@ const Funnel: FC = () => {
getContainerId={(status: StatusSchema) => `${status.id}-status`}
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
sortByLexorank(
items.filter(deal => deal.statusId === status.id)
items.filter(deal => deal.status.id === status.id)
)
}
renderContainer={(

View File

@ -6,9 +6,10 @@ import DealEditorBody from "@/app/deals/drawers/DealEditorDrawer/components/Deal
import { DrawerProps } from "@/drawers/types";
import { DealsCrud } from "@/hooks/cruds/useDealsCrud";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { DealSchema } from "@/lib/client";
import { DealSchema, ProjectSchema } from "@/lib/client";
type Props = {
project: ProjectSchema;
deal: DealSchema;
dealsCrud: DealsCrud;
};

View File

@ -1,18 +1,19 @@
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 GeneralTab from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/GeneralTab";
import { DealsCrud } from "@/hooks/cruds/useDealsCrud";
import { DealSchema } from "@/lib/client";
import { DealSchema, ProjectSchema } from "@/lib/client";
import styles from "../DealEditorDrawer.module.css";
type Props = {
project: ProjectSchema;
dealsCrud: DealsCrud;
deal: DealSchema;
onClose: () => void;
};
const DealEditorBody: FC<Props> = ({ dealsCrud, deal, onClose }) => {
const DealEditorBody: FC<Props> = ({ project, dealsCrud, deal, onClose }) => {
return (
<Tabs
defaultValue="general"
@ -32,6 +33,7 @@ const DealEditorBody: FC<Props> = ({ dealsCrud, deal, onClose }) => {
<Tabs.Panel value="general">
<GeneralTab
project={project}
dealsCrud={dealsCrud}
deal={deal}
onClose={onClose}

View File

@ -1,71 +0,0 @@
import { FC, useState } from "react";
import { isEqual } from "lodash";
import { Button, Group, Stack, Text, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { DealsCrud } from "@/hooks/cruds/useDealsCrud";
import { DealSchema } from "@/lib/client";
import { utcDateTimeToLocalString } from "@/utils/datetime";
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")}
/>
<Text>Создано: {utcDateTimeToLocalString(deal.createdAt)}</Text>
<Group
justify={"space-between"}
wrap={"nowrap"}>
<Group wrap={"nowrap"}>
<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,43 @@
import { FC } from "react";
import { isEqual } from "lodash";
import { Button, Group } from "@mantine/core";
import { UseFormReturnType } from "@mantine/form";
import { DealSchema } from "@/lib/client";
type Props = {
form: UseFormReturnType<Partial<DealSchema>>;
initialValues: Partial<DealSchema>;
onDelete: () => void;
};
const Footer: FC<Props> = ({ form, initialValues, onDelete }) => {
return (
<Group
justify={"space-between"}
wrap={"nowrap"}>
<Group wrap={"nowrap"}>
<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>
);
};
export default Footer;

View File

@ -0,0 +1,71 @@
import { FC, useState } from "react";
import { Stack, Text, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import Footer from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/Footer";
import BoardSelect from "@/components/selects/BoardSelect/BoardSelect";
import StatusSelect from "@/components/selects/StatusSelect/StatusSelect";
import { DealsCrud } from "@/hooks/cruds/useDealsCrud";
import { DealSchema, ProjectSchema } from "@/lib/client";
import { utcDateTimeToLocalString } from "@/utils/datetime";
type Props = {
project: ProjectSchema;
dealsCrud: DealsCrud;
deal: DealSchema;
onClose: () => void;
};
const GeneralTab: FC<Props> = ({ project, deal, dealsCrud, onClose }) => {
const [initialValues, setInitialValues] =
useState<Partial<DealSchema>>(deal);
const form = useForm<Partial<DealSchema>>({
initialValues,
validate: {
name: value => !value && "Введите название",
board: value => !value && "Выберите доску",
status: value => !value && "Выберите статус",
},
});
const onSubmit = (values: Partial<DealSchema>) => {
dealsCrud.onUpdate(deal.id, {
...values,
boardId: values.board?.id,
statusId: values.status?.id,
});
setInitialValues(values);
};
const onDelete = () => {
dealsCrud.onDelete(deal, onClose);
};
return (
<form onSubmit={form.onSubmit(onSubmit)}>
<Stack p={"md"}>
<TextInput
label={"Название"}
{...form.getInputProps("name")}
/>
<Text>Создано: {utcDateTimeToLocalString(deal.createdAt)}</Text>
<BoardSelect
label={"Доска"}
{...form.getInputProps("board")}
projectId={project?.id}
/>
<StatusSelect
label={"Статус"}
{...form.getInputProps("status")}
boardId={form.values.board?.id}
/>
<Footer
form={form}
initialValues={initialValues}
onDelete={onDelete}
/>
</Stack>
</form>
);
};
export default GeneralTab;

View File

@ -41,7 +41,11 @@ const useDealsAndStatusesDnd = (): ReturnType => {
const getStatusByDealId = (dealId: number) => {
const deal = deals.find(deal => deal.id === dealId);
if (!deal) return;
return statuses.find(status => status.id === deal.statusId);
return statuses.find(status => status.id === deal.status.id);
};
const getStatusById = (statusId: number) => {
return statuses.find(status => status.id === statusId);
};
const swipeSliderDuringDrag = (activeId: number, over: Over) => {
@ -105,19 +109,19 @@ const useDealsAndStatusesDnd = (): ReturnType => {
const activeStatusId = getStatusByDealId(activeDealId)?.id;
if (!activeStatusId) return;
const { overStatusId, newLexorank } = getDropTarget(
const { overStatus, newLexorank } = getDropTarget(
over.id,
activeDealId,
activeStatusId
);
if (!overStatusId) return;
if (!overStatus) return;
debouncedSetDeals(
deals.map(deal =>
deal.id === activeDealId
? {
...deal,
statusId: overStatusId,
status: overStatus,
lexorank: newLexorank || deal.lexorank,
}
: deal
@ -134,7 +138,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
} else {
const deal = deals.find(deal => deal.id === over.id);
if (!deal) return;
overStatusId = deal.statusId;
overStatusId = deal.status.id;
}
if (!overStatusId || activeStatusId === overStatusId) return;
@ -156,42 +160,42 @@ const useDealsAndStatusesDnd = (): ReturnType => {
activeDealId: number,
activeStatusId: number,
isOnDragEnd: boolean = false
) => {
): { overStatus?: StatusSchema; newLexorank?: string } => {
if (typeof overId === "string") {
return {
overStatusId: getStatusId(overId),
overStatus: getStatusById(getStatusId(overId)),
newLexorank: undefined,
};
}
const overDealId = Number(overId);
const overStatusId = getStatusByDealId(overDealId)?.id;
const overStatus = getStatusByDealId(overDealId);
if (!overStatusId || (!isOnDragEnd && activeDealId === overDealId)) {
return { overStatusId: undefined, newLexorank: undefined };
if (!overStatus || (!isOnDragEnd && activeDealId === overDealId)) {
return { overStatus: undefined, newLexorank: undefined };
}
const statusDeals = sortByLexorank(
deals.filter(deal => deal.statusId === overStatusId)
deals.filter(deal => deal.status.id === overStatus.id)
);
const overDealIndex = statusDeals.findIndex(
deal => deal.id === overDealId
);
if (activeStatusId === overStatusId) {
if (activeStatusId === overStatus.id) {
const newLexorank = getNewRankForSameStatus(
statusDeals,
overDealIndex,
activeDealId
);
return { overStatusId, newLexorank };
return { overStatus, newLexorank };
}
const newLexorank = getNewRankForAnotherStatus(
statusDeals,
overDealIndex
);
return { overStatusId, newLexorank };
return { overStatus, newLexorank };
};
const handleDragEnd = ({ active, over }: DragOverEvent) => {
@ -215,9 +219,9 @@ const useDealsAndStatusesDnd = (): ReturnType => {
if (typeof over.id === "string" && isStatusId(over.id)) {
overStatusId = getStatusId(over.id);
} else {
const deal = deals.find(deal => deal.statusId === over.id);
const deal = deals.find(deal => deal.status.id === over.id);
if (!deal) return;
overStatusId = deal.statusId;
overStatusId = deal.status.id;
}
if (!overStatusId) return;
@ -237,15 +241,15 @@ const useDealsAndStatusesDnd = (): ReturnType => {
const activeStatusId = getStatusByDealId(activeDealId)?.id;
if (!activeStatusId) return;
const { overStatusId, newLexorank } = getDropTarget(
const { overStatus, newLexorank } = getDropTarget(
over.id,
activeDealId,
activeStatusId,
true
);
if (!overStatusId) return;
if (!overStatus) return;
onDealDragEnd(activeDealId, overStatusId, newLexorank);
onDealDragEnd(activeDealId, overStatus.id, newLexorank);
};
const onDealDragEnd = (