feat: crappy reordering

This commit is contained in:
2024-10-07 23:42:01 +03:00
parent ae0d4ff6f0
commit 223ed0577a
13 changed files with 290 additions and 66 deletions

View File

@@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --force",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview", "preview": "vite preview",
@@ -15,7 +15,7 @@
"@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^17.0.0",
"@mantine/core": "^7.11.2", "@mantine/core": "^7.11.2",
"@mantine/dates": "^7.11.2", "@mantine/dates": "^7.11.2",
"@mantine/dropzone": "^7.11.2", "@mantine/dropzone": "^7.11.2",

View File

@@ -182,6 +182,8 @@ export type { ProductUpdateResponse } from './models/ProductUpdateResponse';
export type { ProductUploadImageResponse } from './models/ProductUploadImageResponse'; export type { ProductUploadImageResponse } from './models/ProductUploadImageResponse';
export type { RoleSchema } from './models/RoleSchema'; export type { RoleSchema } from './models/RoleSchema';
export type { ServiceCategoryPriceSchema } from './models/ServiceCategoryPriceSchema'; export type { ServiceCategoryPriceSchema } from './models/ServiceCategoryPriceSchema';
export type { ServiceCategoryReorderRequest } from './models/ServiceCategoryReorderRequest';
export type { ServiceCategoryReorderResponse } from './models/ServiceCategoryReorderResponse';
export type { ServiceCategorySchema } from './models/ServiceCategorySchema'; export type { ServiceCategorySchema } from './models/ServiceCategorySchema';
export type { ServiceCreateCategoryRequest } from './models/ServiceCreateCategoryRequest'; export type { ServiceCreateCategoryRequest } from './models/ServiceCreateCategoryRequest';
export type { ServiceCreateCategoryResponse } from './models/ServiceCreateCategoryResponse'; export type { ServiceCreateCategoryResponse } from './models/ServiceCreateCategoryResponse';
@@ -193,6 +195,8 @@ export type { ServiceGetAllCategoriesResponse } from './models/ServiceGetAllCate
export type { ServiceGetAllResponse } from './models/ServiceGetAllResponse'; export type { ServiceGetAllResponse } from './models/ServiceGetAllResponse';
export type { ServicePriceCategorySchema } from './models/ServicePriceCategorySchema'; export type { ServicePriceCategorySchema } from './models/ServicePriceCategorySchema';
export type { ServicePriceRangeSchema } from './models/ServicePriceRangeSchema'; export type { ServicePriceRangeSchema } from './models/ServicePriceRangeSchema';
export type { ServiceReorderRequest } from './models/ServiceReorderRequest';
export type { ServiceReorderResponse } from './models/ServiceReorderResponse';
export type { ServiceSchema } from './models/ServiceSchema'; export type { ServiceSchema } from './models/ServiceSchema';
export type { ServiceUpdateRequest } from './models/ServiceUpdateRequest'; export type { ServiceUpdateRequest } from './models/ServiceUpdateRequest';
export type { ServiceUpdateResponse } from './models/ServiceUpdateResponse'; export type { ServiceUpdateResponse } from './models/ServiceUpdateResponse';

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ServiceCategoryReorderRequest = {
moveDown: boolean;
moveUp: boolean;
categoryId: number;
serviceType: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ServiceCategoryReorderResponse = {
ok: boolean;
message: string;
};

View File

@@ -5,5 +5,7 @@
export type ServiceCategorySchema = { export type ServiceCategorySchema = {
id: number; id: number;
name: string; name: string;
dealServiceRank: string;
productServiceRank: string;
}; };

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ServiceReorderRequest = {
drainingServiceId: number;
hoveredServiceId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ServiceReorderResponse = {
ok: boolean;
message: string;
};

View File

@@ -14,5 +14,6 @@ export type ServiceSchema = {
priceRanges: Array<ServicePriceRangeSchema>; priceRanges: Array<ServicePriceRangeSchema>;
categoryPrices: Array<ServiceCategoryPriceSchema>; categoryPrices: Array<ServiceCategoryPriceSchema>;
cost: (number | null); cost: (number | null);
rank: string;
}; };

View File

@@ -11,6 +11,8 @@ import type { DeletePriceCategoryRequest } from '../models/DeletePriceCategoryRe
import type { DeletePriceCategoryResponse } from '../models/DeletePriceCategoryResponse'; import type { DeletePriceCategoryResponse } from '../models/DeletePriceCategoryResponse';
import type { GetAllPriceCategoriesResponse } from '../models/GetAllPriceCategoriesResponse'; import type { GetAllPriceCategoriesResponse } from '../models/GetAllPriceCategoriesResponse';
import type { GetAllServicesKitsResponse } from '../models/GetAllServicesKitsResponse'; import type { GetAllServicesKitsResponse } from '../models/GetAllServicesKitsResponse';
import type { ServiceCategoryReorderRequest } from '../models/ServiceCategoryReorderRequest';
import type { ServiceCategoryReorderResponse } from '../models/ServiceCategoryReorderResponse';
import type { ServiceCreateCategoryRequest } from '../models/ServiceCreateCategoryRequest'; import type { ServiceCreateCategoryRequest } from '../models/ServiceCreateCategoryRequest';
import type { ServiceCreateCategoryResponse } from '../models/ServiceCreateCategoryResponse'; import type { ServiceCreateCategoryResponse } from '../models/ServiceCreateCategoryResponse';
import type { ServiceCreateRequest } from '../models/ServiceCreateRequest'; import type { ServiceCreateRequest } from '../models/ServiceCreateRequest';
@@ -19,6 +21,8 @@ import type { ServiceDeleteRequest } from '../models/ServiceDeleteRequest';
import type { ServiceDeleteResponse } from '../models/ServiceDeleteResponse'; import type { ServiceDeleteResponse } from '../models/ServiceDeleteResponse';
import type { ServiceGetAllCategoriesResponse } from '../models/ServiceGetAllCategoriesResponse'; import type { ServiceGetAllCategoriesResponse } from '../models/ServiceGetAllCategoriesResponse';
import type { ServiceGetAllResponse } from '../models/ServiceGetAllResponse'; import type { ServiceGetAllResponse } from '../models/ServiceGetAllResponse';
import type { ServiceReorderRequest } from '../models/ServiceReorderRequest';
import type { ServiceReorderResponse } from '../models/ServiceReorderResponse';
import type { ServiceUpdateRequest } from '../models/ServiceUpdateRequest'; import type { ServiceUpdateRequest } from '../models/ServiceUpdateRequest';
import type { ServiceUpdateResponse } from '../models/ServiceUpdateResponse'; import type { ServiceUpdateResponse } from '../models/ServiceUpdateResponse';
import type { UpdatePriceCategoryRequest } from '../models/UpdatePriceCategoryRequest'; import type { UpdatePriceCategoryRequest } from '../models/UpdatePriceCategoryRequest';
@@ -100,6 +104,26 @@ export class ServiceService {
}, },
}); });
} }
/**
* Reorder
* @returns ServiceReorderResponse Successful Response
* @throws ApiError
*/
public static reorderService({
requestBody,
}: {
requestBody: ServiceReorderRequest,
}): CancelablePromise<ServiceReorderResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/service/reorder',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/** /**
* Get All Categories * Get All Categories
* @returns ServiceGetAllCategoriesResponse Successful Response * @returns ServiceGetAllCategoriesResponse Successful Response
@@ -131,6 +155,26 @@ export class ServiceService {
}, },
}); });
} }
/**
* Reorder Category
* @returns ServiceCategoryReorderResponse Successful Response
* @throws ApiError
*/
public static reorderServiceCategory({
requestBody,
}: {
requestBody: ServiceCategoryReorderRequest,
}): CancelablePromise<ServiceCategoryReorderResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/service/categories/reorder',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/** /**
* Get All Service Types * Get All Service Types
* @returns BaseEnumListSchema Successful Response * @returns BaseEnumListSchema Successful Response

View File

@@ -1,19 +1,42 @@
import { ServiceSchema } from "../../../../client"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { ServiceSchema, ServiceService } from "../../../../client";
import { FC } from "react"; import { FC } from "react";
import { useServicesTableColumns } from "./columns.tsx"; import { useServicesTableColumns } from "./columns.tsx";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx"; import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { MRT_TableOptions } from "mantine-react-table"; import { MRT_TableOptions } from "mantine-react-table";
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx"; import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
import { ActionIcon, Flex, Tooltip } from "@mantine/core"; import { ActionIcon, Flex, Tooltip } from "@mantine/core";
import { IconEdit, IconTrash } from "@tabler/icons-react"; import { IconArrowDown, IconArrowUp, IconEdit, IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { useQueryClient } from "@tanstack/react-query";
import { ServicesTab } from "../ServiceTypeSegmentedControl/ServiceTypeSegmentedControl.tsx";
const ServicesTable: FC<CRUDTableProps<ServiceSchema>> = ({ type RestProps = {
items, serviceType: ServicesTab;
onDelete, editMode: boolean;
onChange, }
}) => { type Props = CRUDTableProps<ServiceSchema> & RestProps;
const ServicesTable: FC<Props> = ({
items,
onDelete,
onChange,
serviceType,
editMode,
}) => {
const queryClient = useQueryClient();
const columns = useServicesTableColumns(); const columns = useServicesTableColumns();
const categoryRanks = items.map(item => item.category).map(category => {
if (serviceType === ServicesTab.DEAL_SERVICE) {
return category.dealServiceRank;
}
return category.productServiceRank;
});
const minRank = categoryRanks.sort()[0];
const maxRank = categoryRanks.sort()[categoryRanks.length - 1];
console.log(minRank, maxRank);
const onEditClick = (service: ServiceSchema) => { const onEditClick = (service: ServiceSchema) => {
if (!onChange) return; if (!onChange) return;
@@ -31,33 +54,125 @@ const ServicesTable: FC<CRUDTableProps<ServiceSchema>> = ({
<BaseTable <BaseTable
data={items} data={items}
columns={columns} columns={columns}
restProps={
{ restProps={{
enableGrouping: true, enableGrouping: true,
initialState: { grouping: ["category"] }, initialState: {
enableColumnActions: false, grouping: ["category"],
enableRowActions: true, },
renderRowActions: ({ row }) => ( state: {
<Flex gap="md"> columnVisibility: {
<Tooltip label="Редактировать"> "mrt-row-drag": editMode,
<ActionIcon },
onClick={() => onEditClick(row.original)} },
variant={"default"}> enableColumnActions: false,
<IconEdit /> enableRowOrdering: true,
</ActionIcon> mantineRowDragHandleProps: ({ table }) => ({
</Tooltip> onDragEnd: () => {
<Tooltip label="Удалить"> const { draggingRow, hoveredRow } = table.getState();
<ActionIcon if (!hoveredRow?.original || !draggingRow?.original) return;
onClick={() => { ServiceService.reorderService({
if (onDelete) onDelete(row.original); requestBody: {
}} drainingServiceId: draggingRow.original.id,
variant={"default"}> hoveredServiceId: hoveredRow.original.id,
<IconTrash /> },
</ActionIcon> }).then(({ ok, message }) => {
</Tooltip> if (!ok) {
</Flex> notifications.guess(ok, { message });
), return;
} as MRT_TableOptions<ServiceSchema> }
queryClient.invalidateQueries({
queryKey: ["getAllServices"],
}).then(() => {
});
});
},
}),
displayColumnDefOptions: {
"mrt-row-drag": {
AggregatedCell: ({ row }) => {
const rank = serviceType === ServicesTab.DEAL_SERVICE ? row.original.category.dealServiceRank : row.original.category.productServiceRank;
return (
<Flex gap={"xs"}>
<Tooltip label="Поднять вверх">
<ActionIcon
disabled={rank === minRank}
onClick={() => {
ServiceService.reorderServiceCategory({
requestBody: {
serviceType,
categoryId: row.original.category.id,
moveDown: false,
moveUp: true,
},
}).then(({ ok, message }) => {
if (!ok) {
notifications.guess(ok, { message });
return;
}
queryClient.invalidateQueries({
queryKey: ["getAllServices"],
}).then(() => {
});
});
}}
variant={"default"}>
<IconArrowUp />
</ActionIcon>
</Tooltip>
<Tooltip label="Опустить вниз">
<ActionIcon
disabled={rank === maxRank}
onClick={() => {
ServiceService.reorderServiceCategory({
requestBody: {
serviceType,
categoryId: row.original.category.id,
moveDown: true,
moveUp: false,
},
}).then(({ ok, message }) => {
if (!ok) {
notifications.guess(ok, { message });
return;
}
queryClient.invalidateQueries({
queryKey: ["getAllServices"],
}).then(() => {
});
});
}}
variant={"default"}>
<IconArrowDown />
</ActionIcon>
</Tooltip>
</Flex>
);
},
},
},
enableRowActions: true,
renderRowActions: ({ row }) => (
<Flex gap="xs">
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onEditClick(row.original)}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
<Tooltip label="Удалить">
<ActionIcon
onClick={() => {
if (onDelete) onDelete(row.original);
}}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<ServiceSchema>
} }
/> />
); );

View File

@@ -10,7 +10,7 @@ export const useServicesTableColumns = () => {
<> <>
<List> <List>
{service.priceRanges.map(range => ( {service.priceRanges.map(range => (
<List.Item> <List.Item key={range.id}>
{`${range.fromQuantity} - ${range.toQuantity}: ${range.price}`} {`${range.fromQuantity} - ${range.toQuantity}: ${range.price}`}
</List.Item> </List.Item>
))} ))}
@@ -24,9 +24,9 @@ export const useServicesTableColumns = () => {
{ {
accessorKey: "category", accessorKey: "category",
header: "Категория", header: "Категория",
enableGrouping: false, accessorFn: row => `${row.category.name}`,
enableColumnOrdering: true,
enableSorting: false, enableSorting: false,
accessorFn: row => row.category.name,
}, },
{ {
accessorKey: "name", accessorKey: "name",
@@ -47,7 +47,8 @@ export const useServicesTableColumns = () => {
enableGrouping: false, enableGrouping: false,
enableSorting: false, enableSorting: false,
}, },
], ],
[] [],
); );
}; };

View File

@@ -21,29 +21,32 @@ import PriceCategoryInput, {
type Props = CreateEditFormProps<ServiceSchema>; type Props = CreateEditFormProps<ServiceSchema>;
const CreateServiceModal = ({ const CreateServiceModal = ({
context, context,
id, id,
innerProps, innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const [priceType, setPriceType] = useState<ServicePriceType>( const [priceType, setPriceType] = useState<ServicePriceType>(
ServicePriceType.DEFAULT ServicePriceType.DEFAULT,
); );
const isEditing = "onChange" in innerProps; const isEditing = "onChange" in innerProps;
const initialValues: ServiceSchema = isEditing const initialValues: ServiceSchema = isEditing
? innerProps.element ? innerProps.element
: { : {
id: -1, id: -1,
name: "", name: "",
price: 0, price: 0,
category: { category: {
id: -1, id: -1,
name: "", name: "",
}, dealServiceRank: "",
serviceType: -1, productServiceRank: "",
priceRanges: [] as ServicePriceRangeSchema[], },
cost: null, serviceType: -1,
categoryPrices: [], priceRanges: [] as ServicePriceRangeSchema[],
}; cost: null,
categoryPrices: [],
rank: "",
};
const form = useForm<ServiceSchema>({ const form = useForm<ServiceSchema>({
initialValues: initialValues, initialValues: initialValues,
@@ -82,7 +85,7 @@ const CreateServiceModal = ({
return ( return (
<RangePriceInput <RangePriceInput
{...(form.getInputProps( {...(form.getInputProps(
"priceRanges" "priceRanges",
) as PriceRangeInputType)} ) as PriceRangeInputType)}
/> />
); );
@@ -90,7 +93,7 @@ const CreateServiceModal = ({
return ( return (
<PriceCategoryInput <PriceCategoryInput
{...(form.getInputProps( {...(form.getInputProps(
"categoryPrices" "categoryPrices",
) as PriceCategoryInputProps)} ) as PriceCategoryInputProps)}
/> />
); );

View File

@@ -2,7 +2,7 @@ import { FC, useState } from "react";
import ServicesTable from "../components/ServicesTable/ServicesTable.tsx"; import ServicesTable from "../components/ServicesTable/ServicesTable.tsx";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx"; import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import styles from "./ServicesPage.module.css"; import styles from "./ServicesPage.module.css";
import { Button } from "@mantine/core"; import { Button, Flex, rem, Switch } from "@mantine/core";
import ServiceTypeSegmentedControl, { import ServiceTypeSegmentedControl, {
ServicesTab, ServicesTab,
} from "../components/ServiceTypeSegmentedControl/ServiceTypeSegmentedControl.tsx"; } from "../components/ServiceTypeSegmentedControl/ServiceTypeSegmentedControl.tsx";
@@ -15,6 +15,7 @@ import { ObjectStateToTableProps } from "../../../types/utils.ts";
export const ServicesPage: FC = () => { export const ServicesPage: FC = () => {
const [serviceType, setServiceType] = useState(ServicesTab.DEAL_SERVICE); const [serviceType, setServiceType] = useState(ServicesTab.DEAL_SERVICE);
const [isEditMode, setIsEditMode] = useState(false);
const { const {
services, services,
onServiceDelete, onServiceDelete,
@@ -38,16 +39,24 @@ export const ServicesPage: FC = () => {
/> />
); );
case ServicesTab.DEAL_SERVICE: case ServicesTab.DEAL_SERVICE:
case ServicesTab.PRODUCT_SERVICE: case ServicesTab.PRODUCT_SERVICE: {
const servicesOrdered = services.filter(service => service.serviceType === serviceType).sort((a, b) => {
if (serviceType === ServicesTab.DEAL_SERVICE) {
return a.category.dealServiceRank.localeCompare(b.category.dealServiceRank);
}
return a.category.productServiceRank.localeCompare(b.category.productServiceRank);
},
);
return ( return (
<ServicesTable <ServicesTable
onDelete={onServiceDelete} onDelete={onServiceDelete}
onChange={onServiceUpdate} onChange={onServiceUpdate}
items={services.filter( items={servicesOrdered}
service => service.serviceType == serviceType serviceType={serviceType}
)} editMode={isEditMode}
/> />
); );
}
case ServicesTab.SERVICES_PRICE_CATEGORIES: case ServicesTab.SERVICES_PRICE_CATEGORIES:
return ( return (
<ServicePriceCategoryTable <ServicePriceCategoryTable
@@ -70,7 +79,7 @@ export const ServicesPage: FC = () => {
case ServicesTab.DEAL_SERVICE: case ServicesTab.DEAL_SERVICE:
case ServicesTab.PRODUCT_SERVICE: case ServicesTab.PRODUCT_SERVICE:
return ( return (
<> <Flex align={"center"} gap={rem(10)}>
<Button <Button
onClick={onCreateClick} onClick={onCreateClick}
variant={"default"}> variant={"default"}>
@@ -81,7 +90,14 @@ export const ServicesPage: FC = () => {
variant={"default"}> variant={"default"}>
Создать категорию Создать категорию
</Button> </Button>
</> <Switch
variant={"default"}
label={"Режим редактирования"}
checked={isEditMode}
onChange={() => setIsEditMode(!isEditMode)}
/>
</Flex>
); );
case ServicesTab.SERVICES_PRICE_CATEGORIES: case ServicesTab.SERVICES_PRICE_CATEGORIES:
return ( return (