feat: actions for services and categories

This commit is contained in:
2025-09-28 12:46:57 +04:00
parent 47533ad7f5
commit 61f0a9069b
12 changed files with 178 additions and 88 deletions

View File

@ -43,7 +43,9 @@ const PageBody = () => {
/> />
</PageBlock> </PageBlock>
)} )}
<PageBlock style={{ flex: 1, minHeight: 0 }}> <PageBlock
style={{ flex: 1, minHeight: 0 }}
fullScreenMobile>
<div <div
style={{ style={{
height: "100%", height: "100%",

View File

@ -1,9 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable"; import { DataTableColumn } from "mantine-datatable";
import { Flex } from "@mantine/core"; import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ServicesKitSchema } from "@/lib/client"; import { ServicesKitSchema } from "@/lib/client";
import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service"; import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service";
@ -13,8 +10,6 @@ type Props = {
}; };
const useServicesKitsTableColumns = ({ onDelete, onChange }: Props) => { const useServicesKitsTableColumns = ({ onDelete, onChange }: Props) => {
const isMobile = useIsMobile();
return useMemo( return useMemo(
() => () =>
[ [
@ -25,19 +20,10 @@ const useServicesKitsTableColumns = ({ onDelete, onChange }: Props) => {
textAlign: "center", textAlign: "center",
width: "0%", width: "0%",
render: kit => ( render: kit => (
<Flex gap={isMobile ? "xs" : "md"}> <UpdateDeleteTableActions
<ActionIconWithTip onDelete={() => onDelete(kit)}
onClick={() => onChange(kit)} onChange={() => onChange(kit)}
tipLabel={"Редактировать"}> />
<IconEdit />
</ActionIconWithTip>
<ActionIconWithTip
color={"red"}
onClick={() => onDelete(kit)}
tipLabel={"Удалить"}>
<IconTrash />
</ActionIconWithTip>
</Flex>
), ),
}, },
{ {

View File

@ -6,6 +6,7 @@ import useServicesOuterTableColumns from "@/app/services/components/shared/Servi
import { GroupedServices } from "@/app/services/components/shared/ServicesTable/types/GroupedServices"; import { GroupedServices } from "@/app/services/components/shared/ServicesTable/types/GroupedServices";
import { useServicesContext } from "@/app/services/contexts/ServicesContext"; import { useServicesContext } from "@/app/services/contexts/ServicesContext";
import BaseTable from "@/components/ui/BaseTable/BaseTable"; import BaseTable from "@/components/ui/BaseTable/BaseTable";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service"; import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service";
type Props = { type Props = {
@ -14,6 +15,7 @@ type Props = {
const ServicesTable: FC<Props> = ({ serviceType }) => { const ServicesTable: FC<Props> = ({ serviceType }) => {
const { servicesList } = useServicesContext(); const { servicesList } = useServicesContext();
const isMobile = useIsMobile();
const [expandedCategoryIds, setExpandedCategoryIds] = useState<number[]>( const [expandedCategoryIds, setExpandedCategoryIds] = useState<number[]>(
[] []
@ -53,6 +55,9 @@ const ServicesTable: FC<Props> = ({ serviceType }) => {
verticalSpacing={"md"} verticalSpacing={"md"}
groups={undefined} groups={undefined}
idAccessor={"category.id"} idAccessor={"category.id"}
styles={{
header: { zIndex: 1000 },
}}
rowExpansion={{ rowExpansion={{
allowMultiple: true, allowMultiple: true,
expanded: { expanded: {
@ -69,6 +74,9 @@ const ServicesTable: FC<Props> = ({ serviceType }) => {
/> />
), ),
}} }}
style={{
marginInline: isMobile ? "var(--mantine-spacing-xs)" : "0",
}}
/> />
); );
}; };

View File

@ -1,9 +1,19 @@
"use client";
import { useMemo } from "react"; import { useMemo } from "react";
import { DataTableColumn } from "mantine-datatable"; import { DataTableColumn } from "mantine-datatable";
import { List, Text } from "@mantine/core"; import { List, Text } from "@mantine/core";
import { useServicesContext } from "@/app/services/contexts/ServicesContext";
import useServicesActions from "@/app/services/hooks/useServicesActions";
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ServiceSchema } from "@/lib/client"; import { ServiceSchema } from "@/lib/client";
const useServicesInnerTableColumns = () => { const useServicesInnerTableColumns = () => {
const { onChangeService } = useServicesActions();
const { servicesCrud } = useServicesContext();
const isMobile = useIsMobile();
const getPriceRow = (service: ServiceSchema) => { const getPriceRow = (service: ServiceSchema) => {
if (service.priceRanges.length === 0) { if (service.priceRanges.length === 0) {
return <>{service.price.toLocaleString("ru")}</>; return <>{service.price.toLocaleString("ru")}</>;
@ -35,11 +45,25 @@ const useServicesInnerTableColumns = () => {
}, },
{ {
accessor: "cost", accessor: "cost",
title: "Себестоимость", title: isMobile ? "Себестоим." : "Себестоимость",
render: service => `${service.cost?.toLocaleString("ru")}`, render: service => `${service.cost?.toLocaleString("ru")}`,
}, },
{
accessor: "actions",
title: isMobile ? "" : "Действия",
sortable: false,
textAlign: "center",
width: "0%",
render: service => (
<UpdateDeleteTableActions
onDelete={() => servicesCrud.onDelete(service)}
onChange={() => onChangeService(service)}
dotsForMobile
/>
),
},
] as DataTableColumn<ServiceSchema>[], ] as DataTableColumn<ServiceSchema>[],
[] [isMobile]
); );
}; };

View File

@ -10,6 +10,8 @@ import { DataTableColumn } from "mantine-datatable";
import { Box, Group, Text } from "@mantine/core"; import { Box, Group, Text } from "@mantine/core";
import { GroupedServices } from "@/app/services/components/shared/ServicesTable/types/GroupedServices"; import { GroupedServices } from "@/app/services/components/shared/ServicesTable/types/GroupedServices";
import { useServicesContext } from "@/app/services/contexts/ServicesContext"; import { useServicesContext } from "@/app/services/contexts/ServicesContext";
import useCategoriesActions from "@/app/services/hooks/useCategoriesActions";
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
type Props = { type Props = {
expandedCategoryIds: number[]; expandedCategoryIds: number[];
@ -20,18 +22,19 @@ const useServicesOuterTableColumns = ({
expandedCategoryIds, expandedCategoryIds,
setExpandedCategoryIds, setExpandedCategoryIds,
}: Props) => { }: Props) => {
const { categoriesList } = useServicesContext(); const { onChangeCategory } = useCategoriesActions();
const { categoriesCrud, categories } = useServicesContext();
const onExpandAllClick = () => { const onExpandAllClick = () => {
if (expandedCategoryIds.length !== categoriesList.categories.length) { if (expandedCategoryIds.length !== categories.length) {
setExpandedCategoryIds(categoriesList.categories.map(c => c.id)); setExpandedCategoryIds(categories.map(c => c.id));
return; return;
} }
setExpandedCategoryIds([]); setExpandedCategoryIds([]);
}; };
const getExpandAllIcon = () => { const getExpandAllIcon = () => {
if (expandedCategoryIds.length === categoriesList.categories.length) if (expandedCategoryIds.length === categories.length)
return <IconChevronsUp />; return <IconChevronsUp />;
if (expandedCategoryIds.length === 0) return <IconChevronsDown />; if (expandedCategoryIds.length === 0) return <IconChevronsDown />;
return <IconChevronsRight />; return <IconChevronsRight />;
@ -64,8 +67,21 @@ const useServicesOuterTableColumns = ({
</Group> </Group>
), ),
}, },
{
accessor: "actions",
title: "Действия",
sortable: false,
textAlign: "center",
width: "0%",
render: ({ category }) => (
<UpdateDeleteTableActions
onDelete={() => categoriesCrud.onDelete(category)}
onChange={() => onChangeCategory(category)}
/>
),
},
] as DataTableColumn<GroupedServices>[], ] as DataTableColumn<GroupedServices>[],
[expandedCategoryIds, categoriesList.categories] [expandedCategoryIds, categories]
); );
}; };

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { ServiceCategorySchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory"; import makeContext from "@/lib/contextFactory/contextFactory";
import { import {
ServiceCategoriesCrud, ServiceCategoriesCrud,
@ -13,9 +14,6 @@ import {
ServicesKitsCrud, ServicesKitsCrud,
useServicesKitsCrud, useServicesKitsCrud,
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServicesKitsCrud"; } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useServicesKitsCrud";
import useServiceCategoriesList, {
ServiceCategoriesList,
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServiceCategoriesList";
import useServicesKitsList, { import useServicesKitsList, {
ServicesKitsList, ServicesKitsList,
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServicesKitsList"; } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useServicesKitsList";
@ -28,8 +26,8 @@ type ServicesContextState = {
servicesCrud: ServicesCrud; servicesCrud: ServicesCrud;
servicesKitList: ServicesKitsList; servicesKitList: ServicesKitsList;
servicesKitCrud: ServicesKitsCrud; servicesKitCrud: ServicesKitsCrud;
categoriesList: ServiceCategoriesList;
categoriesCrud: ServiceCategoriesCrud; categoriesCrud: ServiceCategoriesCrud;
categories: ServiceCategorySchema[];
}; };
const useFulfillmentBaseContextState = (): ServicesContextState => { const useFulfillmentBaseContextState = (): ServicesContextState => {
@ -39,16 +37,19 @@ const useFulfillmentBaseContextState = (): ServicesContextState => {
const servicesKitList = useServicesKitsList(); const servicesKitList = useServicesKitsList();
const servicesKitCrud = useServicesKitsCrud(servicesKitList); const servicesKitCrud = useServicesKitsCrud(servicesKitList);
const categoriesList = useServiceCategoriesList(); const categories = [...new Set(servicesList.services.map(s => s.category))];
const categoriesCrud = useServiceCategoriesCrud(categoriesList); const categoriesCrud = useServiceCategoriesCrud({
queryKey: servicesList.queryKey,
categories,
});
return { return {
servicesList, servicesList,
servicesCrud, servicesCrud,
servicesKitList, servicesKitList,
servicesKitCrud, servicesKitCrud,
categoriesList,
categoriesCrud, categoriesCrud,
categories,
}; };
}; };

View File

@ -1,7 +1,8 @@
import { FC } from "react"; import { FC } from "react";
import { IconTrash } from "@tabler/icons-react"; import { IconTrash } from "@tabler/icons-react";
import { isNumber } from "lodash"; import { isNumber } from "lodash";
import { ActionIcon, Flex, Input, NumberInput, rem } from "@mantine/core"; import { Flex, Input, NumberInput, rem } from "@mantine/core";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import InlineButton from "@/components/ui/InlineButton/InlineButton"; import InlineButton from "@/components/ui/InlineButton/InlineButton";
import { ServicePriceRangeSchema } from "@/lib/client"; import { ServicePriceRangeSchema } from "@/lib/client";
import BaseFormInputProps from "@/utils/baseFormInputProps"; import BaseFormInputProps from "@/utils/baseFormInputProps";
@ -51,11 +52,9 @@ const RangePriceInput: FC<PriceRangeInputType> = props => {
key={idx} key={idx}
gap={rem(10)} gap={rem(10)}
align={"flex-end"}> align={"flex-end"}>
<ActionIcon <ActionIconWithTip onClick={() => onDeleteRange(idx)}>
onClick={() => onDeleteRange(idx)}
variant={"default"}>
<IconTrash /> <IconTrash />
</ActionIcon> </ActionIconWithTip>
<NumberInput <NumberInput
label={"От количества"} label={"От количества"}
placeholder={"От"} placeholder={"От"}

View File

@ -0,0 +1,84 @@
import React, { CSSProperties, FC } from "react";
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
import { Box, Flex, Group, Menu, Text } from "@mantine/core";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
import useIsMobile from "@/hooks/utils/useIsMobile";
type Props = {
onChange: () => void;
onDelete: () => void;
dotsForMobile?: boolean;
style?: CSSProperties;
};
const UpdateDeleteTableActions: FC<Props> = ({
onChange,
onDelete,
style,
dotsForMobile = false,
}) => {
const isMobile = useIsMobile();
if (dotsForMobile && isMobile) {
return (
<Menu>
<Menu.Target>
<Box onClick={e => e.stopPropagation()}>
<ThemeIcon size={"sm"}>
<IconDotsVertical />
</ThemeIcon>
</Box>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={e => {
e.stopPropagation();
onChange();
}}>
<Group wrap={"nowrap"}>
<IconEdit />
<Text>Редактировать</Text>
</Group>
</Menu.Item>
<Menu.Item
onClick={e => {
e.stopPropagation();
onDelete();
}}>
<Group wrap={"nowrap"}>
<IconTrash />
<Text>Удалить</Text>
</Group>
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
}
return (
<Flex
gap={isMobile ? "xs" : "md"}
style={style}>
<ActionIconWithTip
onClick={e => {
e.stopPropagation();
onChange();
}}
tipLabel={"Редактировать"}>
<IconEdit />
</ActionIconWithTip>
<ActionIconWithTip
color={"red"}
onClick={e => {
e.stopPropagation();
onDelete();
}}
tipLabel={"Удалить"}>
<IconTrash />
</ActionIconWithTip>
</Flex>
);
};
export default UpdateDeleteTableActions;

View File

@ -1,8 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable"; import { DataTableColumn } from "mantine-datatable";
import { Box, Flex, Text } from "@mantine/core"; import { Box, Text } from "@mantine/core";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip"; import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
import { ProductServiceSchema } from "@/lib/client"; import { ProductServiceSchema } from "@/lib/client";
type Props = { type Props = {
@ -32,21 +31,11 @@ const useProductServicesTableColumns = ({
textAlign: "center", textAlign: "center",
width: "0%", width: "0%",
render: dealProductService => ( render: dealProductService => (
<Flex <UpdateDeleteTableActions
gap="md" onDelete={() => onDelete(dealProductService)}
px={"sm"} onChange={() => onChange(dealProductService)}
my={"sm"}> style={{ padding: "var(--mantine-spacing-xs)" }}
<ActionIconWithTip />
tipLabel={"Удалить услугу"}
onClick={() => onDelete(dealProductService)}>
<IconTrash />
</ActionIconWithTip>
<ActionIconWithTip
tipLabel="Редактировать"
onClick={() => onChange(dealProductService)}>
<IconEdit />
</ActionIconWithTip>
</Flex>
), ),
}, },
{ {

View File

@ -1,7 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable"; import { DataTableColumn } from "mantine-datatable";
import { ActionIcon, Flex } from "@mantine/core"; import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
import { DealServiceSchema } from "@/lib/client"; import { DealServiceSchema } from "@/lib/client";
type Props = { type Props = {
@ -36,19 +35,10 @@ const useDealServicesTableColumns = ({ onChange, onDelete }: Props) => {
textAlign: "center", textAlign: "center",
width: "0%", width: "0%",
render: dealService => ( render: dealService => (
<Flex gap={"sm"}> <UpdateDeleteTableActions
<ActionIcon onDelete={() => onDelete(dealService)}
variant={"subtle"} onChange={() => onChange(dealService)}
c={"red"} />
onClick={() => onDelete(dealService)}>
<IconTrash />
</ActionIcon>
<ActionIcon
variant={"subtle"}
onClick={() => onChange(dealService)}>
<IconEdit />
</ActionIcon>
</Flex>
), ),
}, },
] as DataTableColumn<DealServiceSchema>[], ] as DataTableColumn<DealServiceSchema>[],

View File

@ -1,7 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable"; import { DataTableColumn } from "mantine-datatable";
import { ActionIcon, Flex, Text } from "@mantine/core"; import { Text } from "@mantine/core";
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
import { ProductServiceSchema } from "@/lib/client"; import { ProductServiceSchema } from "@/lib/client";
type Props = { type Props = {
@ -31,19 +31,10 @@ const useProductServicesTableColumns = ({
textAlign: "center", textAlign: "center",
width: "0%", width: "0%",
render: dealProductService => ( render: dealProductService => (
<Flex gap="md"> <UpdateDeleteTableActions
<ActionIcon onDelete={() => onDelete(dealProductService)}
variant={"subtle"} onChange={() => onChange(dealProductService)}
color={"red"} />
onClick={() => onDelete(dealProductService)}>
<IconTrash />
</ActionIcon>
<ActionIcon
variant={"subtle"}
onClick={() => onChange(dealProductService)}>
<IconEdit />
</ActionIcon>
</Flex>
), ),
}, },
{ {

View File

@ -55,7 +55,7 @@ export const useServiceCategoriesCrud = ({
UpdateServiceCategorySchema, UpdateServiceCategorySchema,
CreateServiceCategorySchema CreateServiceCategorySchema
>({ >({
key: "getServiceCategories", key: "getServices",
queryKey, queryKey,
mutations: { mutations: {
create: createServiceCategoryMutation(), create: createServiceCategoryMutation(),