feat: services table, base segmented control

This commit is contained in:
2025-09-27 18:24:22 +04:00
parent 14140826a7
commit 47533ad7f5
29 changed files with 1489 additions and 44 deletions

View File

@ -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<Props> = ({
serviceTab,
onServiceTabChange,
}) => {
const { onCreateService } = useServicesActions();
const { onCreateCategory } = useCategoriesActions();
const getTabActions = () => {
switch (serviceTab) {
case ServicesTab.DEAL_SERVICE:
return <Box />;
case ServicesTab.PRODUCT_SERVICE:
return <Box />;
return (
<Group>
<InlineButton onClick={onCreateService}>
<IconPlus />
<Text>Услуга</Text>
</InlineButton>
<InlineButton onClick={onCreateCategory}>
<IconPlus />
<Text>Категория</Text>
</InlineButton>
</Group>
);
case ServicesTab.SERVICES_KITS:
return <CreateServiceKitButton />;
default:
@ -35,8 +52,8 @@ const ServicesDesktopHeader: FC<Props> = ({
justify={"space-between"}>
{getTabActions()}
<ServiceTabSegmentedControl
value={serviceTab.toString()}
onChange={tab => onServiceTabChange(Number(tab))}
value={serviceTab}
onChange={onServiceTabChange}
/>
</Group>
);

View File

@ -16,8 +16,8 @@ const ServicesMobileHeader: FC<Props> = ({
}) => {
return (
<ServiceTabSegmentedControl
value={serviceTab.toString()}
onChange={tab => onServiceTabChange(Number(tab))}
value={serviceTab}
onChange={onServiceTabChange}
w={"100%"}
py={"md"}
px={"sm"}

View File

@ -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 (
<ServicesTable serviceType={ServiceType.PRODUCT_SERVICE} />
);
case ServicesTab.DEAL_SERVICE:
return <></>;
return <ServicesTable serviceType={ServiceType.DEAL_SERVICE} />;
case ServicesTab.SERVICES_KITS:
return <ServicesKitsTable />;
default:
@ -29,7 +34,7 @@ const PageBody = () => {
};
return (
<>
<Stack h={"100%"}>
{!isMobile && (
<PageBlock>
<ServicesDesktopHeader
@ -38,7 +43,7 @@ const PageBody = () => {
/>
</PageBlock>
)}
<PageBlock fullHeight>
<PageBlock style={{ flex: 1, minHeight: 0 }}>
<div
style={{
height: "100%",
@ -56,7 +61,7 @@ const PageBody = () => {
</div>
</div>
</PageBlock>
</>
</Stack>
);
};

View File

@ -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<ServiceCategorySchema | null>,
"data" | "getLabelFn" | "getValueFn"
>;
const ServiceCategorySelect: FC<Props> = props => {
const { categories } = useServiceCategoriesList();
return (
<ObjectSelect
data={categories}
{...props}
/>
);
};
export default ServiceCategorySelect;

View File

@ -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<SegmentedControlProps, "data">;
type Props = Omit<BaseSegmentedControlProps<ServicesTab>, "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> = props => (
<SegmentedControl
<BaseSegmentedControl
data={data}
{...props}
/>

View File

@ -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<SegmentedControlProps, "data">;
type Props = Omit<BaseSegmentedControlProps<ServiceType>, "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> = props => (
<SegmentedControl
<BaseSegmentedControl
data={data}
{...props}
/>

View File

@ -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<Props> = ({ serviceType }) => {
const { servicesList } = useServicesContext();
const [expandedCategoryIds, setExpandedCategoryIds] = useState<number[]>(
[]
);
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 (
<BaseTable
withTableBorder
columns={outerColumns}
records={groupedServices}
verticalSpacing={"md"}
groups={undefined}
idAccessor={"category.id"}
rowExpansion={{
allowMultiple: true,
expanded: {
recordIds: expandedCategoryIds,
onRecordIdsChange: setExpandedCategoryIds,
},
content: ({ record }) => (
<BaseTable
withTableBorder
columns={innerColumns}
records={record.services}
verticalSpacing={"md"}
groups={undefined}
/>
),
}}
/>
);
};
export default ServicesTable;

View File

@ -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 (
<List>
{service.priceRanges.map(range => (
<List.Item key={range.id}>
<Text>
{`${range.fromQuantity} - ${range.toQuantity}: ${range.price.toLocaleString("ru")}`}
</Text>
</List.Item>
))}
</List>
);
};
return useMemo(
() =>
[
{
accessor: "name",
title: "Название",
},
{
accessor: "price",
title: "Цена",
render: service => getPriceRow(service),
},
{
accessor: "cost",
title: "Себестоимость",
render: service => `${service.cost?.toLocaleString("ru")}`,
},
] as DataTableColumn<ServiceSchema>[],
[]
);
};
export default useServicesInnerTableColumns;

View File

@ -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 <IconChevronsUp />;
if (expandedCategoryIds.length === 0) return <IconChevronsDown />;
return <IconChevronsRight />;
};
return useMemo(
() =>
[
{
accessor: "name",
title: (
<Group>
<Box
style={{ cursor: "pointer" }}
onClick={onExpandAllClick}>
{getExpandAllIcon()}
</Box>
Категория
</Group>
),
noWrap: true,
render: ({ category: { id, name } }) => (
<Group key={id}>
{expandedCategoryIds.includes(id) ? (
<IconChevronUp />
) : (
<IconChevronDown />
)}
<Text>{name}</Text>
</Group>
),
},
] as DataTableColumn<GroupedServices>[],
[expandedCategoryIds, categoriesList.categories]
);
};
export default useServicesOuterTableColumns;

View File

@ -0,0 +1,6 @@
import { ServiceCategorySchema, ServiceSchema } from "@/lib/client";
export type GroupedServices = {
category: ServiceCategorySchema;
services: ServiceSchema[];
};

View File

@ -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,
};
};

View File

@ -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;

View File

@ -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;

View File

@ -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<Props>) => {
const initialValues = innerProps.isEditing
? innerProps.entity
: {
name: "",
dealServiceRank: "",
productServiceRank: "",
};
const form = useForm<Partial<ServiceCategorySchema>>({
initialValues,
validate: {
name: name =>
(!name || name.trim() === "") &&
"Необходимо ввести название категории",
},
});
const onClose = () => context.closeContextModal(id);
return (
<BaseFormModal
{...innerProps}
form={form}
closeOnSubmit
onClose={onClose}>
<Flex
gap={"xs"}
direction={"column"}>
<TextInput
placeholder={"Введите название категори"}
label={"Название категории"}
{...form.getInputProps("name")}
/>
</Flex>
</BaseFormModal>
);
};
export default ServiceCategoryEditorModal;

View File

@ -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<Props>) => {
const [priceType, setPriceType] = useState<ServicePriceType>(
ServicePriceType.DEFAULT
);
const initialValues = innerProps.isEditing
? innerProps.entity
: {
name: "",
price: 0,
cost: 0,
serviceType: ServiceType.DEAL_SERVICE,
priceRanges: [],
lexorank: "",
};
const form = useForm<Partial<ServiceSchema>>({
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 (
<NumberInput
placeholder={"Введите стоимость услуги"}
label={"Cтоимость услуги"}
hideControls
{...form.getInputProps("price")}
/>
);
return (
<RangePriceInput
{...(form.getInputProps("priceRanges") as PriceRangeInputType)}
/>
);
};
const onCancel = () => context.closeContextModal(id);
return (
<BaseFormModal
{...innerProps}
closeOnSubmit
form={form}
onClose={onCancel}>
<Fieldset
legend={"Общие параметры"}
bdrs={"lg"}>
<Stack>
<ServiceCategorySelect
placeholder={"Выберите категорию"}
label={"Категория услуги"}
{...form.getInputProps("category")}
/>
<TextInput
placeholder={"Введите название услуги"}
label={"Название услуги"}
{...form.getInputProps("name")}
/>
<NumberInput
placeholder={"Введите себестоимость услуги"}
label={"Себестоимость услуги"}
hideControls
{...form.getInputProps("cost")}
allowNegative={false}
/>
<ServiceTypeSegmentedControl
{...form.getInputProps("serviceType")}
/>
</Stack>
</Fieldset>
<Fieldset
legend={"Стоимость"}
bdrs={"lg"}>
<Flex
direction={"column"}
gap={"xs"}
justify={"center"}>
<ServicePriceTypeSegmentedControl
value={priceType}
onChange={setPriceType}
/>
{getPriceBody()}
</Flex>
</Fieldset>
</BaseFormModal>
);
};
export default ServiceEditorModal;

View File

@ -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<ServicePriceRangeSchema[]>;
const RangePriceInput: FC<PriceRangeInputType> = 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<ServicePriceRangeSchema>
) => {
const newRanges = props.value.map((item, i) =>
i === idx
? {
...item,
...values,
}
: item
);
props.onChange(newRanges);
};
return (
<Input.Wrapper
error={props.error}
label={"Диапазон цен"}>
<Flex
direction={"column"}
gap={"xs"}>
{props.value.map((range, idx) => (
<Flex
key={idx}
gap={rem(10)}
align={"flex-end"}>
<ActionIcon
onClick={() => onDeleteRange(idx)}
variant={"default"}>
<IconTrash />
</ActionIcon>
<NumberInput
label={"От количества"}
placeholder={"От"}
hideControls
value={range.fromQuantity}
onChange={val =>
val &&
isNumber(val) &&
onChangeRange(idx, { fromQuantity: val })
}
allowNegative={false}
/>
<NumberInput
label={"До количества"}
placeholder={"До"}
hideControls
value={range.toQuantity}
onChange={val =>
val &&
isNumber(val) &&
onChangeRange(idx, { toQuantity: val })
}
allowNegative={false}
/>
<NumberInput
label={"Цена"}
placeholder={"Цена"}
hideControls
value={range.price}
onChange={val =>
val &&
isNumber(val) &&
onChangeRange(idx, { price: val })
}
allowNegative={false}
/>
</Flex>
))}
<InlineButton onClick={onAddRange}>
Добавить диапазон
</InlineButton>
</Flex>
</Input.Wrapper>
);
};
export default RangePriceInput;

View File

@ -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<BaseSegmentedControlProps<ServicePriceType>, "data">;
const ServicePriceTypeSegmentedControl: FC<Props> = props => {
const data = [
{
label: "По умолчанию",
value: ServicePriceType.DEFAULT,
},
{
label: "По диапазону",
value: ServicePriceType.BY_RANGE,
},
];
return (
<BaseSegmentedControl
data={data}
{...props}
/>
);
};
export default ServicePriceTypeSegmentedControl;

View File

@ -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";

View File

@ -0,0 +1,34 @@
import { SegmentedControl, SegmentedControlProps } from "@mantine/core";
export type BaseSegmentedControlProps<T> = Omit<
SegmentedControlProps,
"onChange" | "value" | "data"
> & {
onChange?: (value: T) => void;
value?: T;
data: { label: string; value: T }[];
};
const BaseSegmentedControl = <T extends string | number>(
props: BaseSegmentedControlProps<T>
) => {
const handleChange = (value: string) => {
const numValue = Number(value);
const convertedValue = isNaN(numValue) ? (value as T) : (numValue as T);
props.onChange?.(convertedValue);
};
return (
<SegmentedControl
{...props}
onChange={handleChange}
value={String(props.value)}
data={props.data.map(item => ({
...item,
value: item.value.toString(),
}))}
/>
);
};
export default BaseSegmentedControl;

View File

@ -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<GetServiceCategoriesData>
) => createQueryKey("getServiceCategories", options);
/**
* Get Services Categories
*/
export const getServiceCategoriesOptions = (
options?: Options<GetServiceCategoriesData>
) => {
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<CreateServiceCategoryData>
) => createQueryKey("createServiceCategory", options);
/**
* Create Service Category
*/
export const createServiceCategoryOptions = (
options: Options<CreateServiceCategoryData>
) => {
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<Options<CreateServiceCategoryData>>
): UseMutationOptions<
CreateServiceCategoryResponse2,
AxiosError<CreateServiceCategoryError>,
Options<CreateServiceCategoryData>
> => {
const mutationOptions: UseMutationOptions<
CreateServiceCategoryResponse2,
AxiosError<CreateServiceCategoryError>,
Options<CreateServiceCategoryData>
> = {
mutationFn: async localOptions => {
const { data } = await createServiceCategory({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
/**
* Delete Service Category
*/
export const deleteServiceCategoryMutation = (
options?: Partial<Options<DeleteServiceCategoryData>>
): UseMutationOptions<
DeleteServiceCategoryResponse2,
AxiosError<DeleteServiceCategoryError>,
Options<DeleteServiceCategoryData>
> => {
const mutationOptions: UseMutationOptions<
DeleteServiceCategoryResponse2,
AxiosError<DeleteServiceCategoryError>,
Options<DeleteServiceCategoryData>
> = {
mutationFn: async localOptions => {
const { data } = await deleteServiceCategory({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
/**
* Update Service Category
*/
export const updateServiceCategoryMutation = (
options?: Partial<Options<UpdateServiceCategoryData>>
): UseMutationOptions<
UpdateServiceCategoryResponse2,
AxiosError<UpdateServiceCategoryError>,
Options<UpdateServiceCategoryData>
> => {
const mutationOptions: UseMutationOptions<
UpdateServiceCategoryResponse2,
AxiosError<UpdateServiceCategoryError>,
Options<UpdateServiceCategoryData>
> = {
mutationFn: async localOptions => {
const { data } = await updateServiceCategory({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
export const getServicesKitsQueryKey = (
options?: Options<GetServicesKitsData>
) => createQueryKey("getServicesKits", options);

View File

@ -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 = <ThrowOnError extends boolean = false>(
});
};
/**
* Get Services Categories
*/
export const getServiceCategories = <ThrowOnError extends boolean = false>(
options?: Options<GetServiceCategoriesData, ThrowOnError>
) => {
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 = <ThrowOnError extends boolean = false>(
options: Options<CreateServiceCategoryData, ThrowOnError>
) => {
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 = <ThrowOnError extends boolean = false>(
options: Options<DeleteServiceCategoryData, ThrowOnError>
) => {
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 = <ThrowOnError extends boolean = false>(
options: Options<UpdateServiceCategoryData, ThrowOnError>
) => {
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
*/

View File

@ -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<ProjectSchema>;
};
/**
* GetServiceCategoriesResponse
*/
export type GetServiceCategoriesResponse = {
/**
* Items
*/
items: Array<ServiceCategorySchema>;
};
/**
* 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<BuiltInModuleSchemaInput>;
};
/**
* 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;

View File

@ -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()),

View File

@ -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,
};

View File

@ -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: () => "Удаление категории услуг",
});
};

View File

@ -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,

View File

@ -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;

View File

@ -2,3 +2,8 @@ export enum ServiceType {
DEAL_SERVICE,
PRODUCT_SERVICE,
}
export enum ServicePriceType {
DEFAULT,
BY_RANGE,
}

View File

@ -30,6 +30,13 @@ export function getMaxByLexorank<T extends LexorankSortable>(
);
}
export function getMaxLexorankByKey<T>(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