From 03be3903cb5e13620907108096f3470626cd3342 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Sun, 2 Nov 2025 16:07:49 +0400 Subject: [PATCH] feat: attributes page --- .../actions/components/PageBody/PageBody.tsx | 7 +- .../components/AttributesHeader.tsx | 37 ++++++++++ .../attributes/components/AttributesTable.tsx | 40 +++++++++++ src/app/attributes/components/PageBody.tsx | 26 +++++++ .../attributes/contexts/AttributesContext.tsx | 40 +++++++++++ .../hooks/useAttributesTableColumns.tsx | 72 +++++++++++++++++++ .../attributes/hooks/useFilteredAttributes.ts | 29 ++++++++ src/app/attributes/page.tsx | 22 ++++++ .../AttributeTableActions.tsx | 31 ++++---- .../useAttributesTableColumns.tsx | 7 +- .../ModuleAttribute/ModuleAttribute.tsx | 4 +- .../[moduleId]/hooks/useAttributesActions.tsx | 4 +- .../[moduleId]/hooks/useAttributesCrud.tsx | 8 +-- .../hooks/useAttributesInnerTableColumns.tsx | 27 +------ .../layout/Navbar/data/linksData.ts | 7 ++ .../AttributeDefaultValue.tsx | 35 +++++++++ 16 files changed, 348 insertions(+), 48 deletions(-) create mode 100644 src/app/attributes/components/AttributesHeader.tsx create mode 100644 src/app/attributes/components/AttributesTable.tsx create mode 100644 src/app/attributes/components/PageBody.tsx create mode 100644 src/app/attributes/contexts/AttributesContext.tsx create mode 100644 src/app/attributes/hooks/useAttributesTableColumns.tsx create mode 100644 src/app/attributes/hooks/useFilteredAttributes.ts create mode 100644 src/app/attributes/page.tsx create mode 100644 src/components/ui/AttributeDefaultValue/AttributeDefaultValue.tsx diff --git a/src/app/actions/components/PageBody/PageBody.tsx b/src/app/actions/components/PageBody/PageBody.tsx index d6aa1f7..33376f7 100644 --- a/src/app/actions/components/PageBody/PageBody.tsx +++ b/src/app/actions/components/PageBody/PageBody.tsx @@ -1,7 +1,7 @@ "use client"; import { RefObject, useMemo, useRef } from "react"; -import { IconTag } from "@tabler/icons-react"; +import { IconList, IconTag } from "@tabler/icons-react"; import { SimpleGrid, Stack } from "@mantine/core"; import Action from "@/app/actions/components/Action/Action"; import mobileButtonsData from "@/app/actions/data/mobileButtonsData"; @@ -23,6 +23,11 @@ const PageBody = () => { ); const commonActionsData: RefObject = useRef([ + { + icon: IconList, + label: "Атрибуты", + href: "/attributes", + }, { icon: IconTag, label: "Теги", diff --git a/src/app/attributes/components/AttributesHeader.tsx b/src/app/attributes/components/AttributesHeader.tsx new file mode 100644 index 0000000..ca91ba8 --- /dev/null +++ b/src/app/attributes/components/AttributesHeader.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { FC } from "react"; +import { Group, TextInput } from "@mantine/core"; +import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext"; +import InlineButton from "@/components/ui/InlineButton/InlineButton"; +import useIsMobile from "@/hooks/utils/useIsMobile"; + +const AttributesHeader: FC = () => { + const { + attributesActions: { onCreate }, + search, + setSearch, + } = useAttributesContext(); + const isMobile = useIsMobile(); + + return ( + + + Создать атрибут + + setSearch(e.currentTarget.value)} + w={isMobile ? "100%" : "auto"} + placeholder={"Поиск..."} + /> + + ); +}; + +export default AttributesHeader; diff --git a/src/app/attributes/components/AttributesTable.tsx b/src/app/attributes/components/AttributesTable.tsx new file mode 100644 index 0000000..c3a2ec6 --- /dev/null +++ b/src/app/attributes/components/AttributesTable.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { FC } from "react"; +import { IconMoodSad } from "@tabler/icons-react"; +import { Group, Text } from "@mantine/core"; +import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext"; +import useAttributesTableColumns from "@/app/attributes/hooks/useAttributesTableColumns"; +import BaseTable from "@/components/ui/BaseTable/BaseTable"; +import useIsMobile from "@/hooks/utils/useIsMobile"; + +const AttributesTable: FC = () => { + const isMobile = useIsMobile(); + const { attributes } = useAttributesContext(); + const columns = useAttributesTableColumns(); + + return ( + + Нет атрибутов + + + } + groups={undefined} + styles={{ + table: { + width: "100%", + }, + header: { zIndex: 1 }, + }} + mx={isMobile ? "xs" : 0} + /> + ); +}; + +export default AttributesTable; diff --git a/src/app/attributes/components/PageBody.tsx b/src/app/attributes/components/PageBody.tsx new file mode 100644 index 0000000..730d54b --- /dev/null +++ b/src/app/attributes/components/PageBody.tsx @@ -0,0 +1,26 @@ +"use client"; + +import AttributesHeader from "@/app/attributes/components/AttributesHeader"; +import AttributesTable from "@/app/attributes/components/AttributesTable"; +import PageBlock from "@/components/layout/PageBlock/PageBlock"; + +const PageBody = () => ( + +
+ +
+ +
+
+
+); + +export default PageBody; diff --git a/src/app/attributes/contexts/AttributesContext.tsx b/src/app/attributes/contexts/AttributesContext.tsx new file mode 100644 index 0000000..d2867c0 --- /dev/null +++ b/src/app/attributes/contexts/AttributesContext.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Dispatch, SetStateAction } from "react"; +import useFilteredAttributes from "@/app/attributes/hooks/useFilteredAttributes"; +import useAttributesActions, { + AttributesActions, +} from "@/app/module-editor/[moduleId]/hooks/useAttributesActions"; +import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList"; +import { AttributeSchema } from "@/lib/client"; +import makeContext from "@/lib/contextFactory/contextFactory"; + +type AttributesContextState = { + attributes: AttributeSchema[]; + refetchAttributes: () => void; + attributesActions: AttributesActions; + search: string; + setSearch: Dispatch>; +}; + +const useAttributesContextState = (): AttributesContextState => { + const { attributes, refetch } = useAttributesList(); + const attributesActions = useAttributesActions({ + refetchAttributes: refetch, + }); + + const { search, setSearch, filteredAttributes } = useFilteredAttributes({ + attributes, + }); + + return { + attributes: filteredAttributes, + refetchAttributes: refetch, + attributesActions, + search, + setSearch, + }; +}; + +export const [AttributesContextProvider, useAttributesContext] = + makeContext(useAttributesContextState, "Attribute"); diff --git a/src/app/attributes/hooks/useAttributesTableColumns.tsx b/src/app/attributes/hooks/useAttributesTableColumns.tsx new file mode 100644 index 0000000..5a22edb --- /dev/null +++ b/src/app/attributes/hooks/useAttributesTableColumns.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useMemo } from "react"; +import { IconCheck, IconX } from "@tabler/icons-react"; +import { DataTableColumn } from "mantine-datatable"; +import { Box, Center } from "@mantine/core"; +import AttributeTableActions from "@/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions"; +import AttributeDefaultValue from "@/components/ui/AttributeDefaultValue/AttributeDefaultValue"; +import useIsMobile from "@/hooks/utils/useIsMobile"; +import { AttributeSchema } from "@/lib/client"; +import { useAttributesContext } from "../contexts/AttributesContext"; + +const useAttributesTableColumns = () => { + const isMobile = useIsMobile(); + const { attributesActions } = useAttributesContext(); + const renderCheck = (value: boolean) => (value ? : ); + + return useMemo( + () => + [ + { + title: "Название атрибута", + accessor: "label", + }, + { + title: "Тип", + accessor: "type.name", + render: attr => + attr.type.type === "select" + ? `Выбор "${attr.label}"` + : attr.type.name, + }, + { + title: "Значение по умолчанию", + accessor: "defaultValue", + render: attr => , + }, + { + title: isMobile + ? "Синх. в группе" + : "Синхронизировано в группе", + accessor: "isApplicableToGroup", + render: attr => renderCheck(attr.isApplicableToGroup), + }, + { + title: "Может быть пустым", + accessor: "isNullable", + render: attr => renderCheck(attr.isNullable), + }, + { + title: "Описаниие", + accessor: "description", + render: attr => {attr.description}, + }, + { + accessor: "actions", + title:
Действия
, + width: "0%", + render: attribute => ( + + ), + }, + ] as DataTableColumn[], + [isMobile] + ); +}; + +export default useAttributesTableColumns; diff --git a/src/app/attributes/hooks/useFilteredAttributes.ts b/src/app/attributes/hooks/useFilteredAttributes.ts new file mode 100644 index 0000000..3185627 --- /dev/null +++ b/src/app/attributes/hooks/useFilteredAttributes.ts @@ -0,0 +1,29 @@ +import { useMemo, useState } from "react"; +import { AttributeSchema } from "@/lib/client"; + +type Props = { + attributes: AttributeSchema[]; +}; + +const useFilteredAttributes = ({ attributes }: Props) => { + const [search, setSearch] = useState(""); + + const filteredAttributes = useMemo( + () => + attributes.filter( + attr => + attr.type.name.includes(search) || + attr.label.includes(search) || + attr.description.includes(search) + ), + [attributes, search] + ); + + return { + search, + setSearch, + filteredAttributes, + }; +}; + +export default useFilteredAttributes; diff --git a/src/app/attributes/page.tsx b/src/app/attributes/page.tsx new file mode 100644 index 0000000..a8a4c9d --- /dev/null +++ b/src/app/attributes/page.tsx @@ -0,0 +1,22 @@ +import { Suspense } from "react"; +import { Center, Loader } from "@mantine/core"; +import PageBody from "@/app/attributes/components/PageBody"; +import { AttributesContextProvider } from "@/app/attributes/contexts/AttributesContext"; +import PageContainer from "@/components/layout/PageContainer/PageContainer"; + +export default async function AttributesPage() { + return ( + + + + }> + + + + + + + ); +} diff --git a/src/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions.tsx b/src/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions.tsx index c236c1d..61ade6f 100644 --- a/src/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions.tsx +++ b/src/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions.tsx @@ -1,39 +1,44 @@ import { FC } from "react"; import { IconArrowRight, IconEdit, IconTrash } from "@tabler/icons-react"; import { Center, Flex } from "@mantine/core"; -import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext"; import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip"; import { AttributeSchema } from "@/lib/client"; type Props = { attribute: AttributeSchema; + onUpdate: (attribute: AttributeSchema) => void; + onDelete: (attribute: AttributeSchema) => void; + addAttributeToModule?: (attribute: AttributeSchema) => void; }; -const AttributeTableActions: FC = ({ attribute }) => { - const { attributeActions } = useModuleEditorContext(); - +const AttributeTableActions: FC = ({ + attribute, + onUpdate, + onDelete, + addAttributeToModule, +}) => { return (
attributeActions.onUpdate(attribute)} + onClick={() => onUpdate(attribute)} disabled={attribute.isBuiltIn} tipLabel={"Редактировать"}> attributeActions.onDelete(attribute)} + onClick={() => onDelete(attribute)} disabled={attribute.isBuiltIn} tipLabel={"Удалить"}> - - attributeActions.addAttributeToModule(attribute) - } - tipLabel={"Добавить в модуль"}> - - + {addAttributeToModule && ( + addAttributeToModule(attribute)} + tipLabel={"Добавить в модуль"}> + + + )}
); diff --git a/src/app/module-editor/[moduleId]/components/shared/AttributesTable/useAttributesTableColumns.tsx b/src/app/module-editor/[moduleId]/components/shared/AttributesTable/useAttributesTableColumns.tsx index 16d1faa..792d8c0 100644 --- a/src/app/module-editor/[moduleId]/components/shared/AttributesTable/useAttributesTableColumns.tsx +++ b/src/app/module-editor/[moduleId]/components/shared/AttributesTable/useAttributesTableColumns.tsx @@ -3,12 +3,14 @@ import { useMemo } from "react"; import { DataTableColumn } from "mantine-datatable"; import { Center } from "@mantine/core"; +import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext"; import AttributeTableActions from "@/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions"; import useIsMobile from "@/hooks/utils/useIsMobile"; import { AttributeSchema } from "@/lib/client"; const useAttributesTableColumns = () => { const isMobile = useIsMobile(); + const { attributesActions } = useAttributesContext(); return useMemo( () => @@ -30,7 +32,10 @@ const useAttributesTableColumns = () => { title:
Действия
, width: "0%", render: attribute => ( - + ), }, ] as DataTableColumn[], diff --git a/src/app/module-editor/[moduleId]/components/shared/ModuleAttribute/ModuleAttribute.tsx b/src/app/module-editor/[moduleId]/components/shared/ModuleAttribute/ModuleAttribute.tsx index 279d145..ab11d25 100644 --- a/src/app/module-editor/[moduleId]/components/shared/ModuleAttribute/ModuleAttribute.tsx +++ b/src/app/module-editor/[moduleId]/components/shared/ModuleAttribute/ModuleAttribute.tsx @@ -1,5 +1,5 @@ import { FC } from "react"; -import { IconEdit, IconX } from "@tabler/icons-react"; +import { IconEditCircle, IconX } from "@tabler/icons-react"; import { Card, Group, Stack, Text, Title } from "@mantine/core"; import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext"; import styles from "@/app/module-editor/[moduleId]/ModuleEditor.module.css"; @@ -55,7 +55,7 @@ const ModuleAttribute: FC = ({ attribute }) => { "Редактировать название (только для данного модуля)" } onClick={() => onEditAttributeLabel(attribute)}> - + void; + refetchModule?: () => void; refetchAttributes: () => void; }; @@ -40,7 +40,7 @@ const useAttributesActions = (props: Props): AttributesActions => { }; const onEditAttributeLabel = (attribute: AttributeSchema) => { - if (!module) return; + if (!props.module) return; modals.openContextModal({ modal: "enterNameModal", diff --git a/src/app/module-editor/[moduleId]/hooks/useAttributesCrud.tsx b/src/app/module-editor/[moduleId]/hooks/useAttributesCrud.tsx index 5023aa3..a44397e 100644 --- a/src/app/module-editor/[moduleId]/hooks/useAttributesCrud.tsx +++ b/src/app/module-editor/[moduleId]/hooks/useAttributesCrud.tsx @@ -19,7 +19,7 @@ import { notifications } from "@/lib/notifications"; type Props = { module?: ModuleSchemaOutput; - refetchModule: () => void; + refetchModule?: () => void; refetchAttributes: () => void; }; @@ -95,7 +95,7 @@ const useAttributesCrud = ({ { onSuccess: ({ message }) => { notifications.success({ message }); - refetchModule(); + refetchModule && refetchModule(); refetchAttributes(); removeGetDealModuleAttrQuery(); }, @@ -139,8 +139,8 @@ const useAttributesCrud = ({ ...updateAttributeMutation(), onError, onSuccess: () => { + refetchModule && refetchModule(); refetchAttributes(); - refetchModule(); }, }); @@ -159,7 +159,7 @@ const useAttributesCrud = ({ ...deleteAttributeMutation(), onError, onSuccess: () => { - refetchModule(); + refetchModule && refetchModule(); refetchAttributes(); }, }); diff --git a/src/app/modules/hooks/useAttributesInnerTableColumns.tsx b/src/app/modules/hooks/useAttributesInnerTableColumns.tsx index fca20e4..6426d02 100644 --- a/src/app/modules/hooks/useAttributesInnerTableColumns.tsx +++ b/src/app/modules/hooks/useAttributesInnerTableColumns.tsx @@ -4,13 +4,9 @@ import { useMemo } from "react"; import { IconCheck, IconX } from "@tabler/icons-react"; import { DataTableColumn } from "mantine-datatable"; import { Box } from "@mantine/core"; +import AttributeDefaultValue from "@/components/ui/AttributeDefaultValue/AttributeDefaultValue"; import useIsMobile from "@/hooks/utils/useIsMobile"; import { ModuleAttributeSchema } from "@/lib/client"; -import { - naiveDateTimeStringToUtc, - utcDateTimeToLocalString, - utcDateToLocalString, -} from "@/utils/datetime"; const useAttributesInnerTableColumns = () => { const isMobile = useIsMobile(); @@ -35,26 +31,7 @@ const useAttributesInnerTableColumns = () => { { title: "Значение по умолчанию", accessor: "defaultValue", - render: attr => { - if (!attr.defaultValue) return <>-; - const value = attr.defaultValue; - if (value === null) return <>-; - - const type = attr.type.type; - if (type === "datetime") { - return utcDateTimeToLocalString( - naiveDateTimeStringToUtc(value as string) - ); - } - if (type === "date") { - return utcDateToLocalString(value as string); - } - if (type === "bool") { - return value ? : ; - } - - return <>{value}; - }, + render: attr => , }, { title: "Синхронизировано в группе", diff --git a/src/components/layout/Navbar/data/linksData.ts b/src/components/layout/Navbar/data/linksData.ts index a38455a..8825a37 100644 --- a/src/components/layout/Navbar/data/linksData.ts +++ b/src/components/layout/Navbar/data/linksData.ts @@ -4,6 +4,7 @@ import { IconColumns, IconFileBarcode, IconLayoutKanban, + IconList, IconUsers, } from "@tabler/icons-react"; import { ModuleNames } from "@/modules/modules"; @@ -22,6 +23,12 @@ const linksData: LinkData[] = [ href: "/modules", moduleName: undefined, }, + { + icon: IconList, + label: "Атрибуты", + href: "/attributes", + moduleName: undefined, + }, { icon: IconUsers, label: "Клиенты", diff --git a/src/components/ui/AttributeDefaultValue/AttributeDefaultValue.tsx b/src/components/ui/AttributeDefaultValue/AttributeDefaultValue.tsx new file mode 100644 index 0000000..68515a8 --- /dev/null +++ b/src/components/ui/AttributeDefaultValue/AttributeDefaultValue.tsx @@ -0,0 +1,35 @@ +import { FC } from "react"; +import { IconCheck, IconX } from "@tabler/icons-react"; +import { AttributeSchema } from "@/lib/client"; +import { + naiveDateTimeStringToUtc, + utcDateTimeToLocalString, + utcDateToLocalString, +} from "@/utils/datetime"; + +type Props = { + attribute: AttributeSchema; +}; + +const AttributeDefaultValue: FC = ({ attribute }) => { + if (!attribute.defaultValue) return <>-; + const value = attribute.defaultValue; + if (value === null) return <>-; + + const type = attribute.type.type; + if (type === "datetime") { + return utcDateTimeToLocalString( + naiveDateTimeStringToUtc(value as string) + ); + } + if (type === "date") { + return utcDateToLocalString(value as string); + } + if (type === "bool") { + return value ? : ; + } + + return <>{value}; +}; + +export default AttributeDefaultValue;