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 { ServiceCreateCategoryResponse } from './models/ServiceCreateCategoryResponse';
export type { ServiceCreateRequest } from './models/ServiceCreateRequest'; export type { ServiceCreateRequest } from './models/ServiceCreateRequest';
export type { ServiceCreateResponse } from './models/ServiceCreateResponse'; 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 { ServiceGetAllCategoriesResponse } from './models/ServiceGetAllCategoriesResponse';
export type { ServiceGetAllResponse } from './models/ServiceGetAllResponse'; export type { ServiceGetAllResponse } from './models/ServiceGetAllResponse';
export type { ServiceSchema } from './models/ServiceSchema'; 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 { UserSchema } from './models/UserSchema';
export type { ValidationError } from './models/ValidationError'; 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 { ServiceCreateCategoryResponse } from '../models/ServiceCreateCategoryResponse';
import type { ServiceCreateRequest } from '../models/ServiceCreateRequest'; import type { ServiceCreateRequest } from '../models/ServiceCreateRequest';
import type { ServiceCreateResponse } from '../models/ServiceCreateResponse'; 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 { ServiceGetAllCategoriesResponse } from '../models/ServiceGetAllCategoriesResponse';
import type { ServiceGetAllResponse } from '../models/ServiceGetAllResponse'; 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 type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI'; import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request'; 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 * Get All Categories
* @returns ServiceGetAllCategoriesResponse Successful Response * @returns ServiceGetAllCategoriesResponse Successful Response

View File

@@ -2,11 +2,13 @@ import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import useDealProductsTableColumns from "./columns.tsx"; import useDealProductsTableColumns from "./columns.tsx";
import {FC} from "react"; import {FC} from "react";
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx"; 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 {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
import {MRT_TableOptions} from "mantine-react-table"; import {MRT_TableOptions} from "mantine-react-table";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
import {IconBarcode, IconTrash} from "@tabler/icons-react"; import {IconBarcode, IconTrash} from "@tabler/icons-react";
import {notifications} from "../../../../shared/lib/notifications.ts";
import {CreateProductRequest} from "../../../ProductsPage/types.ts";
type RestProps = { type RestProps = {
clientId: number; 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 ( return (
<BaseTable <BaseTable
data={items} data={items}
@@ -87,8 +106,16 @@ const DealProductsTable: FC<Props> = (props: Props) => {
Удалить выбранные Удалить выбранные
</Button> </Button>
)} )}
<Button onClick={onCreateClick} variant={"default"}> <Button
Добавить товар variant={"default"}
onClick={onCreateProductClick}>
Создать товар
</Button>
<Button
onClick={onCreateClick}
variant={"default"}>
Добавить товар в следку
</Button> </Button>
</Flex> </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 useServiceCategoriesList from "../../hooks/useServiceCategoriesList.tsx";
import {ServiceCategorySchema} from "../../../../client"; 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, fullWidth?: boolean,
defaultValue?: ServiceCategorySchema defaultValue?: ServiceCategorySchema
onChange: (category: ServiceCategorySchema) => void 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(); const {categories} = useServiceCategoriesList();
const data = useMemo(() => categories.reduce((acc, category) => {
acc.push({
label: category.name,
value: category.id.toString()
});
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 => parseInt(event) == category.id);
if (!category) return;
if (isControlled) {
props.onChange(category);
return;
}
setInternalValue(category);
}
useEffect(() => {
if (isControlled || !internalValue) return;
props.onChange(internalValue);
}, [internalValue]);
return ( return (
<Select <Select
error={error} {...props}
w={fullWidth ? "100%" : undefined} value={value?.id.toString()}
checkIconPosition={"right"} onChange={handleOnChange}
label={"Категория услуги"} data={data}
placeholder={"Выберите категорию услуги"}
data={categories.map(category => ({
label: category.name,
value: category.id.toString()
}))}
onChange={event => {
if (!event) return;
const category = categories.find(category => category.id == parseInt(event))
if (!category) return;
onChange(category)
}}
value={defaultValue && defaultValue.id.toString()}
/> />
) )
} }

View File

@@ -1,27 +1,55 @@
import {ServiceSchema} from "../../../../client"; import {ServiceSchema} from "../../../../client";
import {FC, RefObject} from "react"; import {FC} from "react";
import {useServicesTableColumns} from "./columns.tsx"; 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 {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 = { const ServicesTable: FC<CRUDTableProps<ServiceSchema>> = ({items, onDelete, onChange}) => {
services: ServiceSchema[];
tableRef?: RefObject<BaseTableRef<ServiceSchema>>
}
const ServicesTable: FC<Props> = ({services, tableRef}) => {
const columns = useServicesTableColumns(); 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 ( return (
<BaseTable <BaseTable
ref={tableRef} data={items}
data={services}
columns={columns} columns={columns}
restProps={{ restProps={{
enableGrouping: true, enableGrouping: true,
initialState: {grouping: ["category"]}, initialState: {grouping: ["category"]},
enableColumnActions: false, enableColumnActions: false,
mantineCreateRowModalProps: { enableRowActions: true,
transitionProps: {transition: 'rotate-left', duration: 300} 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>} } as MRT_TableOptions<ServiceSchema>}
/> />
) )

View File

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

View File

@@ -1,10 +1,9 @@
import {FC, useRef} from "react"; import {FC} from "react";
import ServicesTable from "../components/ServicesTable/ServicesTable.tsx"; import ServicesTable from "../components/ServicesTable/ServicesTable.tsx";
import useServicesList from "../hooks/useServicesList.tsx"; import useServicesList from "../hooks/useServicesList.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, Text} from "@mantine/core";
import {BaseTableRef} from "../../../components/BaseTable/BaseTable.tsx";
import {ServiceCategorySchema, ServiceSchema, ServiceService} from "../../../client"; import {ServiceCategorySchema, ServiceSchema, ServiceService} from "../../../client";
import {notifications} from "../../../shared/lib/notifications.ts"; import {notifications} from "../../../shared/lib/notifications.ts";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
@@ -12,8 +11,7 @@ import {modals} from "@mantine/modals";
export const ServicesPage: FC = () => { export const ServicesPage: FC = () => {
const {services, refetch} = useServicesList(); const {services, refetch} = useServicesList();
const tableRef = useRef<BaseTableRef<ServiceSchema>>(null); // region Service create
const onCreateClick = () => { const onCreateClick = () => {
modals.openContextModal({ modals.openContextModal({
modal: 'createService', modal: 'createService',
@@ -32,6 +30,9 @@ export const ServicesPage: FC = () => {
await refetch(); await refetch();
}) })
} }
// endregion
// region Category create
const onCreateCategoryClick = () => { const onCreateCategoryClick = () => {
modals.openContextModal({ modals.openContextModal({
modal: "createServiceCategory", modal: "createServiceCategory",
@@ -47,7 +48,42 @@ export const ServicesPage: FC = () => {
.then(({ok, message}) => .then(({ok, message}) =>
notifications.guess(ok, {message: 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 ( return (
<div className={styles['container']}> <div className={styles['container']}>
<PageBlock> <PageBlock>
@@ -58,8 +94,9 @@ export const ServicesPage: FC = () => {
</PageBlock> </PageBlock>
<PageBlock> <PageBlock>
<ServicesTable <ServicesTable
tableRef={tableRef} onDelete={onServiceDelete}
services={services} onChange={onServiceUpdate}
items={services}
/> />
</PageBlock> </PageBlock>
</div> </div>