feat: services kits table in service page

This commit is contained in:
2025-09-25 09:36:22 +04:00
parent a83328492e
commit 14140826a7
28 changed files with 641 additions and 32 deletions

View File

@ -15,6 +15,8 @@
--dark-thick-shadow: 1px 1px 13px 1px var(--mantine-color-dark-5);
/* Heights */
--page-height: calc(100vh - (var(--mantine-spacing-md) * 2));
--mobile-footer-height: 70px;
--mobile-page-height: calc(100vh - var(--mobile-footer-height));
}
body {

View File

@ -0,0 +1,27 @@
import { FC } from "react";
import { ButtonProps } from "@mantine/core";
import { modals } from "@mantine/modals";
import { useServicesContext } from "@/app/services/contexts/ServicesContext";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
type Props = ButtonProps;
const CreateServiceKitButton: FC<Props> = () => {
const { servicesKitCrud } = useServicesContext();
const onCreateClick = () => {
modals.openContextModal({
modal: "servicesKitEditorModal",
title: "Создание набора услуг",
withCloseButton: false,
innerProps: {
onCreate: servicesKitCrud.onCreate,
isEditing: false,
},
});
};
return <InlineButton onClick={onCreateClick}>Создать набор</InlineButton>;
};
export default CreateServiceKitButton;

View File

@ -0,0 +1,45 @@
"use client";
import { FC } from "react";
import { Box, Group } from "@mantine/core";
import CreateServiceKitButton from "@/app/services/components/desktop/CreateServiceKitButton/CreateServiceKitButton";
import ServiceTabSegmentedControl, {
ServicesTab,
} from "@/app/services/components/shared/ServiceTabSegmentedControl/ServiceTabSegmentedControl";
type Props = {
serviceTab: ServicesTab;
onServiceTabChange: (serviceTab: ServicesTab) => void;
};
const ServicesDesktopHeader: FC<Props> = ({
serviceTab,
onServiceTabChange,
}) => {
const getTabActions = () => {
switch (serviceTab) {
case ServicesTab.DEAL_SERVICE:
return <Box />;
case ServicesTab.PRODUCT_SERVICE:
return <Box />;
case ServicesTab.SERVICES_KITS:
return <CreateServiceKitButton />;
default:
return <Box />;
}
};
return (
<Group
wrap={"nowrap"}
justify={"space-between"}>
{getTabActions()}
<ServiceTabSegmentedControl
value={serviceTab.toString()}
onChange={tab => onServiceTabChange(Number(tab))}
/>
</Group>
);
};
export default ServicesDesktopHeader;

View File

@ -0,0 +1,28 @@
"use client";
import { FC } from "react";
import ServiceTabSegmentedControl, {
ServicesTab,
} from "@/app/services/components/shared/ServiceTabSegmentedControl/ServiceTabSegmentedControl";
type Props = {
serviceTab: ServicesTab;
onServiceTabChange: (serviceTab: ServicesTab) => void;
};
const ServicesMobileHeader: FC<Props> = ({
serviceTab,
onServiceTabChange,
}) => {
return (
<ServiceTabSegmentedControl
value={serviceTab.toString()}
onChange={tab => onServiceTabChange(Number(tab))}
w={"100%"}
py={"md"}
px={"sm"}
/>
);
};
export default ServicesMobileHeader;

View File

@ -0,0 +1,63 @@
"use client";
import { useState } from "react";
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 { ServicesTab } from "@/app/services/components/shared/ServiceTabSegmentedControl/ServiceTabSegmentedControl";
import PageBlock from "@/components/layout/PageBlock/PageBlock";
import useIsMobile from "@/hooks/utils/useIsMobile";
const PageBody = () => {
const isMobile = useIsMobile();
const [servicesTab, setServicesTab] = useState<ServicesTab>(
ServicesTab.PRODUCT_SERVICE
);
const getPageBody = () => {
switch (servicesTab) {
case ServicesTab.PRODUCT_SERVICE:
return <></>;
case ServicesTab.DEAL_SERVICE:
return <></>;
case ServicesTab.SERVICES_KITS:
return <ServicesKitsTable />;
default:
return <>-</>;
}
};
return (
<>
{!isMobile && (
<PageBlock>
<ServicesDesktopHeader
serviceTab={servicesTab}
onServiceTabChange={setServicesTab}
/>
</PageBlock>
)}
<PageBlock fullHeight>
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
}}>
{isMobile && (
<ServicesMobileHeader
serviceTab={servicesTab}
onServiceTabChange={setServicesTab}
/>
)}
<div style={{ flex: 1, overflow: "auto" }}>
{getPageBody()}
</div>
</div>
</PageBlock>
</>
);
};
export default PageBody;

View File

@ -0,0 +1,34 @@
import { FC } from "react";
import { SegmentedControl, SegmentedControlProps } from "@mantine/core";
export enum ServicesTab {
DEAL_SERVICE,
PRODUCT_SERVICE,
SERVICES_KITS,
}
type Props = Omit<SegmentedControlProps, "data">;
const data = [
{
label: "Для товара",
value: ServicesTab.PRODUCT_SERVICE.toString(),
},
{
label: "Для сделки",
value: ServicesTab.DEAL_SERVICE.toString(),
},
{
label: "Наборы услуг",
value: ServicesTab.SERVICES_KITS.toString(),
},
];
const ServiceTabSegmentedControl: FC<Props> = props => (
<SegmentedControl
data={data}
{...props}
/>
);
export default ServiceTabSegmentedControl;

View File

@ -0,0 +1,25 @@
import { FC } from "react";
import { SegmentedControl, SegmentedControlProps } from "@mantine/core";
import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service";
type Props = Omit<SegmentedControlProps, "data">;
const data = [
{
label: "Для сделки",
value: ServiceType.DEAL_SERVICE.toString(),
},
{
label: "Для товара",
value: ServiceType.PRODUCT_SERVICE.toString(),
},
];
const ServiceTypeSegmentedControl: FC<Props> = props => (
<SegmentedControl
data={data}
{...props}
/>
);
export default ServiceTypeSegmentedControl;

View File

@ -0,0 +1,42 @@
import { modals } from "@mantine/modals";
import useServicesKitsTableColumns from "@/app/services/components/shared/ServicesKitTable/hooks/useServicesKitsTableColumns";
import { useServicesContext } from "@/app/services/contexts/ServicesContext";
import BaseTable from "@/components/ui/BaseTable/BaseTable";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ServicesKitSchema } from "@/lib/client";
const ServicesKitsTable = () => {
const { servicesKitCrud, servicesKitList } = useServicesContext();
const isMobile = useIsMobile();
const onChange = (kit: ServicesKitSchema) => {
modals.openContextModal({
modal: "servicesKitEditorModal",
title: "Редактирование набора услуг",
withCloseButton: false,
innerProps: {
entity: kit,
onChange: value => servicesKitCrud.onUpdate(kit.id, value),
isEditing: true,
},
});
};
const columns = useServicesKitsTableColumns({
onChange,
onDelete: servicesKitCrud.onDelete,
});
return (
<BaseTable
records={servicesKitList.servicesKits}
columns={columns}
groups={undefined}
withTableBorder
verticalSpacing={"xs"}
mx={isMobile ? "xs" : 0}
/>
);
};
export default ServicesKitsTable;

View File

@ -0,0 +1,68 @@
import { useMemo } from "react";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable";
import { Flex } from "@mantine/core";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ServicesKitSchema } from "@/lib/client";
import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service";
type Props = {
onChange: (kit: ServicesKitSchema) => void;
onDelete: (kit: ServicesKitSchema) => void;
};
const useServicesKitsTableColumns = ({ onDelete, onChange }: Props) => {
const isMobile = useIsMobile();
return useMemo(
() =>
[
{
accessor: "actions",
title: "Действия",
sortable: false,
textAlign: "center",
width: "0%",
render: kit => (
<Flex gap={isMobile ? "xs" : "md"}>
<ActionIconWithTip
onClick={() => onChange(kit)}
tipLabel={"Редактировать"}>
<IconEdit />
</ActionIconWithTip>
<ActionIconWithTip
color={"red"}
onClick={() => onDelete(kit)}
tipLabel={"Удалить"}>
<IconTrash />
</ActionIconWithTip>
</Flex>
),
},
{
accessor: "name",
title: "Название",
width: "60%",
},
{
title: "Кол-во услуг",
accessor: "services",
render: kit => kit.services.length,
width: "20%",
},
{
title: "Тип набора",
accessor: "serviceType",
render: kit =>
kit.serviceType === ServiceType.DEAL_SERVICE
? "Для сделок"
: "Для товаров",
width: "20%",
},
] as DataTableColumn<ServicesKitSchema>[],
[]
);
};
export default useServicesKitsTableColumns;

View File

@ -0,0 +1,24 @@
"use client";
import { FC } from "react";
import ObjectMultiSelect, {
ObjectMultiSelectProps,
} from "@/components/selects/ObjectMultiSelect/ObjectMultiSelect";
import { ServiceSchema } from "@/lib/client";
import useServicesList from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServicesList";
type Props = Omit<ObjectMultiSelectProps<ServiceSchema>, "data">;
const ServicesMultiselect: FC<Props> = (props: Props) => {
const { services } = useServicesList();
return (
<ObjectMultiSelect
data={services}
searchable
{...props}
/>
);
};
export default ServicesMultiselect;

View File

@ -0,0 +1,45 @@
"use client";
import makeContext from "@/lib/contextFactory/contextFactory";
import {
ServicesCrud,
useServicesCrud,
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServicesCrud";
import {
ServicesKitsCrud,
useServicesKitsCrud,
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServicesKitsCrud";
import useServicesKitsList, {
ServicesKitsList,
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServicesKitsList";
import useServicesList, {
ServicesList,
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServicesList";
type ServicesContextState = {
servicesList: ServicesList;
servicesCrud: ServicesCrud;
servicesKitList: ServicesKitsList;
servicesKitCrud: ServicesKitsCrud;
};
const useFulfillmentBaseContextState = (): ServicesContextState => {
const servicesList = useServicesList();
const servicesCrud = useServicesCrud(servicesList);
const servicesKitList = useServicesKitsList();
const servicesKitCrud = useServicesKitsCrud(servicesKitList);
return {
servicesList,
servicesCrud,
servicesKitList,
servicesKitCrud,
};
};
export const [ServicesContextProvider, useServicesContext] =
makeContext<ServicesContextState>(
useFulfillmentBaseContextState,
"Services"
);

View File

@ -0,0 +1,70 @@
"use client";
import { TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import ServicesMultiselect from "@/app/services/components/shared/ServicesMultiselect/ServicesMultiselect";
import ServiceTypeSegmentedControl from "@/app/services/components/shared/ServiceTypeSegmentedControl/ServiceTypeSegmentedControl";
import { ServicesKitSchema } from "@/lib/client";
import BaseFormModal, {
CreateEditFormProps,
} from "@/modals/base/BaseFormModal/BaseFormModal";
import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service";
type Props = CreateEditFormProps<ServicesKitSchema>;
const ServicesKitEditorModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const initialValues: Partial<ServicesKitSchema> = innerProps.isEditing
? innerProps.entity
: {
name: "",
serviceType: ServiceType.DEAL_SERVICE,
services: [],
};
const form = useForm<Partial<ServicesKitSchema>>({
initialValues,
validate: {
name: name => !name && "Введите название",
services: services =>
(!services || services.length === 0) && "Выберите услуги",
},
});
return (
<BaseFormModal
{...innerProps}
form={form}
closeOnSubmit
onClose={() => context.closeContextModal(id)}>
<TextInput
label={"Название"}
placeholder={"Введите название набора услуг"}
{...form.getInputProps("name")}
/>
<ServiceTypeSegmentedControl
value={form.values.serviceType?.toString()}
onChange={tab => {
form.setFieldValue("serviceType", Number(tab));
form.setFieldValue("services", []);
}}
mt={"xs"}
/>
<ServicesMultiselect
label={"Услуги"}
placeholder={"Выберите услуги"}
filterBy={service =>
service.serviceType === form.values.serviceType
}
groupBy={service => service.category.name}
{...form.getInputProps("services")}
/>
</BaseFormModal>
);
};
export default ServicesKitEditorModal;

View File

@ -0,0 +1 @@
export { default as ServicesKitEditorModal } from "@/app/services/modals/ServicesKitEditorModal";

22
src/app/services/page.tsx Normal file
View File

@ -0,0 +1,22 @@
import { Suspense } from "react";
import { Center, Loader } from "@mantine/core";
import PageBody from "@/app/services/components/shared/PageBody/PageBody";
import { ServicesContextProvider } from "@/app/services/contexts/ServicesContext";
import PageContainer from "@/components/layout/PageContainer/PageContainer";
export default async function ServicesPage() {
return (
<Suspense
fallback={
<Center h="50vh">
<Loader size="lg" />
</Center>
}>
<ServicesContextProvider>
<PageContainer>
<PageBody />
</PageContainer>
</ServicesContextProvider>
</Suspense>
);
}