This commit is contained in:
2024-03-19 09:02:58 +03:00
parent cc14105276
commit c9f3d4ee12
56 changed files with 995 additions and 121 deletions

View File

@@ -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",

View File

@@ -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';

View File

@@ -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);
};

View 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;
};

View 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>;
};

View 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;
};

View 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;
};

View 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;
};

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 ServiceCreateRequest = {
service: ServiceSchema;
};

View 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;
};

View 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>;
};

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 ServiceGetAllResponse = {
services: Array<ServiceSchema>;
};

View 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;
};

View File

@@ -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',
});
}
}

View 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`,
},
});
}
}

View 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}/>;
});

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;
}

View 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;

View File

@@ -8,7 +8,7 @@
}
.header-button {
height: 100%;
height: 100% !important;
width: 10%;
min-width: 5rem;
min-width: 10rem;
}

View File

@@ -6,6 +6,7 @@ const Header: FC = ()=>{
return (
<div className={styles['header']}>
<TextInput
radius={0}
placeholder={"Поиск и фильтры"}

View File

@@ -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() {

View File

@@ -0,0 +1,6 @@
.container {
border-radius: rem(20);
background-color: var(--mantine-color-body);
padding: rem(10);
flex: 1;
}

View 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;

View File

@@ -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})

View File

@@ -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>

View 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
View 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
}

View File

@@ -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;

View File

@@ -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>}
/>
)
}

View File

@@ -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: 'Название'
}
]

View 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;

View 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;
}

View File

@@ -2,6 +2,7 @@
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
}

View File

@@ -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>
</>
)

View 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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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,
},
], []);
}

View 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;

View 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;

View File

@@ -0,0 +1 @@
export {ServicesPage} from './ui/ServicesPage.tsx';

View 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;

View 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;

View 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;
}

View 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>
)
}

View File

@@ -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,
])

View File

@@ -0,0 +1,6 @@
import {createLazyFileRoute} from "@tanstack/react-router";
import {ServicesPage} from "../pages/ServicesPage";
export const Route = createLazyFileRoute('/services')({
component: ServicesPage
})

View 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];
}

View File

@@ -1,4 +1,5 @@
export type Client = {
id?: number;
id: number;
name: string;
address: string;
}

View File

@@ -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": [