feat: services table, base segmented control
This commit is contained in:
@ -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>
|
||||
);
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -0,0 +1,6 @@
|
||||
import { ServiceCategorySchema, ServiceSchema } from "@/lib/client";
|
||||
|
||||
export type GroupedServices = {
|
||||
category: ServiceCategorySchema;
|
||||
services: ServiceSchema[];
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
39
src/app/services/hooks/useCategoriesActions.ts
Normal file
39
src/app/services/hooks/useCategoriesActions.ts
Normal 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;
|
||||
39
src/app/services/hooks/useServicesActions.ts
Normal file
39
src/app/services/hooks/useServicesActions.ts
Normal 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;
|
||||
64
src/app/services/modals/ServiceCategoryEditorModal.tsx
Normal file
64
src/app/services/modals/ServiceCategoryEditorModal.tsx
Normal 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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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";
|
||||
|
||||
Reference in New Issue
Block a user