From 47533ad7f5e88e4aba9445c06f6b25c66366d851 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Sat, 27 Sep 2025 18:24:22 +0400 Subject: [PATCH] feat: services table, base segmented control --- .../ServicesDesktopHeader.tsx | 27 +- .../ServicesMobileHeader.tsx | 4 +- .../components/shared/PageBody/PageBody.tsx | 15 +- .../ServiceCategorySelect.tsx | 24 ++ .../ServiceTabSegmentedControl.tsx | 14 +- .../ServiceTypeSegmentedControl.tsx | 12 +- .../shared/ServicesTable/ServicesTable.tsx | 76 ++++++ .../hooks/servicesInnerTableColumns.tsx | 46 ++++ .../hooks/servicesOuterTableColumns.tsx | 72 ++++++ .../ServicesTable/types/GroupedServices.ts | 6 + src/app/services/contexts/ServicesContext.tsx | 14 ++ .../services/hooks/useCategoriesActions.ts | 39 +++ src/app/services/hooks/useServicesActions.ts | 39 +++ .../modals/ServiceCategoryEditorModal.tsx | 64 +++++ .../ServiceEditorModal/ServiceEditorModal.tsx | 141 +++++++++++ .../components/RangePriceInput.tsx | 105 ++++++++ .../ServicePriceTypeSegmentedControl.tsx | 33 +++ src/app/services/modals/index.ts | 2 + .../BaseSegmentedControl.tsx | 34 +++ src/lib/client/@tanstack/react-query.gen.ts | 143 +++++++++++ src/lib/client/sdk.gen.ts | 119 +++++++++ src/lib/client/types.gen.ts | 231 ++++++++++++++++-- src/lib/client/zod.gen.ts | 117 ++++++++- src/modals/modals.ts | 4 +- .../hooks/cruds/useServiceCategoriesCrud.tsx | 84 +++++++ .../shared/hooks/cruds/useServicesCrud.tsx | 13 + .../hooks/lists/useServiceCategoriesList.ts | 43 ++++ .../FulfillmentBase/shared/types/service.ts | 5 + src/utils/lexorank.ts | 7 + 29 files changed, 1489 insertions(+), 44 deletions(-) create mode 100644 src/app/services/components/shared/ServiceCategorySelect/ServiceCategorySelect.tsx create mode 100644 src/app/services/components/shared/ServicesTable/ServicesTable.tsx create mode 100644 src/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns.tsx create mode 100644 src/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns.tsx create mode 100644 src/app/services/components/shared/ServicesTable/types/GroupedServices.ts create mode 100644 src/app/services/hooks/useCategoriesActions.ts create mode 100644 src/app/services/hooks/useServicesActions.ts create mode 100644 src/app/services/modals/ServiceCategoryEditorModal.tsx create mode 100644 src/app/services/modals/ServiceEditorModal/ServiceEditorModal.tsx create mode 100644 src/app/services/modals/ServiceEditorModal/components/RangePriceInput.tsx create mode 100644 src/app/services/modals/ServiceEditorModal/components/ServicePriceTypeSegmentedControl.tsx create mode 100644 src/components/ui/BaseSegmentedControl/BaseSegmentedControl.tsx create mode 100644 src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServiceCategoriesCrud.tsx create mode 100644 src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServiceCategoriesList.ts diff --git a/src/app/services/components/desktop/ServicesDesktopHeader/ServicesDesktopHeader.tsx b/src/app/services/components/desktop/ServicesDesktopHeader/ServicesDesktopHeader.tsx index 587fa45..d0500db 100644 --- a/src/app/services/components/desktop/ServicesDesktopHeader/ServicesDesktopHeader.tsx +++ b/src/app/services/components/desktop/ServicesDesktopHeader/ServicesDesktopHeader.tsx @@ -1,11 +1,15 @@ "use client"; import { FC } from "react"; -import { Box, Group } from "@mantine/core"; +import { IconPlus } from "@tabler/icons-react"; +import { Box, Group, Text } from "@mantine/core"; import CreateServiceKitButton from "@/app/services/components/desktop/CreateServiceKitButton/CreateServiceKitButton"; import ServiceTabSegmentedControl, { ServicesTab, } from "@/app/services/components/shared/ServiceTabSegmentedControl/ServiceTabSegmentedControl"; +import useCategoriesActions from "@/app/services/hooks/useCategoriesActions"; +import useServicesActions from "@/app/services/hooks/useServicesActions"; +import InlineButton from "@/components/ui/InlineButton/InlineButton"; type Props = { serviceTab: ServicesTab; @@ -16,12 +20,25 @@ const ServicesDesktopHeader: FC = ({ serviceTab, onServiceTabChange, }) => { + const { onCreateService } = useServicesActions(); + const { onCreateCategory } = useCategoriesActions(); + const getTabActions = () => { switch (serviceTab) { case ServicesTab.DEAL_SERVICE: - return ; case ServicesTab.PRODUCT_SERVICE: - return ; + return ( + + + + Услуга + + + + Категория + + + ); case ServicesTab.SERVICES_KITS: return ; default: @@ -35,8 +52,8 @@ const ServicesDesktopHeader: FC = ({ justify={"space-between"}> {getTabActions()} onServiceTabChange(Number(tab))} + value={serviceTab} + onChange={onServiceTabChange} /> ); diff --git a/src/app/services/components/mobile/ServicesMobileHeader/ServicesMobileHeader.tsx b/src/app/services/components/mobile/ServicesMobileHeader/ServicesMobileHeader.tsx index 7463d15..537e5ae 100644 --- a/src/app/services/components/mobile/ServicesMobileHeader/ServicesMobileHeader.tsx +++ b/src/app/services/components/mobile/ServicesMobileHeader/ServicesMobileHeader.tsx @@ -16,8 +16,8 @@ const ServicesMobileHeader: FC = ({ }) => { return ( onServiceTabChange(Number(tab))} + value={serviceTab} + onChange={onServiceTabChange} w={"100%"} py={"md"} px={"sm"} diff --git a/src/app/services/components/shared/PageBody/PageBody.tsx b/src/app/services/components/shared/PageBody/PageBody.tsx index 63bb566..3a7b6ad 100644 --- a/src/app/services/components/shared/PageBody/PageBody.tsx +++ b/src/app/services/components/shared/PageBody/PageBody.tsx @@ -1,12 +1,15 @@ "use client"; import { useState } from "react"; +import { Stack } from "@mantine/core"; import ServicesDesktopHeader from "@/app/services/components/desktop/ServicesDesktopHeader/ServicesDesktopHeader"; import ServicesMobileHeader from "@/app/services/components/mobile/ServicesMobileHeader/ServicesMobileHeader"; import ServicesKitsTable from "@/app/services/components/shared/ServicesKitTable/ServicesKitTable"; +import ServicesTable from "@/app/services/components/shared/ServicesTable/ServicesTable"; import { ServicesTab } from "@/app/services/components/shared/ServiceTabSegmentedControl/ServiceTabSegmentedControl"; import PageBlock from "@/components/layout/PageBlock/PageBlock"; import useIsMobile from "@/hooks/utils/useIsMobile"; +import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service"; const PageBody = () => { const isMobile = useIsMobile(); @@ -18,9 +21,11 @@ const PageBody = () => { const getPageBody = () => { switch (servicesTab) { case ServicesTab.PRODUCT_SERVICE: - return <>; + return ( + + ); case ServicesTab.DEAL_SERVICE: - return <>; + return ; case ServicesTab.SERVICES_KITS: return ; default: @@ -29,7 +34,7 @@ const PageBody = () => { }; return ( - <> + {!isMobile && ( { /> )} - +
{
- +
); }; diff --git a/src/app/services/components/shared/ServiceCategorySelect/ServiceCategorySelect.tsx b/src/app/services/components/shared/ServiceCategorySelect/ServiceCategorySelect.tsx new file mode 100644 index 0000000..4441749 --- /dev/null +++ b/src/app/services/components/shared/ServiceCategorySelect/ServiceCategorySelect.tsx @@ -0,0 +1,24 @@ +import { FC } from "react"; +import ObjectSelect, { + ObjectSelectProps, +} from "@/components/selects/ObjectSelect/ObjectSelect"; +import { ServiceCategorySchema } from "@/lib/client"; +import useServiceCategoriesList from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServiceCategoriesList"; + +type Props = Omit< + ObjectSelectProps, + "data" | "getLabelFn" | "getValueFn" +>; + +const ServiceCategorySelect: FC = props => { + const { categories } = useServiceCategoriesList(); + + return ( + + ); +}; + +export default ServiceCategorySelect; diff --git a/src/app/services/components/shared/ServiceTabSegmentedControl/ServiceTabSegmentedControl.tsx b/src/app/services/components/shared/ServiceTabSegmentedControl/ServiceTabSegmentedControl.tsx index 4192d95..f29fa19 100644 --- a/src/app/services/components/shared/ServiceTabSegmentedControl/ServiceTabSegmentedControl.tsx +++ b/src/app/services/components/shared/ServiceTabSegmentedControl/ServiceTabSegmentedControl.tsx @@ -1,5 +1,7 @@ import { FC } from "react"; -import { SegmentedControl, SegmentedControlProps } from "@mantine/core"; +import BaseSegmentedControl, { + BaseSegmentedControlProps, +} from "@/components/ui/BaseSegmentedControl/BaseSegmentedControl"; export enum ServicesTab { DEAL_SERVICE, @@ -7,25 +9,25 @@ export enum ServicesTab { SERVICES_KITS, } -type Props = Omit; +type Props = Omit, "data">; const data = [ { label: "Для товара", - value: ServicesTab.PRODUCT_SERVICE.toString(), + value: ServicesTab.PRODUCT_SERVICE, }, { label: "Для сделки", - value: ServicesTab.DEAL_SERVICE.toString(), + value: ServicesTab.DEAL_SERVICE, }, { label: "Наборы услуг", - value: ServicesTab.SERVICES_KITS.toString(), + value: ServicesTab.SERVICES_KITS, }, ]; const ServiceTabSegmentedControl: FC = props => ( - diff --git a/src/app/services/components/shared/ServiceTypeSegmentedControl/ServiceTypeSegmentedControl.tsx b/src/app/services/components/shared/ServiceTypeSegmentedControl/ServiceTypeSegmentedControl.tsx index ceb64ea..059766d 100644 --- a/src/app/services/components/shared/ServiceTypeSegmentedControl/ServiceTypeSegmentedControl.tsx +++ b/src/app/services/components/shared/ServiceTypeSegmentedControl/ServiceTypeSegmentedControl.tsx @@ -1,22 +1,24 @@ import { FC } from "react"; -import { SegmentedControl, SegmentedControlProps } from "@mantine/core"; +import BaseSegmentedControl, { + BaseSegmentedControlProps, +} from "@/components/ui/BaseSegmentedControl/BaseSegmentedControl"; import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service"; -type Props = Omit; +type Props = Omit, "data">; const data = [ { label: "Для сделки", - value: ServiceType.DEAL_SERVICE.toString(), + value: ServiceType.DEAL_SERVICE, }, { label: "Для товара", - value: ServiceType.PRODUCT_SERVICE.toString(), + value: ServiceType.PRODUCT_SERVICE, }, ]; const ServiceTypeSegmentedControl: FC = props => ( - diff --git a/src/app/services/components/shared/ServicesTable/ServicesTable.tsx b/src/app/services/components/shared/ServicesTable/ServicesTable.tsx new file mode 100644 index 0000000..2938e76 --- /dev/null +++ b/src/app/services/components/shared/ServicesTable/ServicesTable.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { FC, useMemo, useState } from "react"; +import useServicesInnerTableColumns from "@/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns"; +import useServicesOuterTableColumns from "@/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns"; +import { GroupedServices } from "@/app/services/components/shared/ServicesTable/types/GroupedServices"; +import { useServicesContext } from "@/app/services/contexts/ServicesContext"; +import BaseTable from "@/components/ui/BaseTable/BaseTable"; +import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service"; + +type Props = { + serviceType: ServiceType; +}; + +const ServicesTable: FC = ({ serviceType }) => { + const { servicesList } = useServicesContext(); + + const [expandedCategoryIds, setExpandedCategoryIds] = useState( + [] + ); + + const innerColumns = useServicesInnerTableColumns(); + const outerColumns = useServicesOuterTableColumns({ + expandedCategoryIds, + setExpandedCategoryIds, + }); + + const groupedServices: GroupedServices[] = useMemo(() => { + const grouped: GroupedServices[] = []; + servicesList.services.forEach(service => { + if (service.serviceType !== serviceType) return; + + const existingGroup = grouped.find( + group => group.category.id === service.category.id + ); + if (existingGroup) { + existingGroup.services.push(service); + } else { + grouped.push({ + category: service.category, + services: [service], + }); + } + }); + return grouped; + }, [servicesList.services, serviceType]); + + return ( + ( + + ), + }} + /> + ); +}; + +export default ServicesTable; diff --git a/src/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns.tsx b/src/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns.tsx new file mode 100644 index 0000000..8d74e29 --- /dev/null +++ b/src/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns.tsx @@ -0,0 +1,46 @@ +import { useMemo } from "react"; +import { DataTableColumn } from "mantine-datatable"; +import { List, Text } from "@mantine/core"; +import { ServiceSchema } from "@/lib/client"; + +const useServicesInnerTableColumns = () => { + const getPriceRow = (service: ServiceSchema) => { + if (service.priceRanges.length === 0) { + return <>{service.price.toLocaleString("ru")}₽; + } + return ( + + {service.priceRanges.map(range => ( + + + {`${range.fromQuantity} - ${range.toQuantity}: ${range.price.toLocaleString("ru")}₽`} + + + ))} + + ); + }; + + return useMemo( + () => + [ + { + accessor: "name", + title: "Название", + }, + { + accessor: "price", + title: "Цена", + render: service => getPriceRow(service), + }, + { + accessor: "cost", + title: "Себестоимость", + render: service => `${service.cost?.toLocaleString("ru")}₽`, + }, + ] as DataTableColumn[], + [] + ); +}; + +export default useServicesInnerTableColumns; diff --git a/src/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns.tsx b/src/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns.tsx new file mode 100644 index 0000000..286d5d2 --- /dev/null +++ b/src/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns.tsx @@ -0,0 +1,72 @@ +import { useMemo } from "react"; +import { + IconChevronDown, + IconChevronsDown, + IconChevronsRight, + IconChevronsUp, + IconChevronUp, +} from "@tabler/icons-react"; +import { DataTableColumn } from "mantine-datatable"; +import { Box, Group, Text } from "@mantine/core"; +import { GroupedServices } from "@/app/services/components/shared/ServicesTable/types/GroupedServices"; +import { useServicesContext } from "@/app/services/contexts/ServicesContext"; + +type Props = { + expandedCategoryIds: number[]; + setExpandedCategoryIds: (ids: number[]) => void; +}; + +const useServicesOuterTableColumns = ({ + expandedCategoryIds, + setExpandedCategoryIds, +}: Props) => { + const { categoriesList } = useServicesContext(); + + const onExpandAllClick = () => { + if (expandedCategoryIds.length !== categoriesList.categories.length) { + setExpandedCategoryIds(categoriesList.categories.map(c => c.id)); + return; + } + setExpandedCategoryIds([]); + }; + + const getExpandAllIcon = () => { + if (expandedCategoryIds.length === categoriesList.categories.length) + return ; + if (expandedCategoryIds.length === 0) return ; + return ; + }; + + return useMemo( + () => + [ + { + accessor: "name", + title: ( + + + {getExpandAllIcon()} + + Категория + + ), + noWrap: true, + render: ({ category: { id, name } }) => ( + + {expandedCategoryIds.includes(id) ? ( + + ) : ( + + )} + {name} + + ), + }, + ] as DataTableColumn[], + [expandedCategoryIds, categoriesList.categories] + ); +}; + +export default useServicesOuterTableColumns; diff --git a/src/app/services/components/shared/ServicesTable/types/GroupedServices.ts b/src/app/services/components/shared/ServicesTable/types/GroupedServices.ts new file mode 100644 index 0000000..ef1a41c --- /dev/null +++ b/src/app/services/components/shared/ServicesTable/types/GroupedServices.ts @@ -0,0 +1,6 @@ +import { ServiceCategorySchema, ServiceSchema } from "@/lib/client"; + +export type GroupedServices = { + category: ServiceCategorySchema; + services: ServiceSchema[]; +}; diff --git a/src/app/services/contexts/ServicesContext.tsx b/src/app/services/contexts/ServicesContext.tsx index 583a94a..0d6a6d1 100644 --- a/src/app/services/contexts/ServicesContext.tsx +++ b/src/app/services/contexts/ServicesContext.tsx @@ -1,6 +1,10 @@ "use client"; import makeContext from "@/lib/contextFactory/contextFactory"; +import { + ServiceCategoriesCrud, + useServiceCategoriesCrud, +} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServiceCategoriesCrud"; import { ServicesCrud, useServicesCrud, @@ -9,6 +13,9 @@ import { ServicesKitsCrud, useServicesKitsCrud, } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServicesKitsCrud"; +import useServiceCategoriesList, { + ServiceCategoriesList, +} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServiceCategoriesList"; import useServicesKitsList, { ServicesKitsList, } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServicesKitsList"; @@ -21,6 +28,8 @@ type ServicesContextState = { servicesCrud: ServicesCrud; servicesKitList: ServicesKitsList; servicesKitCrud: ServicesKitsCrud; + categoriesList: ServiceCategoriesList; + categoriesCrud: ServiceCategoriesCrud; }; const useFulfillmentBaseContextState = (): ServicesContextState => { @@ -30,11 +39,16 @@ const useFulfillmentBaseContextState = (): ServicesContextState => { const servicesKitList = useServicesKitsList(); const servicesKitCrud = useServicesKitsCrud(servicesKitList); + const categoriesList = useServiceCategoriesList(); + const categoriesCrud = useServiceCategoriesCrud(categoriesList); + return { servicesList, servicesCrud, servicesKitList, servicesKitCrud, + categoriesList, + categoriesCrud, }; }; diff --git a/src/app/services/hooks/useCategoriesActions.ts b/src/app/services/hooks/useCategoriesActions.ts new file mode 100644 index 0000000..2d6fc1c --- /dev/null +++ b/src/app/services/hooks/useCategoriesActions.ts @@ -0,0 +1,39 @@ +import { modals } from "@mantine/modals"; +import { useServicesContext } from "@/app/services/contexts/ServicesContext"; +import { ServiceCategorySchema } from "@/lib/client"; + +const useCategoriesActions = () => { + const { categoriesCrud } = useServicesContext(); + + const onChangeCategory = (category: ServiceCategorySchema) => { + modals.openContextModal({ + modal: "serviceCategoryEditorModal", + title: "Создание категории", + withCloseButton: false, + innerProps: { + onChange: value => categoriesCrud.onUpdate(category.id, value), + entity: category, + isEditing: true, + }, + }); + }; + + const onCreateCategory = () => { + modals.openContextModal({ + modal: "serviceCategoryEditorModal", + title: "Создание категории", + withCloseButton: false, + innerProps: { + onCreate: categoriesCrud.onCreate, + isEditing: false, + }, + }); + }; + + return { + onCreateCategory, + onChangeCategory, + }; +}; + +export default useCategoriesActions; diff --git a/src/app/services/hooks/useServicesActions.ts b/src/app/services/hooks/useServicesActions.ts new file mode 100644 index 0000000..5759539 --- /dev/null +++ b/src/app/services/hooks/useServicesActions.ts @@ -0,0 +1,39 @@ +import { modals } from "@mantine/modals"; +import { useServicesContext } from "@/app/services/contexts/ServicesContext"; +import { ServiceSchema } from "@/lib/client"; + +const useServicesActions = () => { + const { servicesCrud } = useServicesContext(); + + const onChangeService = (service: ServiceSchema) => { + modals.openContextModal({ + modal: "serviceEditorModal", + title: "Редактирование услуги", + withCloseButton: false, + innerProps: { + onChange: value => servicesCrud.onUpdate(service.id, value), + entity: service, + isEditing: true, + }, + }); + }; + + const onCreateService = () => { + modals.openContextModal({ + modal: "serviceEditorModal", + title: "Создание услуги", + withCloseButton: false, + innerProps: { + onCreate: servicesCrud.onCreate, + isEditing: false, + }, + }); + }; + + return { + onCreateService, + onChangeService, + }; +}; + +export default useServicesActions; diff --git a/src/app/services/modals/ServiceCategoryEditorModal.tsx b/src/app/services/modals/ServiceCategoryEditorModal.tsx new file mode 100644 index 0000000..baed4db --- /dev/null +++ b/src/app/services/modals/ServiceCategoryEditorModal.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Flex, TextInput } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { ContextModalProps } from "@mantine/modals"; +import { + CreateServiceCategorySchema, + ServiceCategorySchema, + UpdateServiceCategorySchema, +} from "@/lib/client"; +import BaseFormModal, { + CreateEditFormProps, +} from "@/modals/base/BaseFormModal/BaseFormModal"; + +type Props = CreateEditFormProps< + CreateServiceCategorySchema, + UpdateServiceCategorySchema, + ServiceCategorySchema +>; + +const ServiceCategoryEditorModal = ({ + context, + id, + innerProps, +}: ContextModalProps) => { + const initialValues = innerProps.isEditing + ? innerProps.entity + : { + name: "", + dealServiceRank: "", + productServiceRank: "", + }; + + const form = useForm>({ + initialValues, + validate: { + name: name => + (!name || name.trim() === "") && + "Необходимо ввести название категории", + }, + }); + + const onClose = () => context.closeContextModal(id); + + return ( + + + + + + ); +}; + +export default ServiceCategoryEditorModal; diff --git a/src/app/services/modals/ServiceEditorModal/ServiceEditorModal.tsx b/src/app/services/modals/ServiceEditorModal/ServiceEditorModal.tsx new file mode 100644 index 0000000..e1bc919 --- /dev/null +++ b/src/app/services/modals/ServiceEditorModal/ServiceEditorModal.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useState } from "react"; +import { Fieldset, Flex, NumberInput, Stack, TextInput } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { ContextModalProps } from "@mantine/modals"; +import ServiceCategorySelect from "@/app/services/components/shared/ServiceCategorySelect/ServiceCategorySelect"; +import ServiceTypeSegmentedControl from "@/app/services/components/shared/ServiceTypeSegmentedControl/ServiceTypeSegmentedControl"; +import RangePriceInput, { + PriceRangeInputType, +} from "@/app/services/modals/ServiceEditorModal/components/RangePriceInput"; +import ServicePriceTypeSegmentedControl from "@/app/services/modals/ServiceEditorModal/components/ServicePriceTypeSegmentedControl"; +import { + CreateServiceSchema, + ServiceSchema, + UpdateServiceSchema, +} from "@/lib/client"; +import BaseFormModal, { + CreateEditFormProps, +} from "@/modals/base/BaseFormModal/BaseFormModal"; +import { + ServicePriceType, + ServiceType, +} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service"; + +type Props = CreateEditFormProps< + CreateServiceSchema, + UpdateServiceSchema, + ServiceSchema +>; + +const ServiceEditorModal = ({ + context, + id, + innerProps, +}: ContextModalProps) => { + const [priceType, setPriceType] = useState( + ServicePriceType.DEFAULT + ); + + const initialValues = innerProps.isEditing + ? innerProps.entity + : { + name: "", + price: 0, + cost: 0, + serviceType: ServiceType.DEAL_SERVICE, + priceRanges: [], + lexorank: "", + }; + + const form = useForm>({ + initialValues, + validate: { + name: name => + (!name || name.trim() === "") && + "Необходимо ввести название услуги", + category: category => !category && "Необходимо выбрать категорию", + priceRanges: (value, values) => + (!value || value.length === 0) && + (!values.price || values.price <= 0) && + "Необходимо добавить хотя бы один диапазон цен или указать цену за единицу услуги", + price: (value, values) => + (!value || value === 0) && + (!values.priceRanges || values.priceRanges.length === 0) && + "Необходимо добавить хотя бы один диапазон цен или указать цену за единицу услуги", + cost: cost => (!cost || cost < 0) && "Введите себестоимость", + }, + }); + + const getPriceBody = () => { + if (priceType === ServicePriceType.DEFAULT) + return ( + + ); + + return ( + + ); + }; + + const onCancel = () => context.closeContextModal(id); + + return ( + +
+ + + + + + +
+
+ + + {getPriceBody()} + +
+
+ ); +}; + +export default ServiceEditorModal; diff --git a/src/app/services/modals/ServiceEditorModal/components/RangePriceInput.tsx b/src/app/services/modals/ServiceEditorModal/components/RangePriceInput.tsx new file mode 100644 index 0000000..8bd47b8 --- /dev/null +++ b/src/app/services/modals/ServiceEditorModal/components/RangePriceInput.tsx @@ -0,0 +1,105 @@ +import { FC } from "react"; +import { IconTrash } from "@tabler/icons-react"; +import { isNumber } from "lodash"; +import { ActionIcon, Flex, Input, NumberInput, rem } from "@mantine/core"; +import InlineButton from "@/components/ui/InlineButton/InlineButton"; +import { ServicePriceRangeSchema } from "@/lib/client"; +import BaseFormInputProps from "@/utils/baseFormInputProps"; + +export type PriceRangeInputType = BaseFormInputProps; + +const RangePriceInput: FC = props => { + const onAddRange = () => { + const newRange = { + fromQuantity: 0, + toQuantity: 0, + price: 0, + id: null, + }; + props.onChange([...props.value, newRange]); + }; + + const onDeleteRange = (idx: number) => { + const newRanges = props.value.filter((_, i) => i !== idx); + props.onChange(newRanges); + }; + + const onChangeRange = ( + idx: number, + values: Partial + ) => { + const newRanges = props.value.map((item, i) => + i === idx + ? { + ...item, + ...values, + } + : item + ); + props.onChange(newRanges); + }; + + return ( + + + {props.value.map((range, idx) => ( + + onDeleteRange(idx)} + variant={"default"}> + + + + val && + isNumber(val) && + onChangeRange(idx, { fromQuantity: val }) + } + allowNegative={false} + /> + + val && + isNumber(val) && + onChangeRange(idx, { toQuantity: val }) + } + allowNegative={false} + /> + + val && + isNumber(val) && + onChangeRange(idx, { price: val }) + } + allowNegative={false} + /> + + ))} + + + Добавить диапазон + + + + ); +}; +export default RangePriceInput; diff --git a/src/app/services/modals/ServiceEditorModal/components/ServicePriceTypeSegmentedControl.tsx b/src/app/services/modals/ServiceEditorModal/components/ServicePriceTypeSegmentedControl.tsx new file mode 100644 index 0000000..d1b7953 --- /dev/null +++ b/src/app/services/modals/ServiceEditorModal/components/ServicePriceTypeSegmentedControl.tsx @@ -0,0 +1,33 @@ +import { FC } from "react"; +import BaseSegmentedControl, { + BaseSegmentedControlProps, +} from "@/components/ui/BaseSegmentedControl/BaseSegmentedControl"; + +export enum ServicePriceType { + DEFAULT, + BY_RANGE, +} + +type Props = Omit, "data">; + +const ServicePriceTypeSegmentedControl: FC = props => { + const data = [ + { + label: "По умолчанию", + value: ServicePriceType.DEFAULT, + }, + { + label: "По диапазону", + value: ServicePriceType.BY_RANGE, + }, + ]; + + return ( + + ); +}; + +export default ServicePriceTypeSegmentedControl; diff --git a/src/app/services/modals/index.ts b/src/app/services/modals/index.ts index 3449ca5..c06dc64 100644 --- a/src/app/services/modals/index.ts +++ b/src/app/services/modals/index.ts @@ -1 +1,3 @@ export { default as ServicesKitEditorModal } from "@/app/services/modals/ServicesKitEditorModal"; +export { default as ServiceCategoryEditorModal } from "@/app/services/modals/ServiceCategoryEditorModal"; +export { default as ServiceEditorModal } from "@/app/services/modals/ServiceEditorModal/ServiceEditorModal"; diff --git a/src/components/ui/BaseSegmentedControl/BaseSegmentedControl.tsx b/src/components/ui/BaseSegmentedControl/BaseSegmentedControl.tsx new file mode 100644 index 0000000..ba406ed --- /dev/null +++ b/src/components/ui/BaseSegmentedControl/BaseSegmentedControl.tsx @@ -0,0 +1,34 @@ +import { SegmentedControl, SegmentedControlProps } from "@mantine/core"; + +export type BaseSegmentedControlProps = Omit< + SegmentedControlProps, + "onChange" | "value" | "data" +> & { + onChange?: (value: T) => void; + value?: T; + data: { label: string; value: T }[]; +}; + +const BaseSegmentedControl = ( + props: BaseSegmentedControlProps +) => { + const handleChange = (value: string) => { + const numValue = Number(value); + const convertedValue = isNaN(numValue) ? (value as T) : (numValue as T); + props.onChange?.(convertedValue); + }; + + return ( + ({ + ...item, + value: item.value.toString(), + }))} + /> + ); +}; + +export default BaseSegmentedControl; diff --git a/src/lib/client/@tanstack/react-query.gen.ts b/src/lib/client/@tanstack/react-query.gen.ts index c9fb157..2ec42bc 100644 --- a/src/lib/client/@tanstack/react-query.gen.ts +++ b/src/lib/client/@tanstack/react-query.gen.ts @@ -19,6 +19,7 @@ import { createProduct, createProject, createService, + createServiceCategory, createServicesKit, createStatus, deleteBoard, @@ -29,6 +30,7 @@ import { deleteProduct, deleteProject, deleteService, + deleteServiceCategory, deleteServicesKit, deleteStatus, duplicateProductServices, @@ -39,6 +41,7 @@ import { getDealServices, getProducts, getProjects, + getServiceCategories, getServices, getServicesKits, getStatuses, @@ -51,6 +54,7 @@ import { updateProduct, updateProject, updateService, + updateServiceCategory, updateServicesKit, updateStatus, type Options, @@ -83,6 +87,9 @@ import type { CreateProjectData, CreateProjectError, CreateProjectResponse2, + CreateServiceCategoryData, + CreateServiceCategoryError, + CreateServiceCategoryResponse2, CreateServiceData, CreateServiceError, CreateServiceResponse2, @@ -113,6 +120,9 @@ import type { DeleteProjectData, DeleteProjectError, DeleteProjectResponse2, + DeleteServiceCategoryData, + DeleteServiceCategoryError, + DeleteServiceCategoryResponse2, DeleteServiceData, DeleteServiceError, DeleteServiceResponse2, @@ -136,6 +146,7 @@ import type { GetProductsError, GetProductsResponse2, GetProjectsData, + GetServiceCategoriesData, GetServicesData, GetServicesKitsData, GetStatusesData, @@ -161,6 +172,9 @@ import type { UpdateProjectData, UpdateProjectError, UpdateProjectResponse2, + UpdateServiceCategoryData, + UpdateServiceCategoryError, + UpdateServiceCategoryResponse2, UpdateServiceData, UpdateServiceError, UpdateServiceResponse2, @@ -1644,6 +1658,135 @@ export const updateServiceMutation = ( return mutationOptions; }; +export const getServiceCategoriesQueryKey = ( + options?: Options +) => createQueryKey("getServiceCategories", options); + +/** + * Get Services Categories + */ +export const getServiceCategoriesOptions = ( + options?: Options +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getServiceCategories({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getServiceCategoriesQueryKey(options), + }); +}; + +export const createServiceCategoryQueryKey = ( + options: Options +) => createQueryKey("createServiceCategory", options); + +/** + * Create Service Category + */ +export const createServiceCategoryOptions = ( + options: Options +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await createServiceCategory({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: createServiceCategoryQueryKey(options), + }); +}; + +/** + * Create Service Category + */ +export const createServiceCategoryMutation = ( + options?: Partial> +): UseMutationOptions< + CreateServiceCategoryResponse2, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + CreateServiceCategoryResponse2, + AxiosError, + Options + > = { + mutationFn: async localOptions => { + const { data } = await createServiceCategory({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * Delete Service Category + */ +export const deleteServiceCategoryMutation = ( + options?: Partial> +): UseMutationOptions< + DeleteServiceCategoryResponse2, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + DeleteServiceCategoryResponse2, + AxiosError, + Options + > = { + mutationFn: async localOptions => { + const { data } = await deleteServiceCategory({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * Update Service Category + */ +export const updateServiceCategoryMutation = ( + options?: Partial> +): UseMutationOptions< + UpdateServiceCategoryResponse2, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + UpdateServiceCategoryResponse2, + AxiosError, + Options + > = { + mutationFn: async localOptions => { + const { data } = await updateServiceCategory({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + export const getServicesKitsQueryKey = ( options?: Options ) => createQueryKey("getServicesKits", options); diff --git a/src/lib/client/sdk.gen.ts b/src/lib/client/sdk.gen.ts index 681a281..095c809 100644 --- a/src/lib/client/sdk.gen.ts +++ b/src/lib/client/sdk.gen.ts @@ -30,6 +30,9 @@ import type { CreateProjectData, CreateProjectErrors, CreateProjectResponses, + CreateServiceCategoryData, + CreateServiceCategoryErrors, + CreateServiceCategoryResponses, CreateServiceData, CreateServiceErrors, CreateServiceResponses, @@ -60,6 +63,9 @@ import type { DeleteProjectData, DeleteProjectErrors, DeleteProjectResponses, + DeleteServiceCategoryData, + DeleteServiceCategoryErrors, + DeleteServiceCategoryResponses, DeleteServiceData, DeleteServiceErrors, DeleteServiceResponses, @@ -91,6 +97,8 @@ import type { GetProductsResponses, GetProjectsData, GetProjectsResponses, + GetServiceCategoriesData, + GetServiceCategoriesResponses, GetServicesData, GetServicesKitsData, GetServicesKitsResponses, @@ -122,6 +130,9 @@ import type { UpdateProjectData, UpdateProjectErrors, UpdateProjectResponses, + UpdateServiceCategoryData, + UpdateServiceCategoryErrors, + UpdateServiceCategoryResponses, UpdateServiceData, UpdateServiceErrors, UpdateServiceResponses, @@ -151,6 +162,8 @@ import { zCreateProductResponse2, zCreateProjectData, zCreateProjectResponse2, + zCreateServiceCategoryData, + zCreateServiceCategoryResponse2, zCreateServiceData, zCreateServiceResponse2, zCreateServicesKitData, @@ -171,6 +184,8 @@ import { zDeleteProductResponse2, zDeleteProjectData, zDeleteProjectResponse2, + zDeleteServiceCategoryData, + zDeleteServiceCategoryResponse2, zDeleteServiceData, zDeleteServiceResponse2, zDeleteServicesKitData, @@ -193,6 +208,8 @@ import { zGetProductsResponse2, zGetProjectsData, zGetProjectsResponse2, + zGetServiceCategoriesData, + zGetServiceCategoriesResponse2, zGetServicesData, zGetServicesKitsData, zGetServicesKitsResponse, @@ -215,6 +232,8 @@ import { zUpdateProductResponse2, zUpdateProjectData, zUpdateProjectResponse2, + zUpdateServiceCategoryData, + zUpdateServiceCategoryResponse2, zUpdateServiceData, zUpdateServiceResponse2, zUpdateServicesKitData, @@ -1244,6 +1263,106 @@ export const updateService = ( }); }; +/** + * Get Services Categories + */ +export const getServiceCategories = ( + options?: Options +) => { + return (options?.client ?? _heyApiClient).get< + GetServiceCategoriesResponses, + unknown, + ThrowOnError + >({ + requestValidator: async data => { + return await zGetServiceCategoriesData.parseAsync(data); + }, + responseType: "json", + responseValidator: async data => { + return await zGetServiceCategoriesResponse2.parseAsync(data); + }, + url: "/modules/fulfillment-base/service-category/", + ...options, + }); +}; + +/** + * Create Service Category + */ +export const createServiceCategory = ( + options: Options +) => { + return (options.client ?? _heyApiClient).post< + CreateServiceCategoryResponses, + CreateServiceCategoryErrors, + ThrowOnError + >({ + requestValidator: async data => { + return await zCreateServiceCategoryData.parseAsync(data); + }, + responseType: "json", + responseValidator: async data => { + return await zCreateServiceCategoryResponse2.parseAsync(data); + }, + url: "/modules/fulfillment-base/service-category/", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }); +}; + +/** + * Delete Service Category + */ +export const deleteServiceCategory = ( + options: Options +) => { + return (options.client ?? _heyApiClient).delete< + DeleteServiceCategoryResponses, + DeleteServiceCategoryErrors, + ThrowOnError + >({ + requestValidator: async data => { + return await zDeleteServiceCategoryData.parseAsync(data); + }, + responseType: "json", + responseValidator: async data => { + return await zDeleteServiceCategoryResponse2.parseAsync(data); + }, + url: "/modules/fulfillment-base/service-category/{pk}", + ...options, + }); +}; + +/** + * Update Service Category + */ +export const updateServiceCategory = ( + options: Options +) => { + return (options.client ?? _heyApiClient).patch< + UpdateServiceCategoryResponses, + UpdateServiceCategoryErrors, + ThrowOnError + >({ + requestValidator: async data => { + return await zUpdateServiceCategoryData.parseAsync(data); + }, + responseType: "json", + responseValidator: async data => { + return await zUpdateServiceCategoryResponse2.parseAsync(data); + }, + url: "/modules/fulfillment-base/service-category/{pk}", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }); +}; + /** * Get Services Kits */ diff --git a/src/lib/client/types.gen.ts b/src/lib/client/types.gen.ts index fa2f604..6b0f0b0 100644 --- a/src/lib/client/types.gen.ts +++ b/src/lib/client/types.gen.ts @@ -388,6 +388,42 @@ export type CreateProjectSchema = { name: string; }; +/** + * CreateServiceCategoryRequest + */ +export type CreateServiceCategoryRequest = { + entity: CreateServiceCategorySchema; +}; + +/** + * CreateServiceCategoryResponse + */ +export type CreateServiceCategoryResponse = { + /** + * Message + */ + message: string; + entity: ServiceCategorySchema; +}; + +/** + * CreateServiceCategorySchema + */ +export type CreateServiceCategorySchema = { + /** + * Name + */ + name: string; + /** + * Dealservicerank + */ + dealServiceRank: string; + /** + * Productservicerank + */ + productServiceRank: string; +}; + /** * CreateServiceRequest */ @@ -410,10 +446,6 @@ export type CreateServiceResponse = { * CreateServiceSchema */ export type CreateServiceSchema = { - /** - * Id - */ - id: number; /** * Name */ @@ -713,6 +745,16 @@ export type DeleteProjectResponse = { message: string; }; +/** + * DeleteServiceCategoryResponse + */ +export type DeleteServiceCategoryResponse = { + /** + * Message + */ + message: string; +}; + /** * DeleteServiceResponse */ @@ -814,6 +856,16 @@ export type GetProjectsResponse = { items: Array; }; +/** + * GetServiceCategoriesResponse + */ +export type GetServiceCategoriesResponse = { + /** + * Items + */ + items: Array; +}; + /** * GetServicesKitResponse */ @@ -1015,10 +1067,6 @@ export type ProjectSchema = { * ServiceCategorySchema */ export type ServiceCategorySchema = { - /** - * Id - */ - id: number; /** * Name */ @@ -1031,6 +1079,10 @@ export type ServiceCategorySchema = { * Productservicerank */ productServiceRank: string; + /** + * Id + */ + id: number; }; /** @@ -1059,10 +1111,6 @@ export type ServicePriceRangeSchema = { * ServiceSchema */ export type ServiceSchema = { - /** - * Id - */ - id: number; /** * Name */ @@ -1088,6 +1136,10 @@ export type ServiceSchema = { * Lexorank */ lexorank: string; + /** + * Id + */ + id: number; }; /** @@ -1409,6 +1461,45 @@ export type UpdateProjectSchema = { builtInModules?: Array; }; +/** + * UpdateServiceCategoryRequest + */ +export type UpdateServiceCategoryRequest = { + entity: UpdateServiceCategorySchema; +}; + +/** + * UpdateServiceCategoryResponse + */ +export type UpdateServiceCategoryResponse = { + /** + * Message + */ + message: string; +}; + +/** + * UpdateServiceCategorySchema + */ +export type UpdateServiceCategorySchema = { + /** + * Name + */ + name: string; + /** + * Dealservicerank + */ + dealServiceRank: string; + /** + * Productservicerank + */ + productServiceRank: string; + /** + * Id + */ + id: number; +}; + /** * UpdateServiceRequest */ @@ -1430,10 +1521,6 @@ export type UpdateServiceResponse = { * UpdateServiceSchema */ export type UpdateServiceSchema = { - /** - * Id - */ - id: number; /** * Name */ @@ -1459,6 +1546,10 @@ export type UpdateServiceSchema = { * Lexorank */ lexorank: string; + /** + * Id + */ + id: number; }; /** @@ -2767,6 +2858,114 @@ export type UpdateServiceResponses = { export type UpdateServiceResponse2 = UpdateServiceResponses[keyof UpdateServiceResponses]; +export type GetServiceCategoriesData = { + body?: never; + path?: never; + query?: never; + url: "/modules/fulfillment-base/service-category/"; +}; + +export type GetServiceCategoriesResponses = { + /** + * Successful Response + */ + 200: GetServiceCategoriesResponse; +}; + +export type GetServiceCategoriesResponse2 = + GetServiceCategoriesResponses[keyof GetServiceCategoriesResponses]; + +export type CreateServiceCategoryData = { + body: CreateServiceCategoryRequest; + path?: never; + query?: never; + url: "/modules/fulfillment-base/service-category/"; +}; + +export type CreateServiceCategoryErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateServiceCategoryError = + CreateServiceCategoryErrors[keyof CreateServiceCategoryErrors]; + +export type CreateServiceCategoryResponses = { + /** + * Successful Response + */ + 200: CreateServiceCategoryResponse; +}; + +export type CreateServiceCategoryResponse2 = + CreateServiceCategoryResponses[keyof CreateServiceCategoryResponses]; + +export type DeleteServiceCategoryData = { + body?: never; + path: { + /** + * Pk + */ + pk: number; + }; + query?: never; + url: "/modules/fulfillment-base/service-category/{pk}"; +}; + +export type DeleteServiceCategoryErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteServiceCategoryError = + DeleteServiceCategoryErrors[keyof DeleteServiceCategoryErrors]; + +export type DeleteServiceCategoryResponses = { + /** + * Successful Response + */ + 200: DeleteServiceCategoryResponse; +}; + +export type DeleteServiceCategoryResponse2 = + DeleteServiceCategoryResponses[keyof DeleteServiceCategoryResponses]; + +export type UpdateServiceCategoryData = { + body: UpdateServiceCategoryRequest; + path: { + /** + * Pk + */ + pk: number; + }; + query?: never; + url: "/modules/fulfillment-base/service-category/{pk}"; +}; + +export type UpdateServiceCategoryErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateServiceCategoryError = + UpdateServiceCategoryErrors[keyof UpdateServiceCategoryErrors]; + +export type UpdateServiceCategoryResponses = { + /** + * Successful Response + */ + 200: UpdateServiceCategoryResponse; +}; + +export type UpdateServiceCategoryResponse2 = + UpdateServiceCategoryResponses[keyof UpdateServiceCategoryResponses]; + export type GetServicesKitsData = { body?: never; path?: never; diff --git a/src/lib/client/zod.gen.ts b/src/lib/client/zod.gen.ts index 8957a33..974bc4e 100644 --- a/src/lib/client/zod.gen.ts +++ b/src/lib/client/zod.gen.ts @@ -119,10 +119,10 @@ export const zProductSchema = z.object({ * ServiceCategorySchema */ export const zServiceCategorySchema = z.object({ - id: z.int(), name: z.string(), dealServiceRank: z.string(), productServiceRank: z.string(), + id: z.int(), }); /** @@ -139,7 +139,6 @@ export const zServicePriceRangeSchema = z.object({ * ServiceSchema */ export const zServiceSchema = z.object({ - id: z.int(), name: z.string(), category: zServiceCategorySchema, price: z.number(), @@ -147,6 +146,7 @@ export const zServiceSchema = z.object({ priceRanges: z.array(zServicePriceRangeSchema), cost: z.union([z.number(), z.null()]), lexorank: z.string(), + id: z.int(), }); /** @@ -351,11 +351,34 @@ export const zCreateProjectResponse = z.object({ entity: zProjectSchema, }); +/** + * CreateServiceCategorySchema + */ +export const zCreateServiceCategorySchema = z.object({ + name: z.string(), + dealServiceRank: z.string(), + productServiceRank: z.string(), +}); + +/** + * CreateServiceCategoryRequest + */ +export const zCreateServiceCategoryRequest = z.object({ + entity: zCreateServiceCategorySchema, +}); + +/** + * CreateServiceCategoryResponse + */ +export const zCreateServiceCategoryResponse = z.object({ + message: z.string(), + entity: zServiceCategorySchema, +}); + /** * CreateServiceSchema */ export const zCreateServiceSchema = z.object({ - id: z.int(), name: z.string(), category: zServiceCategorySchema, price: z.number(), @@ -518,6 +541,13 @@ export const zDeleteProjectResponse = z.object({ message: z.string(), }); +/** + * DeleteServiceCategoryResponse + */ +export const zDeleteServiceCategoryResponse = z.object({ + message: z.string(), +}); + /** * DeleteServiceResponse */ @@ -597,6 +627,13 @@ export const zGetProjectsResponse = z.object({ items: z.array(zProjectSchema), }); +/** + * GetServiceCategoriesResponse + */ +export const zGetServiceCategoriesResponse = z.object({ + items: z.array(zServiceCategorySchema), +}); + /** * GetServicesKitResponse */ @@ -845,11 +882,34 @@ export const zUpdateProjectResponse = z.object({ message: z.string(), }); +/** + * UpdateServiceCategorySchema + */ +export const zUpdateServiceCategorySchema = z.object({ + name: z.string(), + dealServiceRank: z.string(), + productServiceRank: z.string(), + id: z.int(), +}); + +/** + * UpdateServiceCategoryRequest + */ +export const zUpdateServiceCategoryRequest = z.object({ + entity: zUpdateServiceCategorySchema, +}); + +/** + * UpdateServiceCategoryResponse + */ +export const zUpdateServiceCategoryResponse = z.object({ + message: z.string(), +}); + /** * UpdateServiceSchema */ export const zUpdateServiceSchema = z.object({ - id: z.int(), name: z.string(), category: zServiceCategorySchema, price: z.number(), @@ -857,6 +917,7 @@ export const zUpdateServiceSchema = z.object({ priceRanges: z.array(zServicePriceRangeSchema), cost: z.union([z.number(), z.null()]), lexorank: z.string(), + id: z.int(), }); /** @@ -1431,6 +1492,54 @@ export const zUpdateServiceData = z.object({ */ export const zUpdateServiceResponse2 = zUpdateServiceResponse; +export const zGetServiceCategoriesData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.never()), +}); + +/** + * Successful Response + */ +export const zGetServiceCategoriesResponse2 = zGetServiceCategoriesResponse; + +export const zCreateServiceCategoryData = z.object({ + body: zCreateServiceCategoryRequest, + path: z.optional(z.never()), + query: z.optional(z.never()), +}); + +/** + * Successful Response + */ +export const zCreateServiceCategoryResponse2 = zCreateServiceCategoryResponse; + +export const zDeleteServiceCategoryData = z.object({ + body: z.optional(z.never()), + path: z.object({ + pk: z.int(), + }), + query: z.optional(z.never()), +}); + +/** + * Successful Response + */ +export const zDeleteServiceCategoryResponse2 = zDeleteServiceCategoryResponse; + +export const zUpdateServiceCategoryData = z.object({ + body: zUpdateServiceCategoryRequest, + path: z.object({ + pk: z.int(), + }), + query: z.optional(z.never()), +}); + +/** + * Successful Response + */ +export const zUpdateServiceCategoryResponse2 = zUpdateServiceCategoryResponse; + export const zGetServicesKitsData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), diff --git a/src/modals/modals.ts b/src/modals/modals.ts index 7be14d6..d8d778e 100644 --- a/src/modals/modals.ts +++ b/src/modals/modals.ts @@ -1,7 +1,7 @@ import DealsBoardFiltersModal from "@/app/deals/modals/DealsBoardFiltersModal/DealsBoardFiltersModal"; import DealsScheduleFiltersModal from "@/app/deals/modals/DealsScheduleFiltersModal/DealsScheduleFiltersModal"; import DealsTableFiltersModal from "@/app/deals/modals/DealsTableFiltersModal/DealsTableFiltersModal"; -import { ServicesKitEditorModal } from "@/app/services/modals"; +import { ServiceCategoryEditorModal, ServiceEditorModal, ServicesKitEditorModal } from "@/app/services/modals"; import EnterNameModal from "@/modals/EnterNameModal/EnterNameModal"; import { DealProductEditorModal, @@ -24,4 +24,6 @@ export const modals = { duplicateServicesModal: DuplicateServicesModal, servicesKitSelectModal: ServicesKitSelectModal, servicesKitEditorModal: ServicesKitEditorModal, + serviceCategoryEditorModal: ServiceCategoryEditorModal, + serviceEditorModal: ServiceEditorModal, }; diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServiceCategoriesCrud.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServiceCategoriesCrud.tsx new file mode 100644 index 0000000..5d2a9af --- /dev/null +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServiceCategoriesCrud.tsx @@ -0,0 +1,84 @@ +import { LexoRank } from "lexorank"; +import { useCrudOperations } from "@/hooks/cruds/baseCrud"; +import { + CreateServiceCategorySchema, + ServiceCategorySchema, + UpdateServiceCategorySchema, +} from "@/lib/client"; +import { + createServiceCategoryMutation, + deleteServiceCategoryMutation, + updateServiceCategoryMutation, +} from "@/lib/client/@tanstack/react-query.gen"; +import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service"; +import { getMaxLexorankByKey, getNewLexorank } from "@/utils/lexorank"; + +type UseServiceCategoryProps = { + queryKey: any[]; + categories: ServiceCategorySchema[]; +}; + +export type ServiceCategoriesCrud = { + onCreate: (category: CreateServiceCategorySchema) => void; + onUpdate: ( + categoryId: number, + category: UpdateServiceCategorySchema, + onSuccess?: () => void + ) => void; + onDelete: (service: ServiceCategorySchema) => void; +}; + +export const useServiceCategoriesCrud = ({ + queryKey, + categories, +}: UseServiceCategoryProps): ServiceCategoriesCrud => { + const getCategoryRank = (serviceType: ServiceType): string => { + const lastCategory = getMaxLexorankByKey( + categories, + serviceType === ServiceType.DEAL_SERVICE + ? "dealServiceRank" + : "productServiceRank" + ); + + if (!lastCategory) return LexoRank.middle().toString(); + + const rank = + serviceType === ServiceType.DEAL_SERVICE + ? lastCategory.dealServiceRank + : lastCategory?.productServiceRank; + + return getNewLexorank(LexoRank.parse(rank)).toString(); + }; + + return useCrudOperations< + ServiceCategorySchema, + UpdateServiceCategorySchema, + CreateServiceCategorySchema + >({ + key: "getServiceCategories", + queryKey, + mutations: { + create: createServiceCategoryMutation(), + update: updateServiceCategoryMutation(), + delete: deleteServiceCategoryMutation(), + }, + getCreateEntity: category => { + const dealServiceRank = getCategoryRank(ServiceType.DEAL_SERVICE); + const productServiceRank = getCategoryRank( + ServiceType.PRODUCT_SERVICE + ); + + return { + dealServiceRank, + productServiceRank, + name: category.name!, + }; + }, + getUpdateEntity: (old, update) => + ({ + ...old, + ...update, + }) as ServiceCategorySchema, + getDeleteConfirmTitle: () => "Удаление категории услуг", + }); +}; diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServicesCrud.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServicesCrud.tsx index f9cc773..7f69a2f 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServicesCrud.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServicesCrud.tsx @@ -1,3 +1,4 @@ +import { LexoRank } from "lexorank"; import { useCrudOperations } from "@/hooks/cruds/baseCrud"; import { CreateServiceSchema, @@ -9,9 +10,11 @@ import { deleteServiceMutation, updateServiceMutation, } from "@/lib/client/@tanstack/react-query.gen"; +import { getMaxByLexorank, getNewLexorank } from "@/utils/lexorank"; type UseServicesProps = { queryKey: any[]; + services: ServiceSchema[]; }; export type ServicesCrud = { @@ -26,6 +29,7 @@ export type ServicesCrud = { export const useServicesCrud = ({ queryKey, + services, }: UseServicesProps): ServicesCrud => { return useCrudOperations< ServiceSchema, @@ -39,6 +43,15 @@ export const useServicesCrud = ({ update: updateServiceMutation(), delete: deleteServiceMutation(), }, + getCreateEntity: service => { + const maxRankStr = getMaxByLexorank(services)?.lexorank; + const maxRank = maxRankStr ? LexoRank.parse(maxRankStr) : null; + + return { + ...(service as CreateServiceSchema), + lexorank: getNewLexorank(maxRank).toString(), + }; + }, getUpdateEntity: (old, update) => ({ ...old, diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServiceCategoriesList.ts b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServiceCategoriesList.ts new file mode 100644 index 0000000..32eda5c --- /dev/null +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServiceCategoriesList.ts @@ -0,0 +1,43 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { ServiceCategorySchema } from "@/lib/client"; +import { + getServiceCategoriesOptions, + getServiceCategoriesQueryKey, +} from "@/lib/client/@tanstack/react-query.gen"; + +export type ServiceCategoriesList = { + categories: ServiceCategorySchema[]; + setCategories: (categories: ServiceCategorySchema[]) => void; + refetch: () => void; + queryKey: any[]; + isLoading: boolean; +}; + +const useServiceCategoriesList = (): ServiceCategoriesList => { + const queryClient = useQueryClient(); + const { data, refetch, isLoading } = useQuery( + getServiceCategoriesOptions() + ); + + const queryKey = getServiceCategoriesQueryKey(); + + const setCategories = (categories: ServiceCategorySchema[]) => { + queryClient.setQueryData( + queryKey, + (old: { items: ServiceCategorySchema[] }) => ({ + ...old, + items: categories, + }) + ); + }; + + return { + categories: data?.items ?? [], + setCategories, + refetch, + queryKey, + isLoading, + }; +}; + +export default useServiceCategoriesList; diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service.ts b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service.ts index c7ce22a..a98d1a2 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service.ts +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service.ts @@ -2,3 +2,8 @@ export enum ServiceType { DEAL_SERVICE, PRODUCT_SERVICE, } + +export enum ServicePriceType { + DEFAULT, + BY_RANGE, +} diff --git a/src/utils/lexorank.ts b/src/utils/lexorank.ts index 6b70ae1..854679b 100644 --- a/src/utils/lexorank.ts +++ b/src/utils/lexorank.ts @@ -30,6 +30,13 @@ export function getMaxByLexorank( ); } +export function getMaxLexorankByKey(items: T[], key: keyof T): T | null { + return items.reduce( + (max, item) => (!max || item[key] > max[key] ? item : max), + null as T | null + ); +} + export function getNewLexorank( left?: LexoRank | null, right?: LexoRank | null