feat: added tags for cards, aligned status headers

This commit is contained in:
2025-03-09 19:32:45 +04:00
parent 56135ae10c
commit ea80e92c18
48 changed files with 876 additions and 85 deletions

View File

@@ -0,0 +1,18 @@
import { TagsInput, TagsInputProps } from "@mantine/core";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
type Props = Omit<TagsInputProps, "data">
const CardTagsInput = (props: Props) => {
const { selectedProject } = useProjectsContext();
return (
<TagsInput
{...props}
data={selectedProject?.tags.map(tag => tag.name)}
label={"Теги"}
/>
);
};
export default CardTagsInput;

View File

@@ -42,7 +42,7 @@ const CardEditDrawer: FC = () => {
const getTabPanel = (value: string, component: ReactNode) => {
return (
<Tabs.Panel value={value}>
<Tabs.Panel key={value} value={value}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -61,6 +61,7 @@ const CardEditDrawer: FC = () => {
const getTabs = () => {
const moduleTabs = modules.map(module => (
<Tabs.Tab
key={module.info.key}
value={module.info.key}
leftSection={module.info.icon}
>

View File

@@ -22,9 +22,10 @@ import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyCon
import { useClipboard } from "@mantine/hooks";
import ProjectSelect from "../../../../../../components/ProjectSelect/ProjectSelect.tsx";
import BoardSelect from "../../../../../../components/BoardSelect/BoardSelect.tsx";
import CardStatusSelect from "../../../../../../components/DealStatusSelect/CardStatusSelect.tsx";
import CardStatusSelect from "../../../../../../components/CardStatusSelect/CardStatusSelect.tsx";
import CardAttributeFields from "../../../../../../components/CardAttributeFields/CardAttributeFields.tsx";
import getAttributesFromCard from "../../../../../../components/CardAttributeFields/utils/getAttributesFromCard.ts";
import CardTagsInput from "../../../../components/CardTagsInput/CardTagsInput.tsx";
type Props = {
card: CardSchema;
@@ -41,6 +42,7 @@ const Content: FC<Props> = ({ card }) => {
const clipboard = useClipboard();
const queryClient = useQueryClient();
const [project, setProject] = useState<ProjectSchema | null>(card.board.project);
const [cardTags, setCardTags] = useState<string[]>(card.tags?.map(tag => tag.name) ?? []);
const getInitialValues = (card: CardSchema): CardGeneralFormType => {
return {
@@ -82,6 +84,7 @@ const Content: FC<Props> = ({ card }) => {
boardId: values.board.id,
clientId: values.client?.id ?? null,
attributes,
tags: cardTags,
},
},
}).then(({ ok, message }) => {
@@ -125,6 +128,20 @@ const Content: FC<Props> = ({ card }) => {
});
};
const cancelChanges = () => {
form.reset();
setCardTags(card.tags?.map(tag => tag.name) ?? []);
};
const isEqualValues = () => {
const initialCardTagsSet = new Set(card.tags?.map(tag => tag.name) ?? []);
const tagsEqual = initialCardTagsSet.size === cardTags.length &&
cardTags.every(element => initialCardTagsSet.has(element));
return isEqual(initialValues, form.values) && tagsEqual;
};
return (
<form onSubmit={form.onSubmit(values => handleSubmit(values))}>
<Flex
@@ -177,6 +194,12 @@ const Content: FC<Props> = ({ card }) => {
placeholder={"Введите коментарий"}
{...form.getInputProps("comment")}
/>
{project && project?.tags.length > 0 && (
<CardTagsInput
value={cardTags}
onChange={setCardTags}
/>
)}
{project && (
<CardAttributeFields
project={project}
@@ -223,14 +246,14 @@ const Content: FC<Props> = ({ card }) => {
<Button
color={"red"}
type={"reset"}
disabled={isEqual(initialValues, form.values)}
onClick={() => form.reset()}>
disabled={isEqualValues()}
onClick={cancelChanges}>
Отменить изменения
</Button>
<Button
variant={"default"}
type={"submit"}
disabled={isEqual(initialValues, form.values)}>
disabled={isEqualValues()}>
Сохранить изменения
</Button>
</Group>

View File

@@ -1,11 +1,12 @@
import { Box, Drawer, rem, Tabs } from "@mantine/core";
import { IconHexagons, IconSettings, IconSubtask } from "@tabler/icons-react";
import { IconHexagons, IconSettings, IconSubtask, IconTags } from "@tabler/icons-react";
import { ReactNode } from "react";
import { motion } from "framer-motion";
import { useProjectsEditorContext } from "../../contexts/ProjectsEditorContext.tsx";
import General from "./tabs/General/General.tsx";
import Attributes from "./tabs/Attributes/Attributes.tsx";
import Modules from "./tabs/Modules/Modules.tsx";
import Tags from "./tabs/Tags/Tags.tsx";
const ProjectEditDrawer = () => {
@@ -67,11 +68,17 @@ const ProjectEditDrawer = () => {
leftSection={<IconSubtask />}>
Атрибуты
</Tabs.Tab>
<Tabs.Tab
value={"tags"}
leftSection={<IconTags />}>
Теги
</Tabs.Tab>
</Tabs.List>
{getTabPanel("general", <General/>)}
{getTabPanel("attributes", <Attributes/>)}
{getTabPanel("general", <General />)}
{getTabPanel("attributes", <Attributes />)}
{getTabPanel("modules", <Modules />)}
{getTabPanel("tags", <Tags />)}
</Tabs>
</Drawer>
);

View File

@@ -0,0 +1,66 @@
import { ActionIcon, Flex, Group, rem, Stack, Tooltip } from "@mantine/core";
import { BaseTable } from "../../../../../../components/BaseTable/BaseTable.tsx";
import tagsTableColumns from "./hooks/tagsTableColumns.tsx";
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
import { CardTagSchema } from "../../../../../../client";
import { MRT_TableOptions } from "mantine-react-table";
import useTags from "./hooks/useTags.tsx";
import InlineButton from "../../../../../../components/InlineButton/InlineButton.tsx";
const Tags = () => {
const columns = tagsTableColumns();
const {
project,
onDeleteClick,
onChangeClick,
onCreateClick,
} = useTags();
return (
<Stack gap={rem(10)}>
<Group>
<InlineButton onClick={onCreateClick}>
<IconPlus />
Создать
</InlineButton>
</Group>
<BaseTable
data={project?.tags}
columns={columns}
restProps={
{
enableSorting: false,
enableColumnActions: false,
enableRowActions: true,
renderRowActions: ({ row }) => {
return (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() => onDeleteClick(row.original)}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onChangeClick(row.original)}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
</Flex>
);
},
} as MRT_TableOptions<CardTagSchema>
}
/>
</Stack>
);
};
export default Tags;

View File

@@ -0,0 +1,18 @@
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { CardTagSchema } from "../../../../../../../client";
const useTagsTableColumns = () => {
return useMemo<MRT_ColumnDef<CardTagSchema>[]>(
() => [
{
header: "Название",
accessorKey: "name",
size: 1000,
},
],
[],
);
};
export default useTagsTableColumns;

View File

@@ -0,0 +1,109 @@
import {
CancelablePromise,
CardTagSchema,
CardTagService,
CreateTagResponse,
DeleteTagResponse,
UpdateTagResponse,
} from "../../../../../../../client";
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
import { modals } from "@mantine/modals";
import { Text } from "@mantine/core";
import { useProjectsContext } from "../../../../../../../contexts/ProjectsContext.tsx";
import { useCardPageContext } from "../../../../../contexts/CardPageContext.tsx";
const useTags = () => {
const { selectedProject: project, refetchProjects } = useProjectsContext();
const { refetchCards } = useCardPageContext();
const processResponse = (
response: CancelablePromise<DeleteTagResponse | UpdateTagResponse | CreateTagResponse>,
) => {
response
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
refetchProjects();
refetchCards();
})
.catch(err => console.log(err));
};
const onDelete = (tag: CardTagSchema) => {
const response = CardTagService.deleteTag({
cardTagId: tag.id,
});
processResponse(response);
};
const onDeleteClick = (tag: CardTagSchema) => {
modals.openConfirmModal({
title: "Удаление тега",
children: (
<Text size="sm">
Вы уверены что хотите удалить тег {tag.name}
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onDelete(tag),
});
};
const onChange = (tag: CardTagSchema) => {
const response = CardTagService.updateTag({
requestBody: { tag },
});
processResponse(response);
};
const onChangeClick = (tag: CardTagSchema) => {
modals.openContextModal({
modal: "cardTagModal",
innerProps: {
element: tag,
onChange,
},
withCloseButton: false,
});
};
const onCreate = (tag: CardTagSchema) => {
if (!project) return;
const response = CardTagService.createTag({
requestBody: {
tag: {
name: tag.name,
projectId: project.id,
}
},
});
processResponse(response);
};
const onCreateClick = () => {
modals.openContextModal({
modal: "cardTagModal",
innerProps: {
onCreate,
},
withCloseButton: false,
});
};
return {
project,
onDeleteClick,
onChangeClick,
onCreateClick,
};
};
export default useTags;

View File

@@ -0,0 +1,47 @@
import { ContextModalProps } from "@mantine/modals";
import BaseFormModal, {
CreateEditFormProps,
} from "../../../../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import { CardTagSchema } from "../../../../../../../client";
import { useForm } from "@mantine/form";
import { TextInput } from "@mantine/core";
type Props = CreateEditFormProps<CardTagSchema>;
const CardTagModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const isEditing = "element" in innerProps;
const initialValue: Partial<CardTagSchema> = isEditing
? innerProps.element
: {
name: "",
};
const form = useForm<Partial<CardTagSchema>>({
initialValues: initialValue,
validate: {
name: name => !name && "Необходимо указать название тега",
},
});
return (
<BaseFormModal
form={form}
closeOnSubmit
onClose={() => context.closeContextModal(id)}
{...innerProps}>
<BaseFormModal.Body>
<TextInput
label={"Название"}
placeholder={"Введите название тега"}
{...form.getInputProps("name")}
/>
</BaseFormModal.Body>
</BaseFormModal>
);
};
export default CardTagModal;

View File

@@ -3,7 +3,7 @@ import { Flex, Modal, NumberInput, rem } from "@mantine/core";
import { UseFormReturnType } from "@mantine/form";
import { CardsPageState } from "../hooks/useCardsPageState.tsx";
import ObjectSelect from "../../../components/ObjectSelect/ObjectSelect.tsx";
import CardStatusSelect from "../../../components/DealStatusSelect/CardStatusSelect.tsx";
import CardStatusSelect from "../../../components/CardStatusSelect/CardStatusSelect.tsx";
import BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import { useDisclosure } from "@mantine/hooks";

View File

@@ -21,6 +21,7 @@
align-items: stretch;
text-align: center;
flex-direction: column;
height: 100%;
}
.delete {

View File

@@ -6,13 +6,15 @@ import useAllTransactionTagsList from "../../../AdminPage/hooks/useAllTransactio
type IsIncome = {
isIncome: boolean;
}
type Props = Omit<
ObjectSelectProps<TransactionTagSchema | null>,
"data" | "getValueFn" | "getLabelFn"
> & IsIncome;
const TransactionTagSelect: FC<Props> = props => {
const TransactionTagSelect: FC<Props> = ({ isIncome, ...props }) => {
let { objects: tags } = useAllTransactionTagsList();
tags = tags.filter(tag => tag.isIncome === props.isIncome);
tags = tags.filter(tag => tag.isIncome === isIncome);
return (
<ObjectSelect

View File

@@ -3,7 +3,7 @@ import { Checkbox, CheckboxProps, Divider, Stack, Text } from "@mantine/core";
import ClientSelectNew from "../../../../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import {
BaseMarketplaceSchema,
BoardSchema,
BoardSchema, CardTagSchema,
ClientSchema,
ProjectSchema,
StatusSchema,
@@ -13,12 +13,14 @@ import {
import { ObjectSelectProps } from "../../../../../../components/ObjectSelect/ObjectSelect.tsx";
import BaseMarketplaceSelect
from "../../../../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import CardStatusSelect from "../../../../../../components/DealStatusSelect/CardStatusSelect.tsx";
import CardStatusSelect from "../../../../../../components/CardStatusSelect/CardStatusSelect.tsx";
import { ProfitTableSegmentedControl } from "../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
import ManagerSelect from "../../../../../../components/ManagerSelect/ManagerSelect.tsx";
import TransactionTagSelect from "../../../../components/ExpenseTagSelect/TransactionTagSelect.tsx";
import BoardSelect from "../../../../../../components/BoardSelect/BoardSelect.tsx";
import ProjectSelect from "../../../../../../components/ProjectSelect/ProjectSelect.tsx";
import CardTagSelect from "../../../../../../components/Selects/CardTagSelect/CardTagSelect.tsx";
import { useEffect } from "react";
type SelectProps<T> = Omit<
@@ -41,6 +43,8 @@ type FiltersProps = {
statusSelectProps?: Omit<ObjectSelectProps<StatusSchema | null>, "data">;
cardTagSelectProps?: Omit<ObjectSelectProps<CardTagSchema | null>, "data">;
managerSelectProps?: SelectProps<UserSchema | null | undefined>;
onManagerClear?: () => void;
@@ -78,6 +82,21 @@ export const Filters = (props: FiltersProps) => {
);
};
useEffect(() => {
if (props.boardSelectProps?.onClear) {
props.boardSelectProps.onClear();
}
if (props.cardTagSelectProps && !props.projectSelectProps?.value) {
props.cardTagSelectProps.onChange(null);
}
}, [props.projectSelectProps?.value]);
useEffect(() => {
if (props.statusSelectProps?.onClear) {
props.statusSelectProps.onClear();
}
}, [props.boardSelectProps?.value]);
return (
<Stack mb={"lg"}>
<Divider />
@@ -104,6 +123,7 @@ export const Filters = (props: FiltersProps) => {
project={props.projectSelectProps?.value ?? null}
{...props.boardSelectProps}
clearable
disabled={!props.projectSelectProps?.value}
/>
}
{props.statusSelectProps &&
@@ -111,6 +131,16 @@ export const Filters = (props: FiltersProps) => {
board={props.boardSelectProps?.value ?? null}
{...props.statusSelectProps}
clearable
disabled={!props.boardSelectProps?.value}
/>
}
{props.cardTagSelectProps &&
<CardTagSelect
project={props.projectSelectProps?.value ?? null}
{...props.cardTagSelectProps}
clearable
searchable
disabled={!props.projectSelectProps?.value}
/>
}
{props.clientSelectProps &&

View File

@@ -19,6 +19,7 @@ export const useProfitTableColumns = ({ groupTableBy, statuses }: Props) => {
[GroupStatisticsTable.BY_MARKETPLACES]: "Маркетплейс",
[GroupStatisticsTable.BY_WAREHOUSES]: "Склад отгрузки",
[GroupStatisticsTable.BY_MANAGERS]: "Менеджер",
[GroupStatisticsTable.BY_TAGS]: "Тег",
};
const getConditionalColumns = (): MRT_ColumnDef<ProfitTableDataItem>[] => {

View File

@@ -11,6 +11,7 @@ export enum GroupStatisticsTable {
BY_WAREHOUSES,
BY_MARKETPLACES,
BY_MANAGERS,
BY_TAGS,
}
type ControlProps = Omit<SegmentedControlProps, "data">;
@@ -22,7 +23,7 @@ type OtherProps = {
type Props = ControlProps & OtherProps;
export const ProfitTableSegmentedControl: FC<Props> = props => {
export const ProfitTableSegmentedControl: FC<Props> = ({ selectedBoard, selectedProject, ...props}) => {
const data: (string | SegmentedControlItem)[] = [
{
label: "По датам",
@@ -39,12 +40,17 @@ export const ProfitTableSegmentedControl: FC<Props> = props => {
{
label: "По доскам",
value: GroupStatisticsTable.BY_BOARDS.toString(),
disabled: !props.selectedProject,
disabled: !selectedProject,
},
{
label: "По статусам",
value: GroupStatisticsTable.BY_STATUSES.toString(),
disabled: !props.selectedBoard,
disabled: !selectedBoard,
},
{
label: "По тегам",
value: GroupStatisticsTable.BY_TAGS.toString(),
disabled: !selectedProject,
},
{
label: "По складам отгрузки",
@@ -67,15 +73,15 @@ export const ProfitTableSegmentedControl: FC<Props> = props => {
useEffect(() => {
if (props.value === GroupStatisticsTable.BY_STATUSES.toString()) {
if (!props.selectedProject) {
if (!selectedProject) {
setGrouping(GroupStatisticsTable.BY_PROJECTS);
} else if (!props.selectedBoard) {
} else if (!selectedBoard) {
setGrouping(GroupStatisticsTable.BY_BOARDS);
}
} else if (props.value === GroupStatisticsTable.BY_BOARDS.toString() && !props.selectedProject) {
} else if (props.value === GroupStatisticsTable.BY_BOARDS.toString() && !selectedProject) {
setGrouping(GroupStatisticsTable.BY_PROJECTS);
}
}, [props.selectedBoard, props.selectedProject]);
}, [selectedBoard, selectedProject]);
return (
<SegmentedControl

View File

@@ -29,10 +29,11 @@ const useProfitTabContextState = () => {
project: null,
board: null,
status: null,
cardTag: null,
manager: null,
expenseTag: null,
incomeTag: null,
isCompletedOnly: true,
isCompletedOnly: false,
},
});
const [isChartLoading, setIsChartLoading] = useState(false);
@@ -53,6 +54,7 @@ const useProfitTabContextState = () => {
projectId: form.values.project?.id ?? -1,
boardId: form.values.board?.id ?? -1,
cardStatusId: form.values.status?.id ?? -1,
cardTagId: form.values.cardTag?.id ?? -1,
managerId: form.values.manager?.id ?? -1,
expenseTagId: form.values.expenseTag?.id ?? -1,
incomeTagId: form.values.incomeTag?.id ?? -1,

View File

@@ -43,6 +43,7 @@ export const ProfitFiltersModal = ({ form }: Props) => {
projectSelectProps={form.getInputProps("project")}
boardSelectProps={form.getInputProps("board")}
statusSelectProps={form.getInputProps("status")}
cardTagSelectProps={form.getInputProps("cardTag")}
managerSelectProps={form.getInputProps("manager")}
onManagerClear={() => form.setFieldValue("manager", null)}
isCompletedOnlyCheckboxProps={form.getInputProps("isCompletedOnly", { type: "checkbox" })}

View File

@@ -2,7 +2,7 @@ import {
GroupStatisticsTable,
} from "../tabs/ProfitTab/components/ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
import {
BaseMarketplaceSchema, BoardSchema,
BaseMarketplaceSchema, BoardSchema, CardTagSchema,
ClientSchema,
ProjectSchema,
StatusSchema,
@@ -18,6 +18,7 @@ export interface FormFilters {
project: ProjectSchema | null;
board: BoardSchema | null;
status: StatusSchema | null;
cardTag: CardTagSchema | null;
manager: UserSchema | null;
isCompletedOnly: boolean;
expenseTag: TransactionTagSchema | null;