crap
This commit is contained in:
@@ -26,8 +26,10 @@
|
||||
"@tanstack/router-devtools": "^1.16.6",
|
||||
"@tanstack/router-vite-plugin": "^1.16.5",
|
||||
"axios": "^1.6.7",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"dot-object": "^2.1.4",
|
||||
"mantine-form-zod-resolver": "^1.1.0",
|
||||
"mantine-react-table": "^2.0.0-beta.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -37,6 +39,7 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dot-object": "^2",
|
||||
"@types/react": "^18.2.56",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
|
||||
@@ -18,9 +18,20 @@ export type { DealChangeStatusResponse } from './models/DealChangeStatusResponse
|
||||
export type { DealCreateRequest } from './models/DealCreateRequest';
|
||||
export type { DealQuickCreateRequest } from './models/DealQuickCreateRequest';
|
||||
export type { DealQuickCreateResponse } from './models/DealQuickCreateResponse';
|
||||
export type { DealSummary } from './models/DealSummary';
|
||||
export type { DealSummaryResponse } from './models/DealSummaryResponse';
|
||||
export type { HTTPValidationError } from './models/HTTPValidationError';
|
||||
export type { ServiceCategorySchema } from './models/ServiceCategorySchema';
|
||||
export type { ServiceCreateCategoryRequest } from './models/ServiceCreateCategoryRequest';
|
||||
export type { ServiceCreateCategoryResponse } from './models/ServiceCreateCategoryResponse';
|
||||
export type { ServiceCreateRequest } from './models/ServiceCreateRequest';
|
||||
export type { ServiceCreateResponse } from './models/ServiceCreateResponse';
|
||||
export type { ServiceGetAllCategoriesResponse } from './models/ServiceGetAllCategoriesResponse';
|
||||
export type { ServiceGetAllResponse } from './models/ServiceGetAllResponse';
|
||||
export type { ServiceSchema } from './models/ServiceSchema';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
|
||||
export { AuthService } from './services/AuthService';
|
||||
export { ClientService } from './services/ClientService';
|
||||
export { DealService } from './services/DealService';
|
||||
export { ServiceService } from './services/ServiceService';
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ClientDetailsSchema } from './ClientDetailsSchema';
|
||||
export type ClientSchema = {
|
||||
id: number;
|
||||
name: string;
|
||||
details?: (ClientDetailsSchema | null);
|
||||
};
|
||||
|
||||
|
||||
12
src/client/models/DealSummary.ts
Normal file
12
src/client/models/DealSummary.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DealSummary = {
|
||||
id: number;
|
||||
name: string;
|
||||
client_name: string;
|
||||
changed_at: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
9
src/client/models/DealSummaryResponse.ts
Normal file
9
src/client/models/DealSummaryResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { DealSummary } from './DealSummary';
|
||||
export type DealSummaryResponse = {
|
||||
summaries: Array<DealSummary>;
|
||||
};
|
||||
|
||||
9
src/client/models/ServiceCategorySchema.ts
Normal file
9
src/client/models/ServiceCategorySchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ServiceCategorySchema = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
9
src/client/models/ServiceCreateCategoryRequest.ts
Normal file
9
src/client/models/ServiceCreateCategoryRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ServiceCategorySchema } from './ServiceCategorySchema';
|
||||
export type ServiceCreateCategoryRequest = {
|
||||
category: ServiceCategorySchema;
|
||||
};
|
||||
|
||||
9
src/client/models/ServiceCreateCategoryResponse.ts
Normal file
9
src/client/models/ServiceCreateCategoryResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ServiceCreateCategoryResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/ServiceCreateRequest.ts
Normal file
9
src/client/models/ServiceCreateRequest.ts
Normal 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 ServiceCreateRequest = {
|
||||
service: ServiceSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/ServiceCreateResponse.ts
Normal file
9
src/client/models/ServiceCreateResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ServiceCreateResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/ServiceGetAllCategoriesResponse.ts
Normal file
9
src/client/models/ServiceGetAllCategoriesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ServiceCategorySchema } from './ServiceCategorySchema';
|
||||
export type ServiceGetAllCategoriesResponse = {
|
||||
categories: Array<ServiceCategorySchema>;
|
||||
};
|
||||
|
||||
9
src/client/models/ServiceGetAllResponse.ts
Normal file
9
src/client/models/ServiceGetAllResponse.ts
Normal 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 ServiceGetAllResponse = {
|
||||
services: Array<ServiceSchema>;
|
||||
};
|
||||
|
||||
12
src/client/models/ServiceSchema.ts
Normal file
12
src/client/models/ServiceSchema.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ServiceCategorySchema } from './ServiceCategorySchema';
|
||||
export type ServiceSchema = {
|
||||
id: number;
|
||||
name: string;
|
||||
category: ServiceCategorySchema;
|
||||
price: number;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { DealChangeStatusResponse } from '../models/DealChangeStatusRespons
|
||||
import type { DealCreateRequest } from '../models/DealCreateRequest';
|
||||
import type { DealQuickCreateRequest } from '../models/DealQuickCreateRequest';
|
||||
import type { DealQuickCreateResponse } from '../models/DealQuickCreateResponse';
|
||||
import type { DealSummaryResponse } from '../models/DealSummaryResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
@@ -71,4 +72,15 @@ export class DealService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get Summary
|
||||
* @returns DealSummaryResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getDealSummaries(): CancelablePromise<DealSummaryResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/deal/summaries',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
77
src/client/services/ServiceService.ts
Normal file
77
src/client/services/ServiceService.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ServiceCreateCategoryRequest } from '../models/ServiceCreateCategoryRequest';
|
||||
import type { ServiceCreateCategoryResponse } from '../models/ServiceCreateCategoryResponse';
|
||||
import type { ServiceCreateRequest } from '../models/ServiceCreateRequest';
|
||||
import type { ServiceCreateResponse } from '../models/ServiceCreateResponse';
|
||||
import type { ServiceGetAllCategoriesResponse } from '../models/ServiceGetAllCategoriesResponse';
|
||||
import type { ServiceGetAllResponse } from '../models/ServiceGetAllResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class ServiceService {
|
||||
/**
|
||||
* Get All
|
||||
* @returns ServiceGetAllResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getAllServices(): CancelablePromise<ServiceGetAllResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/service/get-all',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create
|
||||
* @returns ServiceCreateResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createService({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: ServiceCreateRequest,
|
||||
}): CancelablePromise<ServiceCreateResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/service/create',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get All Categories
|
||||
* @returns ServiceGetAllCategoriesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getAllServiceCategories(): CancelablePromise<ServiceGetAllCategoriesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/service/categories/get-all',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create Category
|
||||
* @returns ServiceCreateCategoryResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createServiceCategory({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: ServiceCreateCategoryRequest,
|
||||
}): CancelablePromise<ServiceCreateCategoryResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/service/categories/create',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
49
src/components/BaseTable/BaseTable.tsx
Normal file
49
src/components/BaseTable/BaseTable.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
MantineReactTable,
|
||||
MRT_ColumnDef,
|
||||
MRT_RowData,
|
||||
MRT_Table,
|
||||
MRT_TableInstance,
|
||||
MRT_TableOptions,
|
||||
useMantineReactTable
|
||||
} from "mantine-react-table";
|
||||
import {MRT_Localization_RU} from "mantine-react-table/locales/ru/index.cjs";
|
||||
import {forwardRef, useEffect, useImperativeHandle} from 'react';
|
||||
|
||||
type Props<T extends Record<string, any>, K extends keyof T> = {
|
||||
data: T[],
|
||||
columns: MRT_ColumnDef<T>[],
|
||||
restProps?: MRT_TableOptions<T>,
|
||||
striped?: boolean
|
||||
}
|
||||
|
||||
|
||||
// Экспортируем тип рефа, чтобы он мог быть использован в других компонентах
|
||||
export type BaseTableRef<T extends MRT_RowData> = {
|
||||
getTable: () => MRT_TableInstance<T>;
|
||||
};
|
||||
|
||||
export const BaseTable = forwardRef<BaseTableRef<any>, Props<any>>((props, ref) => {
|
||||
const {data, columns, restProps, striped} = props;
|
||||
|
||||
const table = useMantineReactTable({
|
||||
localization: MRT_Localization_RU,
|
||||
enablePagination: false,
|
||||
data,
|
||||
columns,
|
||||
mantineTableProps: {
|
||||
striped: striped
|
||||
},
|
||||
enableTopToolbar: false,
|
||||
...restProps,
|
||||
});
|
||||
|
||||
// Используем useImperativeHandle для определения, что будет доступно через ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
// Предполагаем, что есть метод getTable в table, который мы хотим выставить
|
||||
getTable: () => table
|
||||
}));
|
||||
|
||||
|
||||
return <MantineReactTable table={table}/>;
|
||||
});
|
||||
@@ -21,4 +21,20 @@
|
||||
flex-direction: column;
|
||||
/*background-color: red;*/
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.items-list::after {
|
||||
height: 5rem;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.items-list-drag-over {
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-1);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
|
||||
}
|
||||
border-radius: var(--item-border-radius);
|
||||
}
|
||||
@@ -3,14 +3,18 @@ import styles from './Board.module.css';
|
||||
import {Divider, Text, Title} from '@mantine/core';
|
||||
import {Draggable, Droppable} from "@hello-pangea/dnd";
|
||||
import CreateDealButton from "../CreateDealButton/CreateDealButton.tsx";
|
||||
import {DealSummary} from "../../../client";
|
||||
import DealSummaryCard from "../DealSummaryCard/DealSummaryCard.tsx";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
droppableId: string;
|
||||
title: string;
|
||||
withCreateButton?: boolean;
|
||||
summaries: DealSummary[];
|
||||
}
|
||||
|
||||
export const Board: FC<Props> = ({droppableId, title, withCreateButton = false}) => {
|
||||
export const Board: FC<Props> = ({droppableId, title, summaries, withCreateButton = false}) => {
|
||||
|
||||
|
||||
return (
|
||||
@@ -20,26 +24,43 @@ export const Board: FC<Props> = ({droppableId, title, withCreateButton = false})
|
||||
<Text>12 сделок: 500р</Text>
|
||||
<Divider size={"xl"} my={10} color={"blue"}/>
|
||||
</div>
|
||||
<Droppable droppableId={droppableId}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} className={styles["items-list"]}>
|
||||
<Droppable
|
||||
droppableId={droppableId}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef}
|
||||
className={classNames(
|
||||
styles["items-list"],
|
||||
(snapshot.isDraggingOver && !snapshot.draggingFromThisWith)
|
||||
&& styles["items-list-drag-over"]
|
||||
)}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{withCreateButton &&
|
||||
<CreateDealButton
|
||||
|
||||
onClick={() => {
|
||||
}}
|
||||
/>}
|
||||
<Draggable draggableId={droppableId + '1'} index={1}>
|
||||
{(provided) => (
|
||||
<div {...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
{summaries.map(summary =>
|
||||
(
|
||||
<Draggable
|
||||
draggableId={summary.id.toString()}
|
||||
index={1}
|
||||
key={summary.id}
|
||||
>
|
||||
</div>
|
||||
{(provided) => (
|
||||
<div {...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
|
||||
)}
|
||||
</Draggable>
|
||||
>
|
||||
<DealSummaryCard dealSummary={summary}/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
min-height: 5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {dateWithoutTimezone} from "../../../shared/lib/utils.ts";
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
}
|
||||
const CreateDealButton: FC<Props> = ({onClick}) => {
|
||||
const CreateDealButton: FC<Props> = () => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isTransitionEnded, setIsTransitionEnded] = useState(true);
|
||||
return (
|
||||
@@ -27,6 +27,7 @@ const CreateDealButton: FC<Props> = ({onClick}) => {
|
||||
mounted={isCreating}
|
||||
transition={"scale-y"}
|
||||
onExited={() => setIsTransitionEnded(true)}
|
||||
|
||||
>
|
||||
{(styles) => (
|
||||
<div style={styles}>
|
||||
@@ -35,7 +36,6 @@ const CreateDealButton: FC<Props> = ({onClick}) => {
|
||||
setIsCreating(false)
|
||||
}}
|
||||
onSubmit={(quickDeal) => {
|
||||
console.log(quickDeal);
|
||||
DealService.quickCreateDealQuickCreatePost({
|
||||
requestBody: {
|
||||
...quickDeal,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
.container {
|
||||
min-height: 5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
cursor: pointer;
|
||||
padding-left: rem(10);
|
||||
padding-right: rem(10);
|
||||
padding-top: rem(5);
|
||||
padding-bottom: rem(5);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-row-right {
|
||||
align-items: flex-end;
|
||||
}
|
||||
46
src/components/Dnd/DealSummaryCard/DealSummaryCard.tsx
Normal file
46
src/components/Dnd/DealSummaryCard/DealSummaryCard.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import {FC} from "react";
|
||||
import {DealSummary} from "../../../client";
|
||||
import styles from './DealSummaryCard.module.css';
|
||||
|
||||
import {Text} from '@mantine/core';
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
dealSummary: DealSummary
|
||||
}
|
||||
|
||||
const DealSummaryCard: FC<Props> = ({dealSummary}) => {
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<div className={styles['flex-row']}>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={"gray.6"}>
|
||||
{dealSummary.client_name}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"md"} c={"blue.5"}>{dealSummary.name}</Text>
|
||||
</div>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={"gray.6"}>
|
||||
228 руб
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(styles['flex-row'], styles['flex-row-right'])}>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={"gray.6"}>
|
||||
{new Date(dealSummary.changed_at).toLocaleString('ru-RU')}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={"yellow.8"}>
|
||||
Нет задач
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default DealSummaryCard;
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.header-button {
|
||||
height: 100%;
|
||||
height: 100% !important;
|
||||
width: 10%;
|
||||
min-width: 5rem;
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const Header: FC = ()=>{
|
||||
|
||||
return (
|
||||
<div className={styles['header']}>
|
||||
|
||||
<TextInput
|
||||
radius={0}
|
||||
placeholder={"Поиск и фильтры"}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Center, Image, rem, Stack, Tooltip, UnstyledButton, useMantineColorScheme} from '@mantine/core';
|
||||
import {IconCash, IconHome2, IconLogout, IconMan, IconMoon, IconSun,} from '@tabler/icons-react';
|
||||
import {IconBox, IconCash, IconHome2, IconLogout, IconMan, IconMoon, IconSun,} from '@tabler/icons-react';
|
||||
import classes from './Navbar.module.css';
|
||||
import {useAppDispatch} from "../../redux/store.ts";
|
||||
import {logout} from "../../features/authSlice.ts";
|
||||
@@ -44,7 +44,12 @@ const mockdata = [
|
||||
icon: IconMan,
|
||||
label: 'Клиенты',
|
||||
href: '/clients'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: IconBox,
|
||||
label: 'Услуги',
|
||||
href: '/services'
|
||||
},
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
|
||||
6
src/components/PageBlock/PageBlock.module.css
Normal file
6
src/components/PageBlock/PageBlock.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.container {
|
||||
border-radius: rem(20);
|
||||
background-color: var(--mantine-color-body);
|
||||
padding: rem(10);
|
||||
flex: 1;
|
||||
}
|
||||
14
src/components/PageBlock/PageBlock.tsx
Normal file
14
src/components/PageBlock/PageBlock.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import {FC, ReactNode} from "react";
|
||||
import styles from './PageBlock.module.css';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
}
|
||||
export const PageBlock: FC<Props> = ({children}) => {
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default PageBlock;
|
||||
@@ -42,7 +42,7 @@ const ClientSelect: FC<Props> = ({onSelect, addressRestProps, nameRestProps, wit
|
||||
{
|
||||
name: value,
|
||||
id: -1,
|
||||
address: ''
|
||||
address: ""
|
||||
});
|
||||
}, [value]);
|
||||
useEffect(() => {
|
||||
@@ -74,6 +74,12 @@ const ClientSelect: FC<Props> = ({onSelect, addressRestProps, nameRestProps, wit
|
||||
{withAddress &&
|
||||
<TextInput
|
||||
placeholder={'Клиент: адрес'}
|
||||
styles={{
|
||||
input: {
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
}
|
||||
}}
|
||||
value={selectedClient?.address || ''}
|
||||
onChange={event => {
|
||||
selectClient(prevState => prevState && {...prevState, address: event.target.value})
|
||||
|
||||
13
src/main.tsx
13
src/main.tsx
@@ -9,6 +9,8 @@ import {store} from "./redux/store.ts";
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import 'mantine-react-table/styles.css';
|
||||
|
||||
import 'dayjs/locale/ru';
|
||||
|
||||
import './main.css';
|
||||
@@ -16,6 +18,7 @@ import {Notifications} from "@mantine/notifications";
|
||||
import {ModalsProvider} from "@mantine/modals";
|
||||
import {OpenAPI} from "./client";
|
||||
import {DatesProvider} from "@mantine/dates";
|
||||
import {modals} from "./modals/modals.ts";
|
||||
|
||||
// Configuring router
|
||||
const router = createRouter({routeTree})
|
||||
@@ -25,6 +28,12 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mantine/modals' {
|
||||
export interface MantineModalsOverride {
|
||||
modals: typeof modals;
|
||||
}
|
||||
}
|
||||
|
||||
// Configuring query
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -36,13 +45,11 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MantineProvider defaultColorScheme={"dark"}>
|
||||
<ModalsProvider>
|
||||
<ModalsProvider modals={modals}>
|
||||
<DatesProvider settings={{locale: 'ru'}}>
|
||||
|
||||
<RouterProvider router={router}/>
|
||||
<Notifications/>
|
||||
</DatesProvider>
|
||||
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
19
src/modals/EnterDeadlineModal/EnterDeadlineModal.tsx
Normal file
19
src/modals/EnterDeadlineModal/EnterDeadlineModal.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {Button, Text} from "@mantine/core";
|
||||
|
||||
const EnterDeadlineModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<{ modalBody: string }>) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text size="sm">{innerProps.modalBody}</Text>
|
||||
<Button fullWidth mt="md" onClick={() => context.closeModal(id)}>
|
||||
Close modal
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
};
|
||||
export default EnterDeadlineModal;
|
||||
9
src/modals/modals.ts
Normal file
9
src/modals/modals.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import EnterDeadlineModal from "./EnterDeadlineModal/EnterDeadlineModal.tsx";
|
||||
import CreateServiceCategoryModal from "../pages/ServicesPage/modals/CreateServiceCategoryModal.tsx";
|
||||
import CreateServiceModal from "../pages/ServicesPage/modals/CreateServiceModal.tsx";
|
||||
|
||||
export const modals = {
|
||||
enterDeadline: EnterDeadlineModal,
|
||||
createServiceCategory: CreateServiceCategoryModal,
|
||||
createService: CreateServiceModal
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import {FC} from "react";
|
||||
import styles from './ClientsPage.module.css';
|
||||
import ClientsTable from "./components/ClientsTable/ClientsTable.tsx";
|
||||
import {ClientService} from "../../client";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
|
||||
import useClientsList from "./hooks/useClientsList.tsx";
|
||||
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
|
||||
|
||||
const ClientsPage: FC = () => {
|
||||
const {data} = useQuery({queryKey: ['clients'], queryFn: ClientService.getAllClients})
|
||||
const {clients} = useClientsList();
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<ClientsTable data={data?.clients || []}/>
|
||||
</div>
|
||||
<>
|
||||
<PageBlock>
|
||||
|
||||
<ClientsTable data={clients}/>
|
||||
</PageBlock>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ClientsPage;
|
||||
@@ -1,52 +1,28 @@
|
||||
import {Checkbox, rem, Table} from "@mantine/core";
|
||||
import {columns} from "./columns.tsx";
|
||||
import React, {FC} from "react";
|
||||
import {Client} from "../../../../types/Client.ts";
|
||||
import {IconEdit} from "@tabler/icons-react";
|
||||
import {notifications} from "../../../../shared/lib/notifications.ts";
|
||||
import {FC} from "react";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {useClientsTableColumns} from "./columns.tsx";
|
||||
import {MantineReactTable, MRT_Table, MRT_TableOptions, useMantineReactTable} from "mantine-react-table";
|
||||
import {ClientSchema} from "../../../../client";
|
||||
|
||||
type Props = {
|
||||
data: Client[];
|
||||
data: ClientSchema[];
|
||||
|
||||
}
|
||||
const ClientsTable: FC<Props> = ({data}) => {
|
||||
|
||||
const columns = useClientsTableColumns();
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={rem(50)}>
|
||||
<Table striped>
|
||||
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th/>
|
||||
|
||||
{columns.map(column => (
|
||||
<Table.Th>{column.header}</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data.map(client => (
|
||||
<Table.Tr key={client.name}>
|
||||
<Table.Td>
|
||||
|
||||
<IconEdit
|
||||
onClick={() => {
|
||||
notifications.success({message: `Редактирую ${client.id}`})
|
||||
}}
|
||||
style={{cursor: 'pointer'}}
|
||||
/>
|
||||
</Table.Td>
|
||||
|
||||
{columns.map(column => (
|
||||
<Table.Td>{client[column.accessorKey]}</Table.Td>
|
||||
|
||||
))}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
</Table.ScrollContainer>
|
||||
<BaseTable
|
||||
striped
|
||||
data={data}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowSelection: true,
|
||||
enableColumnResizing: true,
|
||||
layoutMode: "grid"
|
||||
} as MRT_TableOptions<ClientSchema>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import {Client} from "../../../../types/Client.ts";
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {ClientSchema} from "../../../../client";
|
||||
|
||||
type Column = {
|
||||
accessorKey: keyof Client;
|
||||
header: string;
|
||||
|
||||
export const useClientsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<ClientSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Имя",
|
||||
},
|
||||
{
|
||||
accessorKey: "details.address",
|
||||
header: "Адрес"
|
||||
},
|
||||
{
|
||||
accessorKey: "details.email",
|
||||
header: "EMAIL"
|
||||
},
|
||||
{
|
||||
accessorKey: "details.phone_number",
|
||||
header: "Телефон"
|
||||
}
|
||||
], []);
|
||||
}
|
||||
export const columns: Column[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Название'
|
||||
}
|
||||
]
|
||||
13
src/pages/ClientsPage/hooks/useClientsList.tsx
Normal file
13
src/pages/ClientsPage/hooks/useClientsList.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import {ClientService} from "../../../client";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
|
||||
const useClientsList = () => {
|
||||
const {isPending, error, data, refetch} = useQuery({
|
||||
queryKey: ['getAllClients'],
|
||||
queryFn: ClientService.getAllClients
|
||||
});
|
||||
const clients = isPending || error || !data ? [] : data.clients;
|
||||
|
||||
return {clients, refetch}
|
||||
}
|
||||
export default useClientsList;
|
||||
15
src/pages/LeadsPage/hooks/useDealSummaries.tsx
Normal file
15
src/pages/LeadsPage/hooks/useDealSummaries.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {DealService, DealSummary} from "../../../client";
|
||||
|
||||
export const useDealSummaries = (): DealSummary[] => {
|
||||
const {data: summaries = []} = useQuery({
|
||||
queryKey: ['getDealSummaries'],
|
||||
queryFn: DealService.getDealSummaries,
|
||||
select: data => data.summaries || [] // Трансформируем полученные данные
|
||||
});
|
||||
|
||||
// Теперь summaries будет содержать либо трансформированные данные, либо пустой массив по умолчанию
|
||||
// isLoading и isError могут быть использованы для отображения индикаторов загрузки или ошибки
|
||||
|
||||
return summaries;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,70 @@
|
||||
import {FC, useEffect} from "react";
|
||||
import React, {FC, useEffect, useState} from "react";
|
||||
import styles from './LeadsPage.module.css';
|
||||
import Board from "../../../components/Dnd/Board/Board.tsx";
|
||||
import {Button, TextInput} from "@mantine/core";
|
||||
import {DragDropContext} from "@hello-pangea/dnd";
|
||||
import {useDealSummaries} from "../hooks/useDealSummaries.tsx";
|
||||
import {DealStatus} from "../../../shared/enums/DealStatus.ts";
|
||||
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
|
||||
|
||||
|
||||
export const LeadsPage: FC = () => {
|
||||
|
||||
const summariesRaw = useDealSummaries();
|
||||
const [summaries, setSummaries] = useState(summariesRaw);
|
||||
useEffect(() => {
|
||||
// dispatch(setIsLoading(true));
|
||||
}, []);
|
||||
|
||||
setSummaries(summariesRaw);
|
||||
}, [summariesRaw]);
|
||||
const onDragEnd = () => {
|
||||
// if (!result.destination) return;
|
||||
//
|
||||
// const newStatus = getDealStatusByName(
|
||||
// result.destination.droppableId
|
||||
// );
|
||||
// const summaryId = parseInt(result.draggableId);
|
||||
//
|
||||
// return;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={styles['container']}>
|
||||
<div className={styles['boards']}>
|
||||
<DragDropContext onDragEnd={() => {
|
||||
}}>
|
||||
<Board title={"Ожидает приемки"} withCreateButton droppableId={"AWAITING_ACCEPTANCE"}/>
|
||||
<Board title={"Упаковка"} droppableId={"PACKAGING"}/>
|
||||
<Board title={"Ожидает отгрузки"} droppableId={"AWAITING_SHIPMENT"}/>
|
||||
<Board title={"Ожидает оплаты"} droppableId={"AWAITING_PAYMENT"}/>
|
||||
<Board title={"Завершена"} droppableId={"COMPLETED"}/>
|
||||
|
||||
</DragDropContext>
|
||||
|
||||
<PageBlock>
|
||||
<div className={styles['container']}>
|
||||
<div className={styles['boards']}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Board
|
||||
withCreateButton
|
||||
summaries={summaries
|
||||
.filter(summary => summary.status == DealStatus.AWAITING_ACCEPTANCE)}
|
||||
title={"Ожидает приемки"}
|
||||
droppableId={"AWAITING_ACCEPTANCE"}
|
||||
/>
|
||||
<Board
|
||||
summaries={summaries
|
||||
.filter(summary => summary.status == DealStatus.PACKAGING)}
|
||||
title={"Упаковка"}
|
||||
droppableId={"PACKAGING"}
|
||||
/>
|
||||
<Board
|
||||
summaries={summaries
|
||||
.filter(summary => summary.status == DealStatus.AWAITING_SHIPMENT)}
|
||||
title={"Ожидает отгрузки"}
|
||||
droppableId={"AWAITING_SHIPMENT"}
|
||||
/>
|
||||
<Board
|
||||
summaries={summaries
|
||||
.filter(summary => summary.status == DealStatus.AWAITING_PAYMENT)}
|
||||
title={"Ожидает оплаты"}
|
||||
droppableId={"AWAITING_PAYMENT"}
|
||||
/>
|
||||
<Board
|
||||
summaries={summaries
|
||||
.filter(summary => summary.status == DealStatus.COMPLETED)}
|
||||
title={"Завершена"}
|
||||
droppableId={"COMPLETED"}
|
||||
/>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageBlock>
|
||||
|
||||
</>
|
||||
|
||||
)
|
||||
|
||||
14
src/pages/PageWrapper/PageWrapper.module.css
Normal file
14
src/pages/PageWrapper/PageWrapper.module.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.main-container {
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: rem(20);
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -1,31 +1,35 @@
|
||||
import {FC, ReactNode} from "react";
|
||||
import {Flex, rem} from "@mantine/core";
|
||||
import {AppShell, rem} from "@mantine/core";
|
||||
import {Navbar} from "../../components/Navbar/Navbar.tsx";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "../../redux/store.ts";
|
||||
import Header from "../../components/Header/Header.tsx";
|
||||
import styles from './PageWrapper.module.css';
|
||||
|
||||
export type Props = {
|
||||
children: ReactNode;
|
||||
}
|
||||
const PageWrapper: FC<Props> = ({children}) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
return (<Flex style={{flex: 1}}>
|
||||
|
||||
{authState.isAuthorized &&
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
layout={"alt"}
|
||||
navbar={{width: rem('80px'), breakpoint: "sm"}}
|
||||
// header={{height:rem(60)}}
|
||||
>
|
||||
{/*<AppShell.Header>*/}
|
||||
{/* <Header/>*/}
|
||||
{/*</AppShell.Header>*/}
|
||||
<AppShell.Navbar>
|
||||
<Navbar/>
|
||||
}
|
||||
<div style={{flex: 1, display: 'flex', flexDirection: 'column'}}>
|
||||
<Header/>
|
||||
<div style={{padding:rem(20), flex:1}}>
|
||||
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Main className={styles['main-container']}>
|
||||
<div className={styles['container']}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</Flex>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
export default PageWrapper;
|
||||
@@ -0,0 +1,37 @@
|
||||
import {Select} from "@mantine/core";
|
||||
import useServiceCategoriesList from "../../hooks/useServiceCategoriesList.tsx";
|
||||
import {ServiceCategorySchema} from "../../../../client";
|
||||
import {FC, ReactNode} from "react";
|
||||
|
||||
type Props = {
|
||||
fullWidth?: boolean,
|
||||
defaultValue?: ServiceCategorySchema
|
||||
onChange: (category: ServiceCategorySchema) => void
|
||||
error?: ReactNode
|
||||
}
|
||||
const ServiceCategorySelect: FC<Props> = ({defaultValue, onChange, fullWidth, error}) => {
|
||||
const {categories} = useServiceCategoriesList();
|
||||
|
||||
return (
|
||||
<Select
|
||||
error={error}
|
||||
w={fullWidth ? "100%" : undefined}
|
||||
checkIconPosition={"right"}
|
||||
label={"Категория услуги"}
|
||||
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()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceCategorySelect;
|
||||
@@ -0,0 +1,30 @@
|
||||
import {ServiceSchema} from "../../../../client";
|
||||
import {FC, RefObject} from "react";
|
||||
import {useServicesTableColumns} from "./columns.tsx";
|
||||
import {BaseTable, BaseTableRef} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
|
||||
type Props = {
|
||||
services: ServiceSchema[];
|
||||
tableRef?: RefObject<BaseTableRef<ServiceSchema>>
|
||||
}
|
||||
const ServicesTable: FC<Props> = ({services, tableRef}) => {
|
||||
const columns = useServicesTableColumns();
|
||||
return (
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
data={services}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableGrouping: true,
|
||||
initialState: {grouping: ["category"]},
|
||||
enableColumnActions: false,
|
||||
mantineCreateRowModalProps: {
|
||||
transitionProps: {transition: 'rotate-left', duration: 300}
|
||||
},
|
||||
} as MRT_TableOptions<ServiceSchema>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServicesTable;
|
||||
31
src/pages/ServicesPage/components/ServicesTable/columns.tsx
Normal file
31
src/pages/ServicesPage/components/ServicesTable/columns.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {ServiceSchema} from "../../../../client";
|
||||
|
||||
export const useServicesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<ServiceSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "category",
|
||||
header: "Категория",
|
||||
enableGrouping: false,
|
||||
enableSorting: false,
|
||||
accessorFn: (row) => row.category.name
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Услуга",
|
||||
enableGrouping: false,
|
||||
enableSorting: false,
|
||||
|
||||
},
|
||||
{
|
||||
accessorKey: "price",
|
||||
header: "Цена",
|
||||
enableGrouping: false,
|
||||
enableSorting: false,
|
||||
|
||||
|
||||
},
|
||||
], []);
|
||||
|
||||
}
|
||||
14
src/pages/ServicesPage/hooks/useServiceCategoriesList.tsx
Normal file
14
src/pages/ServicesPage/hooks/useServiceCategoriesList.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {ServiceService} from "../../../client";
|
||||
|
||||
const useServiceCategoriesList = () => {
|
||||
const {isPending, error, data, refetch} = useQuery({
|
||||
queryKey: ['getAllServiceCategories'],
|
||||
queryFn: ServiceService.getAllServiceCategories,
|
||||
});
|
||||
const categories = isPending || error || !data ? [] : data.categories;
|
||||
|
||||
return {categories, refetch}
|
||||
}
|
||||
|
||||
export default useServiceCategoriesList;
|
||||
13
src/pages/ServicesPage/hooks/useServicesList.tsx
Normal file
13
src/pages/ServicesPage/hooks/useServicesList.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {ServiceService} from "../../../client";
|
||||
|
||||
const useServicesList = () => {
|
||||
const {isPending, error, data, refetch} = useQuery({
|
||||
queryKey: ['getAllServices'],
|
||||
queryFn: ServiceService.getAllServices
|
||||
});
|
||||
const services = isPending || error || !data ? [] : data.services;
|
||||
|
||||
return {services, refetch}
|
||||
}
|
||||
export default useServicesList;
|
||||
1
src/pages/ServicesPage/index.ts
Normal file
1
src/pages/ServicesPage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {ServicesPage} from './ui/ServicesPage.tsx';
|
||||
52
src/pages/ServicesPage/modals/CreateServiceCategoryModal.tsx
Normal file
52
src/pages/ServicesPage/modals/CreateServiceCategoryModal.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import {ServiceCategorySchema} from "../../../client";
|
||||
import {Button, Flex, rem, TextInput} from "@mantine/core";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
|
||||
type Props = {
|
||||
onCreate: (category: ServiceCategorySchema) => void
|
||||
}
|
||||
const CreateServiceCategoryModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: ''
|
||||
},
|
||||
validate: {
|
||||
name: (name) => name.trim() !== '' ? null : "Необходимо ввести название категории",
|
||||
}
|
||||
})
|
||||
const onSubmit = (values: { name: string }) => {
|
||||
innerProps.onCreate({name: values.name, id: -1});
|
||||
context.closeContextModal(id);
|
||||
|
||||
}
|
||||
const onCancelClick = () => {
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
|
||||
<Flex gap={rem(10)} direction={"column"}>
|
||||
<TextInput
|
||||
|
||||
placeholder={"Введите название категори"}
|
||||
label={"Название категории"}
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
|
||||
|
||||
<Flex justify={"center"} mt={rem(5)} gap={rem(10)}>
|
||||
<Button onClick={() => onCancelClick()} variant={"subtle"}>Отменить</Button>
|
||||
<Button type={"submit"} variant={"default"}>Сохранить</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default CreateServiceCategoryModal;
|
||||
76
src/pages/ServicesPage/modals/CreateServiceModal.tsx
Normal file
76
src/pages/ServicesPage/modals/CreateServiceModal.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
onCreate: (service: ServiceSchema) => void
|
||||
}
|
||||
const CreateServiceModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
category: {
|
||||
id: -1,
|
||||
name: ''
|
||||
},
|
||||
name: '',
|
||||
price: NaN
|
||||
},
|
||||
validate: {
|
||||
category: (category) => category.id >= 0 ? null : "Необходимо выбрать категорию",
|
||||
name: (name) => name.trim() !== '' ? null : "Необходимо ввести название услуги",
|
||||
price: (price) => !isNaN(price) ? 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 (
|
||||
<>
|
||||
<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}
|
||||
/>
|
||||
<TextInput
|
||||
|
||||
placeholder={"Введите название услуги"}
|
||||
label={"Название услуги"}
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<NumberInput
|
||||
placeholder={"Введите стоимость услуги"}
|
||||
label={"Стоимость услуги"}
|
||||
hideControls
|
||||
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>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateServiceModal;
|
||||
12
src/pages/ServicesPage/ui/ServicesPage.module.css
Normal file
12
src/pages/ServicesPage/ui/ServicesPage.module.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: rem(10);
|
||||
}
|
||||
|
||||
.top-panel {
|
||||
padding: rem(5);
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
}
|
||||
67
src/pages/ServicesPage/ui/ServicesPage.tsx
Normal file
67
src/pages/ServicesPage/ui/ServicesPage.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import {FC, useRef} 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 {ServiceCategorySchema, ServiceSchema, ServiceService} from "../../../client";
|
||||
import {notifications} from "../../../shared/lib/notifications.ts";
|
||||
import {modals} from "@mantine/modals";
|
||||
|
||||
export const ServicesPage: FC = () => {
|
||||
const {services, refetch} = useServicesList();
|
||||
|
||||
const tableRef = useRef<BaseTableRef<ServiceSchema>>(null);
|
||||
|
||||
const onCreateClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: 'createService',
|
||||
title: 'Создание услуги',
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate
|
||||
}
|
||||
})
|
||||
}
|
||||
const onCreate = (values: ServiceSchema) => {
|
||||
ServiceService.createService({requestBody: {service: values}})
|
||||
.then(({ok, message}) => {
|
||||
notifications.guess(ok, {message: message});
|
||||
if (!ok) return;
|
||||
refetch();
|
||||
})
|
||||
}
|
||||
const onCreateCategoryClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "createServiceCategory",
|
||||
title: 'Создание категории',
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: onCategoryCreate
|
||||
}
|
||||
})
|
||||
}
|
||||
const onCategoryCreate = (category: ServiceCategorySchema) => {
|
||||
ServiceService.createServiceCategory({requestBody: {category: category}})
|
||||
.then(({ok, message}) =>
|
||||
notifications.guess(ok, {message: message}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<PageBlock>
|
||||
<div className={styles['top-panel']}>
|
||||
<Button onClick={onCreateClick} variant={"default"}>Создать услугу</Button>
|
||||
<Button onClick={onCreateCategoryClick} variant={"default"}>Создать категорию</Button>
|
||||
</div>
|
||||
</PageBlock>
|
||||
<PageBlock>
|
||||
<ServicesTable
|
||||
tableRef={tableRef}
|
||||
services={services}
|
||||
/>
|
||||
</PageBlock>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { Route as rootRoute } from './routes/__root'
|
||||
// Create Virtual Routes
|
||||
|
||||
const TestLazyImport = createFileRoute('/test')()
|
||||
const ServicesLazyImport = createFileRoute('/services')()
|
||||
const LoginLazyImport = createFileRoute('/login')()
|
||||
const LeadsLazyImport = createFileRoute('/leads')()
|
||||
const ClientsLazyImport = createFileRoute('/clients')()
|
||||
@@ -29,6 +30,11 @@ const TestLazyRoute = TestLazyImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/test.lazy').then((d) => d.Route))
|
||||
|
||||
const ServicesLazyRoute = ServicesLazyImport.update({
|
||||
path: '/services',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/services.lazy').then((d) => d.Route))
|
||||
|
||||
const LoginLazyRoute = LoginLazyImport.update({
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -69,6 +75,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof LoginLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/services': {
|
||||
preLoaderRoute: typeof ServicesLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/test': {
|
||||
preLoaderRoute: typeof TestLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
@@ -83,6 +93,7 @@ export const routeTree = rootRoute.addChildren([
|
||||
ClientsLazyRoute,
|
||||
LeadsLazyRoute,
|
||||
LoginLazyRoute,
|
||||
ServicesLazyRoute,
|
||||
TestLazyRoute,
|
||||
])
|
||||
|
||||
|
||||
6
src/routes/services.lazy.tsx
Normal file
6
src/routes/services.lazy.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import {createLazyFileRoute} from "@tanstack/react-router";
|
||||
import {ServicesPage} from "../pages/ServicesPage";
|
||||
|
||||
export const Route = createLazyFileRoute('/services')({
|
||||
component: ServicesPage
|
||||
})
|
||||
13
src/shared/enums/DealStatus.ts
Normal file
13
src/shared/enums/DealStatus.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export enum DealStatus {
|
||||
CREATED = 0,
|
||||
AWAITING_ACCEPTANCE = 1,
|
||||
PACKAGING = 2,
|
||||
AWAITING_SHIPMENT = 3,
|
||||
AWAITING_PAYMENT = 4,
|
||||
COMPLETED = 5,
|
||||
CANCELLED = 6,
|
||||
}
|
||||
|
||||
export const getDealStatusByName = (name: string): DealStatus => {
|
||||
return DealStatus[name as keyof typeof DealStatus];
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export type Client = {
|
||||
id?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
@@ -20,7 +21,7 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user