From e9bfd39ab46d2e4b09f9dd189a19b54aba639c3c Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Sat, 4 Oct 2025 18:18:17 +0400 Subject: [PATCH] feat: clients page --- .../ClientDesktopHeader.tsx | 23 ++ .../ClientMobileHeader/ClientMobileHeader.tsx | 31 +++ .../shared/ClientsTable/ClientsTable.tsx | 32 +++ .../shared/ClientsTable/columns.tsx | 58 +++++ .../components/shared/PageBody/PageBody.tsx | 37 +++ src/app/clients/contexts/ClientsContext.tsx | 39 +++ src/app/clients/hooks/useClientsActions.tsx | 37 +++ src/app/clients/hooks/useClientsCrud.tsx | 45 ++++ src/app/clients/hooks/useClientsFilter.tsx | 46 ++++ src/app/clients/hooks/useClientsList.tsx | 17 ++ .../ClientFormModal/ClientFormModal.tsx | 105 ++++++++ src/app/clients/page.tsx | 22 ++ src/app/clients/utils/isValidInn.ts | 5 + src/components/layout/Navbar/Navbar.tsx | 6 + .../cruds/baseCrud/useCrudOperations.tsx | 2 +- src/lib/client/@tanstack/react-query.gen.ts | 137 ++++++++++ src/lib/client/sdk.gen.ts | 119 +++++++++ src/lib/client/types.gen.ts | 242 ++++++++++++++++++ src/lib/client/zod.gen.ts | 131 ++++++++++ src/modals/modals.ts | 10 +- 20 files changed, 1141 insertions(+), 3 deletions(-) create mode 100644 src/app/clients/components/desktop/ClientDesktopHeader/ClientDesktopHeader.tsx create mode 100644 src/app/clients/components/mobile/ClientMobileHeader/ClientMobileHeader.tsx create mode 100644 src/app/clients/components/shared/ClientsTable/ClientsTable.tsx create mode 100644 src/app/clients/components/shared/ClientsTable/columns.tsx create mode 100644 src/app/clients/components/shared/PageBody/PageBody.tsx create mode 100644 src/app/clients/contexts/ClientsContext.tsx create mode 100644 src/app/clients/hooks/useClientsActions.tsx create mode 100644 src/app/clients/hooks/useClientsCrud.tsx create mode 100644 src/app/clients/hooks/useClientsFilter.tsx create mode 100644 src/app/clients/hooks/useClientsList.tsx create mode 100644 src/app/clients/modals/ClientFormModal/ClientFormModal.tsx create mode 100644 src/app/clients/page.tsx create mode 100644 src/app/clients/utils/isValidInn.ts diff --git a/src/app/clients/components/desktop/ClientDesktopHeader/ClientDesktopHeader.tsx b/src/app/clients/components/desktop/ClientDesktopHeader/ClientDesktopHeader.tsx new file mode 100644 index 0000000..d495c33 --- /dev/null +++ b/src/app/clients/components/desktop/ClientDesktopHeader/ClientDesktopHeader.tsx @@ -0,0 +1,23 @@ +import { FC } from "react"; +import { Group, TextInput } from "@mantine/core"; +import { useClientsContext } from "@/app/clients/contexts/ClientsContext"; +import useClientsActions from "@/app/clients/hooks/useClientsActions"; +import InlineButton from "@/components/ui/InlineButton/InlineButton"; + +const ClientDesktopHeader: FC = () => { + const { search, setSearch } = useClientsContext(); + const { onCreateClick } = useClientsActions(); + + return ( + + Создать клиента + setSearch(e.target.value)} + /> + + ); +}; + +export default ClientDesktopHeader; diff --git a/src/app/clients/components/mobile/ClientMobileHeader/ClientMobileHeader.tsx b/src/app/clients/components/mobile/ClientMobileHeader/ClientMobileHeader.tsx new file mode 100644 index 0000000..c2f9db3 --- /dev/null +++ b/src/app/clients/components/mobile/ClientMobileHeader/ClientMobileHeader.tsx @@ -0,0 +1,31 @@ +import { FC } from "react"; +import { Flex, TextInput } from "@mantine/core"; +import { useClientsContext } from "@/app/clients/contexts/ClientsContext"; +import useClientsActions from "@/app/clients/hooks/useClientsActions"; +import InlineButton from "@/components/ui/InlineButton/InlineButton"; + +const ClientMobileHeader: FC = () => { + const { search, setSearch } = useClientsContext(); + const { onCreateClick } = useClientsActions(); + + return ( + + + Создать клиента + + setSearch(e.target.value)} + /> + + ); +}; + +export default ClientMobileHeader; diff --git a/src/app/clients/components/shared/ClientsTable/ClientsTable.tsx b/src/app/clients/components/shared/ClientsTable/ClientsTable.tsx new file mode 100644 index 0000000..c455f51 --- /dev/null +++ b/src/app/clients/components/shared/ClientsTable/ClientsTable.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { FC } from "react"; +import { useClientsTableColumns } from "@/app/clients/components/shared/ClientsTable/columns"; +import { useClientsContext } from "@/app/clients/contexts/ClientsContext"; +import useClientsActions from "@/app/clients/hooks/useClientsActions"; +import BaseTable from "@/components/ui/BaseTable/BaseTable"; +import useIsMobile from "@/hooks/utils/useIsMobile"; + +const ClientsTable: FC = () => { + const isMobile = useIsMobile(); + const { clientsCrud, clients } = useClientsContext(); + const { onUpdateClick } = useClientsActions(); + + const columns = useClientsTableColumns({ + onDelete: clientsCrud.onDelete, + onChange: onUpdateClick, + }); + + return ( + + ); +}; + +export default ClientsTable; diff --git a/src/app/clients/components/shared/ClientsTable/columns.tsx b/src/app/clients/components/shared/ClientsTable/columns.tsx new file mode 100644 index 0000000..368c121 --- /dev/null +++ b/src/app/clients/components/shared/ClientsTable/columns.tsx @@ -0,0 +1,58 @@ +import { useMemo } from "react"; +import { DataTableColumn } from "mantine-datatable"; +import { Center } from "@mantine/core"; +import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions"; +import { ClientSchema } from "@/lib/client"; + +type Props = { + onChange: (client: ClientSchema) => void; + onDelete: (client: ClientSchema) => void; +}; + +export const useClientsTableColumns = ({ onChange, onDelete }: Props) => { + return useMemo( + () => + [ + { + accessor: "actions", + title:
Действия
, + width: "0%", + render: client => ( + onDelete(client)} + onChange={() => onChange(client)} + /> + ), + }, + { + accessor: "name", + title: "Имя", + }, + { + accessor: "details.telegram", + title: "Телеграм", + }, + { + accessor: "details.email", + title: "Почта", + }, + { + accessor: "details.phoneNumber", + title: "Телефон", + }, + { + accessor: "details.inn", + title: "ИНН", + }, + { + accessor: "companyName", + title: "Название компании", + }, + { + accessor: "comment", + title: "Комментарий", + }, + ] as DataTableColumn[], + [onChange, onDelete] + ); +}; diff --git a/src/app/clients/components/shared/PageBody/PageBody.tsx b/src/app/clients/components/shared/PageBody/PageBody.tsx new file mode 100644 index 0000000..b7c512d --- /dev/null +++ b/src/app/clients/components/shared/PageBody/PageBody.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { FC } from "react"; +import { Stack } from "@mantine/core"; +import ClientDesktopHeader from "@/app/clients/components/desktop/ClientDesktopHeader/ClientDesktopHeader"; +import ClientsTable from "@/app/clients/components/shared/ClientsTable/ClientsTable"; +import PageBlock from "@/components/layout/PageBlock/PageBlock"; +import useIsMobile from "@/hooks/utils/useIsMobile"; +import ClientMobileHeader from "@/app/clients/components/mobile/ClientMobileHeader/ClientMobileHeader"; + +const PageBody: FC = () => { + const isMobile = useIsMobile(); + + return ( + + {!isMobile && ( + + + + )} + + + {isMobile && } +
+ +
+
+
+
+ ); +}; + +export default PageBody; diff --git a/src/app/clients/contexts/ClientsContext.tsx b/src/app/clients/contexts/ClientsContext.tsx new file mode 100644 index 0000000..4b74d14 --- /dev/null +++ b/src/app/clients/contexts/ClientsContext.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Dispatch, SetStateAction } from "react"; +import { + ClientsCrud, + useClientsCrud, +} from "@/app/clients/hooks/useClientsCrud"; +import useClientsFilter from "@/app/clients/hooks/useClientsFilter"; +import useClientsList from "@/app/clients/hooks/useClientsList"; +import { ClientSchema } from "@/lib/client"; +import makeContext from "@/lib/contextFactory/contextFactory"; + +type ClientsContextState = { + clients: ClientSchema[]; + refetchClients: () => void; + search: string; + setSearch: Dispatch>; + clientsCrud: ClientsCrud; +}; + +const useClientsContextState = (): ClientsContextState => { + const clientsList = useClientsList(); + + const { filteredClients, search, setSearch } = + useClientsFilter(clientsList); + + const clientsCrud = useClientsCrud(clientsList); + + return { + clients: filteredClients, + refetchClients: clientsList.refetch, + search, + setSearch, + clientsCrud, + }; +}; + +export const [ClientsContextProvider, useClientsContext] = + makeContext(useClientsContextState, "Clients"); diff --git a/src/app/clients/hooks/useClientsActions.tsx b/src/app/clients/hooks/useClientsActions.tsx new file mode 100644 index 0000000..a91f56a --- /dev/null +++ b/src/app/clients/hooks/useClientsActions.tsx @@ -0,0 +1,37 @@ +import { modals } from "@mantine/modals"; +import { useClientsContext } from "@/app/clients/contexts/ClientsContext"; +import { ClientSchema } from "@/lib/client"; + +const useClientsActions = () => { + const { clientsCrud } = useClientsContext(); + + const onCreateClick = () => { + modals.openContextModal({ + modal: "clientEditorModal", + title: "Создание клиента", + innerProps: { + onCreate: clientsCrud.onCreate, + isEditing: false, + }, + }); + }; + + const onUpdateClick = (client: ClientSchema) => { + modals.openContextModal({ + modal: "clientEditorModal", + title: "Редактирование клиента", + innerProps: { + onChange: updates => clientsCrud.onUpdate(client.id, updates), + entity: client, + isEditing: true, + }, + }); + }; + + return { + onCreateClick, + onUpdateClick, + }; +}; + +export default useClientsActions; diff --git a/src/app/clients/hooks/useClientsCrud.tsx b/src/app/clients/hooks/useClientsCrud.tsx new file mode 100644 index 0000000..ca13248 --- /dev/null +++ b/src/app/clients/hooks/useClientsCrud.tsx @@ -0,0 +1,45 @@ +import { useCrudOperations } from "@/hooks/cruds/baseCrud"; +import { + ClientSchema, + CreateClientSchema, + UpdateClientSchema, +} from "@/lib/client"; +import { + createClientMutation, + deleteClientMutation, + updateClientMutation, +} from "@/lib/client/@tanstack/react-query.gen"; + +type UseClientsProps = { + queryKey: any[]; +}; + +export type ClientsCrud = { + onCreate: (client: CreateClientSchema) => void; + onUpdate: (clientId: number, client: UpdateClientSchema) => void; + onDelete: (client: ClientSchema) => void; +}; + +export const useClientsCrud = ({ queryKey }: UseClientsProps): ClientsCrud => { + return useCrudOperations< + ClientSchema, + UpdateClientSchema, + CreateClientSchema + >({ + key: "getClients", + queryKey, + mutations: { + create: createClientMutation(), + update: updateClientMutation(), + delete: deleteClientMutation(), + }, + getUpdateEntity: (old, update) => ({ + ...old, + details: update.details ?? old.details, + name: update.name ?? old.name, + companyName: update.companyName ?? old.companyName, + comment: update.comment ?? old.comment, + }), + getDeleteConfirmTitle: () => "Удаление клиента", + }); +}; diff --git a/src/app/clients/hooks/useClientsFilter.tsx b/src/app/clients/hooks/useClientsFilter.tsx new file mode 100644 index 0000000..7d7aa3f --- /dev/null +++ b/src/app/clients/hooks/useClientsFilter.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; +import { useDebouncedValue } from "@mantine/hooks"; +import { ClientSchema } from "@/lib/client"; + +type Props = { + clients: ClientSchema[]; +}; + +const useClientsFilter = ({ clients }: Props) => { + const [search, setSearch] = useState(""); + const [debouncedSearch] = useDebouncedValue(search, 400); + const [filteredClients, setFilteredClients] = useState([]); + + const filterClients = () => { + if (debouncedSearch.length === 0) { + setFilteredClients(clients); + return; + } + + const loweredSearch = debouncedSearch.toLowerCase(); + const filtered = clients.filter( + client => + client.name.toLowerCase().includes(loweredSearch) || + client.details?.inn?.includes(loweredSearch) || + client.details?.email?.toLowerCase().includes(loweredSearch) || + client.details?.telegram + ?.toLowerCase() + .includes(loweredSearch) || + client.details?.phoneNumber?.includes(loweredSearch) || + client.companyName.toLowerCase().includes(loweredSearch) + ); + setFilteredClients(filtered); + }; + + useEffect(() => { + filterClients(); + }, [debouncedSearch, clients]); + + return { + search, + setSearch, + filteredClients, + }; +}; + +export default useClientsFilter; diff --git a/src/app/clients/hooks/useClientsList.tsx b/src/app/clients/hooks/useClientsList.tsx new file mode 100644 index 0000000..5305ddf --- /dev/null +++ b/src/app/clients/hooks/useClientsList.tsx @@ -0,0 +1,17 @@ +import { useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { + getClientsOptions, + getClientsQueryKey, +} from "@/lib/client/@tanstack/react-query.gen"; + +const useClientsList = () => { + const { data, refetch } = useQuery(getClientsOptions()); + const clients = useMemo(() => data?.items ?? [], [data]); + + const queryKey = getClientsQueryKey(); + + return { clients, refetch, queryKey }; +}; + +export default useClientsList; diff --git a/src/app/clients/modals/ClientFormModal/ClientFormModal.tsx b/src/app/clients/modals/ClientFormModal/ClientFormModal.tsx new file mode 100644 index 0000000..6625920 --- /dev/null +++ b/src/app/clients/modals/ClientFormModal/ClientFormModal.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { Fieldset, Stack, Textarea, TextInput } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { ContextModalProps } from "@mantine/modals"; +import isValidInn from "@/app/clients/utils/isValidInn"; +import { + ClientSchema, + CreateClientSchema, + UpdateClientSchema, +} from "@/lib/client"; +import BaseFormModal, { + CreateEditFormProps, +} from "@/modals/base/BaseFormModal/BaseFormModal"; + +type Props = CreateEditFormProps< + ClientSchema, + CreateClientSchema, + UpdateClientSchema +>; + +const ClientEditorModal = ({ + context, + id, + innerProps, +}: ContextModalProps) => { + const initialValues = innerProps.isEditing + ? innerProps.entity + : ({ + name: "", + companyName: "", + details: { + telegram: "", + phoneNumber: "", + email: "", + inn: "", + }, + comment: "", + } as CreateClientSchema); + + const form = useForm({ + initialValues, + validate: { + name: name => + (!name || name.trim() === "") && + "Необходимо ввести название клиента", + details: { + inn: inn => inn.length > 0 && !isValidInn(inn) && "Некорректный ИНН", + }, + }, + }); + + return ( + context.closeContextModal(id)}> +
+ +
+
+ + + + + + +