feat: ud service, create product in deal details

This commit is contained in:
2024-04-29 04:29:36 +03:00
parent 532bb738bd
commit c402271bbe
11 changed files with 281 additions and 84 deletions

View File

@@ -75,9 +75,13 @@ export type { ServiceCreateCategoryRequest } from './models/ServiceCreateCategor
export type { ServiceCreateCategoryResponse } from './models/ServiceCreateCategoryResponse';
export type { ServiceCreateRequest } from './models/ServiceCreateRequest';
export type { ServiceCreateResponse } from './models/ServiceCreateResponse';
export type { ServiceDeleteRequest } from './models/ServiceDeleteRequest';
export type { ServiceDeleteResponse } from './models/ServiceDeleteResponse';
export type { ServiceGetAllCategoriesResponse } from './models/ServiceGetAllCategoriesResponse';
export type { ServiceGetAllResponse } from './models/ServiceGetAllResponse';
export type { ServiceSchema } from './models/ServiceSchema';
export type { ServiceUpdateRequest } from './models/ServiceUpdateRequest';
export type { ServiceUpdateResponse } from './models/ServiceUpdateResponse';
export type { UserSchema } from './models/UserSchema';
export type { ValidationError } from './models/ValidationError';

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ServiceDeleteRequest = {
serviceId: number;
};

View File

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

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ServiceSchema } from './ServiceSchema';
export type ServiceUpdateRequest = {
data: ServiceSchema;
};

View File

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

View File

@@ -6,8 +6,12 @@ import type { ServiceCreateCategoryRequest } from '../models/ServiceCreateCatego
import type { ServiceCreateCategoryResponse } from '../models/ServiceCreateCategoryResponse';
import type { ServiceCreateRequest } from '../models/ServiceCreateRequest';
import type { ServiceCreateResponse } from '../models/ServiceCreateResponse';
import type { ServiceDeleteRequest } from '../models/ServiceDeleteRequest';
import type { ServiceDeleteResponse } from '../models/ServiceDeleteResponse';
import type { ServiceGetAllCategoriesResponse } from '../models/ServiceGetAllCategoriesResponse';
import type { ServiceGetAllResponse } from '../models/ServiceGetAllResponse';
import type { ServiceUpdateRequest } from '../models/ServiceUpdateRequest';
import type { ServiceUpdateResponse } from '../models/ServiceUpdateResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
@@ -43,6 +47,46 @@ export class ServiceService {
},
});
}
/**
* Update
* @returns ServiceUpdateResponse Successful Response
* @throws ApiError
*/
public static updateService({
requestBody,
}: {
requestBody: ServiceUpdateRequest,
}): CancelablePromise<ServiceUpdateResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/service/update',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete
* @returns ServiceDeleteResponse Successful Response
* @throws ApiError
*/
public static deleteService({
requestBody,
}: {
requestBody: ServiceDeleteRequest,
}): CancelablePromise<ServiceDeleteResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/service/delete',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get All Categories
* @returns ServiceGetAllCategoriesResponse Successful Response

View File

@@ -2,11 +2,13 @@ import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import useDealProductsTableColumns from "./columns.tsx";
import {FC} from "react";
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {DealProductSchema} from "../../../../client";
import {DealProductSchema, ProductService} from "../../../../client";
import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
import {MRT_TableOptions} from "mantine-react-table";
import {modals} from "@mantine/modals";
import {IconBarcode, IconTrash} from "@tabler/icons-react";
import {notifications} from "../../../../shared/lib/notifications.ts";
import {CreateProductRequest} from "../../../ProductsPage/types.ts";
type RestProps = {
clientId: number;
@@ -51,7 +53,24 @@ const DealProductsTable: FC<Props> = (props: Props) => {
}
})
}
const onCreateProduct = (newProduct: CreateProductRequest) => {
ProductService.createProduct({
requestBody: newProduct
}).then(({ok, message}) => {
notifications.guess(ok, {message: message});
})
}
const onCreateProductClick = () => {
modals.openContextModal({
modal: "createProduct",
title: 'Создание товара',
withCloseButton: false,
innerProps: {
clientId: clientId,
onCreate: onCreateProduct
}
})
}
return (
<BaseTable
data={items}
@@ -87,8 +106,16 @@ const DealProductsTable: FC<Props> = (props: Props) => {
Удалить выбранные
</Button>
)}
<Button onClick={onCreateClick} variant={"default"}>
Добавить товар
<Button
variant={"default"}
onClick={onCreateProductClick}>
Создать товар
</Button>
<Button
onClick={onCreateClick}
variant={"default"}>
Добавить товар в следку
</Button>
</Flex>
),

View File

@@ -1,35 +1,55 @@
import {Select} from "@mantine/core";
import {Select, SelectProps} from "@mantine/core";
import useServiceCategoriesList from "../../hooks/useServiceCategoriesList.tsx";
import {ServiceCategorySchema} from "../../../../client";
import {FC, ReactNode} from "react";
import {FC, useEffect, useMemo, useState} from "react";
type Props = {
type ControlledValueProps = {
value: ServiceCategorySchema
onChange: (value: ServiceCategorySchema) => void
}
type RestProps = {
fullWidth?: boolean,
defaultValue?: ServiceCategorySchema
onChange: (category: ServiceCategorySchema) => void
error?: ReactNode
}
const ServiceCategorySelect: FC<Props> = ({defaultValue, onChange, fullWidth, error}) => {
type Props = (RestProps & Partial<ControlledValueProps>) & Omit<SelectProps, 'value' | 'onChange'>;
const ServiceCategorySelect: FC<Props> = (props: Props) => {
const {categories} = useServiceCategoriesList();
return (
<Select
error={error}
w={fullWidth ? "100%" : undefined}
checkIconPosition={"right"}
label={"Категория услуги"}
placeholder={"Выберите категорию услуги"}
data={categories.map(category => ({
const data = useMemo(() => categories.reduce((acc, category) => {
acc.push({
label: category.name,
value: category.id.toString()
}))}
onChange={event => {
});
return acc;
}, [] as { label: string, value: string }[]), [categories]);
const isControlled = 'value' in props;
const [internalValue, setInternalValue] = useState<ServiceCategorySchema | undefined>(props.defaultValue);
const value = isControlled ? props.value : internalValue;
const handleOnChange = (event: string | null) => {
if (!event) return;
const category = categories.find(category => category.id == parseInt(event))
const category = categories.find(category => parseInt(event) == category.id);
if (!category) return;
onChange(category)
}}
value={defaultValue && defaultValue.id.toString()}
if (isControlled) {
props.onChange(category);
return;
}
setInternalValue(category);
}
useEffect(() => {
if (isControlled || !internalValue) return;
props.onChange(internalValue);
}, [internalValue]);
return (
<Select
{...props}
value={value?.id.toString()}
onChange={handleOnChange}
data={data}
/>
)
}

View File

@@ -1,27 +1,55 @@
import {ServiceSchema} from "../../../../client";
import {FC, RefObject} from "react";
import {FC} from "react";
import {useServicesTableColumns} from "./columns.tsx";
import {BaseTable, BaseTableRef} from "../../../../components/BaseTable/BaseTable.tsx";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import {MRT_TableOptions} from "mantine-react-table";
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {ActionIcon, Flex, Tooltip} from "@mantine/core";
import {IconEdit, IconTrash} from "@tabler/icons-react";
import {modals} from "@mantine/modals";
type Props = {
services: ServiceSchema[];
tableRef?: RefObject<BaseTableRef<ServiceSchema>>
}
const ServicesTable: FC<Props> = ({services, tableRef}) => {
const ServicesTable: FC<CRUDTableProps<ServiceSchema>> = ({items, onDelete, onChange}) => {
const columns = useServicesTableColumns();
const onEditClick = (service: ServiceSchema) => {
if (!onChange) return;
modals.openContextModal({
modal: "createService",
title: 'Создание услуги',
withCloseButton: false,
innerProps: {
onChange: (newService) => onChange(newService),
element: service,
}
})
}
return (
<BaseTable
ref={tableRef}
data={services}
data={items}
columns={columns}
restProps={{
enableGrouping: true,
initialState: {grouping: ["category"]},
enableColumnActions: false,
mantineCreateRowModalProps: {
transitionProps: {transition: 'rotate-left', duration: 300}
},
enableRowActions: true,
renderRowActions: ({row}) => (
<Flex gap="md">
<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

@@ -1,51 +1,60 @@
import {ServiceSchema} from "../../../client";
import {Button, Flex, NumberInput, rem, TextInput} from "@mantine/core";
import ServiceCategorySelect from "../components/ServiceCategorySelect/ServiceCategorySelect.tsx";
import {useForm} from "@mantine/form";
import {ContextModalProps} from "@mantine/modals";
import BaseFormModal, {CreateEditFormProps} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import {NumberInput, TextInput} from "@mantine/core";
import ServiceCategorySelect from "../components/ServiceCategorySelect/ServiceCategorySelect.tsx";
type Props = {
onCreate: (service: ServiceSchema) => void
}
type Props = CreateEditFormProps<ServiceSchema>
const CreateServiceModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const form = useForm({
initialValues: {
const isEditing = 'onChange' in innerProps;
const initialValues: ServiceSchema = isEditing ? {
id: innerProps.element.id,
name: innerProps.element.name,
price: innerProps.element.price,
category: innerProps.element.category,
} : {
id: -1,
name: '',
price: 0,
category: {
id: -1,
name: ''
},
name: '',
price: NaN
},
}
}
const form = useForm<ServiceSchema>({
initialValues: initialValues,
validate: {
category: (category) => category.id >= 0 ? null : "Необходимо выбрать категорию",
name: (name) => name.trim() !== '' ? null : "Необходимо ввести название услуги",
price: (price) => !isNaN(price) ? null : "Небходимо ввести стоимость услуги"
name: (name: string) => name.trim() !== '' ? null : "Необходимо ввести название услуги",
price: (price: number) => price > 0 ? null : "Цена должна быть больше 0",
category: (category: {
id: number,
name: string
}) => category.id !== -1 ? null : "Необходимо выбрать категорию"
}
})
const onSubmit = (values: { category: { id: number; name: string; }; name: string; price: number; }) => {
innerProps.onCreate({...values, id: -1});
context.closeContextModal(id);
}
const onCancelClick = () => {
context.closeContextModal(id);
}
return (
<BaseFormModal
{...innerProps}
closeOnSubmit
form={form}
onClose={onCancelClick}
>
<BaseFormModal.Body>
<>
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
<Flex gap={rem(10)} direction={"column"}>
<ServiceCategorySelect
fullWidth
onChange={event => {
form.setFieldValue("category", event)
}}
error={form.getInputProps("category").error}
placeholder={"Выберите категорию"}
label={"Категория услуги"}
{...form.getInputProps('category')}
/>
<TextInput
@@ -60,16 +69,9 @@ const CreateServiceModal = ({
decimalScale={2}
{...form.getInputProps('price')}
/>
<Flex justify={"center"} mt={rem(5)} gap={rem(10)}>
<Button onClick={() => onCancelClick()} variant={"subtle"}>Отменить</Button>
<Button type={"submit"} variant={"default"}>Сохранить</Button>
</Flex>
</Flex>
</form>
</>
</BaseFormModal.Body>
</BaseFormModal>
)
}

View File

@@ -1,10 +1,9 @@
import {FC, useRef} from "react";
import {FC} from "react";
import ServicesTable from "../components/ServicesTable/ServicesTable.tsx";
import useServicesList from "../hooks/useServicesList.tsx";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import styles from './ServicesPage.module.css';
import {Button} from "@mantine/core";
import {BaseTableRef} from "../../../components/BaseTable/BaseTable.tsx";
import {Button, Text} from "@mantine/core";
import {ServiceCategorySchema, ServiceSchema, ServiceService} from "../../../client";
import {notifications} from "../../../shared/lib/notifications.ts";
import {modals} from "@mantine/modals";
@@ -12,8 +11,7 @@ import {modals} from "@mantine/modals";
export const ServicesPage: FC = () => {
const {services, refetch} = useServicesList();
const tableRef = useRef<BaseTableRef<ServiceSchema>>(null);
// region Service create
const onCreateClick = () => {
modals.openContextModal({
modal: 'createService',
@@ -32,6 +30,9 @@ export const ServicesPage: FC = () => {
await refetch();
})
}
// endregion
// region Category create
const onCreateCategoryClick = () => {
modals.openContextModal({
modal: "createServiceCategory",
@@ -47,7 +48,42 @@ export const ServicesPage: FC = () => {
.then(({ok, message}) =>
notifications.guess(ok, {message: message}))
}
// endregion
const onServiceDelete = (service: ServiceSchema) => {
modals.openConfirmModal({
title: 'Удаление услуги',
children: (<Text>
Вы уверены, что хотите удалить услугу "{service.name}"?
</Text>),
onConfirm: () => {
ServiceService.deleteService({requestBody: {serviceId: service.id}})
.then(async ({ok, message}) => {
notifications.guess(ok, {message: message});
if (!ok) return;
await refetch();
})
},
labels: {
confirm: 'Удалить',
cancel: 'Отмена'
}
});
}
const onServiceUpdate = (service: ServiceSchema) => {
ServiceService
.updateService({
requestBody: {
data: service
}
})
.then(async ({ok, message}) => {
notifications.guess(ok, {message: message});
if (!ok) return;
await refetch();
})
}
return (
<div className={styles['container']}>
<PageBlock>
@@ -58,8 +94,9 @@ export const ServicesPage: FC = () => {
</PageBlock>
<PageBlock>
<ServicesTable
tableRef={tableRef}
services={services}
onDelete={onServiceDelete}
onChange={onServiceUpdate}
items={services}
/>
</PageBlock>
</div>