NestJS backend rewrite; migrate client to FluentUI V9

This commit is contained in:
2025-09-18 15:48:08 +09:00
parent 32ff36a12c
commit 34529cea68
62 changed files with 5642 additions and 3679 deletions

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useBoilers } from '../hooks/swrHooks'
import { Stack, Text } from '@mantine/core'
import CustomTable from '../components/CustomTable'
import { Text } from '@fluentui/react-components'
function Boilers() {
const [boilersPage, setBoilersPage] = useState(1)
@@ -25,82 +25,47 @@ function Boilers() {
}, [])
return (
<Stack w={'100%'} h={'100%'} p='sm'>
<Text size="xl" fw={600}>
<div style={{
display: 'flex',
flexDirection: 'column',
padding: '1rem',
width: '100%',
gap: '1rem'
}}>
<Text size={600} weight='bold'>
Котельные
</Text>
{boilers &&
<CustomTable data={boilers} columns={[
{
accessorKey: 'id_object',
name: 'id_object',
header: 'ID',
cell: (info) => info.getValue(),
type: 'string'
},
{
accessorKey: 'boiler_name',
name: 'boiler_name',
header: 'Название',
cell: (info) => info.getValue(),
type: 'string'
},
{
accessorKey: 'boiler_code',
name: 'boiler_code',
header: 'Код',
cell: (info) => info.getValue(),
type: 'string'
},
{
accessorKey: 'id_city',
name: 'id_city',
header: 'Город',
cell: (info) => info.getValue(),
type: 'dictionary'
},
{
accessorKey: 'activity',
name: 'activity',
header: 'Активен',
cell: (info) => info.getValue(),
type: 'boolean'
},
]} />
}
{/* {boilers &&
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{boilersColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{boilers.map((boiler: IBoiler) => (
<Table.Tr key={boiler.id_object}>
{boilersColumns.map(column => {
if (column.field === 'activity') {
return (
boiler.activity ? (
<Table.Td key={`${boiler.id_object}-${boiler[column.field]}`}>
<Badge fullWidth variant="light">
Активен
</Badge>
</Table.Td>
) : (
<Table.Td key={`${boiler.id_object}-${boiler[column.field]}`}>
<Badge color="gray" fullWidth variant="light">
Отключен
</Badge>
</Table.Td>
)
)
}
else return (
<Table.Td key={`${boiler.id_object}-${column.field}`}>{boiler[column.field as keyof IBoiler]}</Table.Td>
)
})}
</Table.Tr>
))}
</Table.Tbody>
</Table>
} */}
</Stack>
</div>
)
}

View File

@@ -1,11 +1,10 @@
import { Flex } from '@mantine/core'
import ServerHardware from '../components/ServerHardware'
const ComponentTest = () => {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '1rem', padding: '1rem' }}>
<ServerHardware />
</Flex>
</div>
)
}

View File

@@ -1,36 +1,41 @@
import { Stack, Tabs } from '@mantine/core'
import useSWR from 'swr'
import { BASE_URL } from '../constants'
import { fetcher } from '../http/axiosInstance'
import { useState } from 'react'
import CustomTable from '../components/CustomTable'
import { Tab, TabList } from '@fluentui/react-components'
const DBManager = () => {
const { data: tablesData } = useSWR(`/db/tables`, (key) => fetcher(key, BASE_URL.ems), {
revalidateOnFocus: false
})
const [selectedTab, setSelectedTab] = useState<string | unknown>(undefined)
return (
<Stack w={'100%'} h={'100%'} p='xs'>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%', padding: '0.5rem' }}>
{tablesData && Array.isArray(tablesData) && tablesData.length > 0 &&
<Tabs w='100%' h='80%'>
<Stack h='100%'>
<Tabs.List>
<div style={{ width: '100%', height: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<TabList selectedValue={selectedTab} onTabSelect={(_, data) => setSelectedTab(data.value)}>
{tablesData.map(table => (
<Tabs.Tab key={table.tablename} value={table.tablename}>
<Tab key={table.tablename} value={table.tablename}>
{table.tablename}
</Tabs.Tab>
</Tab>
))}
</Tabs.List>
</TabList>
{tablesData.map(table => (
<Tabs.Panel h='100%' key={table.tablename} value={table.tablename} w='100%'>
<TableData tablename={table.tablename} />
</Tabs.Panel>
))}
</Stack>
<div style={{ width: '100%', height: '100%' }}>
{tablesData.map((table) => {
if (table.tablename === selectedTab)
return (
<TableData tablename={table.tablename} />
)
})}
</div>
</div>
</Tabs>
</div>
}
{/* <Card withBorder radius='sm'>
<Stack>
@@ -42,7 +47,7 @@ const DBManager = () => {
</Grid>
</Stack>
</Card> */}
</Stack>
</div>
)
}
@@ -65,9 +70,9 @@ const TableData = ({
{columnsData && rowsData && Array.isArray(columnsData) && Array.isArray(rowsData) && columnsData.length > 0 &&
<CustomTable data={rowsData} columns={columnsData.map(column => (
{
accessorKey: column.column_name,
name: column.column_name,
header: column.column_name,
cell: (info) => JSON.stringify(info.getValue()).length > 30 ? [JSON.stringify(info.getValue()).substring(0, 30), '...'].join('') : JSON.stringify(info.getValue()),
type: 'string',
}
))} />
}

View File

@@ -1,15 +1,15 @@
import { ActionIcon, Button, Flex, Input, Loader, LoadingOverlay, Modal, Overlay, Table, Tabs, TextInput, useMantineColorScheme } from "@mantine/core";
import { IconMathMax, IconPlus, IconTableMinus, IconTablePlus } from "@tabler/icons-react";
import { FuelExpenseDto, FuelExpenseDtoHeaders, FuelLimitDto, FuelLimitDtoHeaders } from "../dto/fuel/fuel.dto";
import { Modal, useMantineColorScheme } from "@mantine/core";
import { IconMathMax, IconPlus, IconTableMinus } from "@tabler/icons-react";
import { FuelExpenseDtoHeaders, FuelLimitDtoHeaders } from "../dto/fuel/fuel.dto";
import useSWR from "swr";
import { fetcher } from "../http/axiosInstanceNest";
import { useEffect, useState } from "react";
import { useDisclosure } from "@mantine/hooks";
import { DateInput, DatePicker } from '@mantine/dates'
import { DateInput } from '@mantine/dates'
import { SubmitHandler, useForm } from "react-hook-form";
import { AgGridReact } from "ag-grid-react";
import { AllCommunityModule, ColDef, ModuleRegistry } from 'ag-grid-community'
import { Button, Field, Input, Spinner, Tab, TabList } from "@fluentui/react-components";
ModuleRegistry.registerModules([AllCommunityModule])
@@ -92,9 +92,9 @@ export default function FuelPage() {
const [currentTab, setCurrentTab] = useState(tables[0])
const { data, isLoading } = useSWR(currentTab.get, () => fetcher(currentTab.get), { revalidateOnFocus: false })
const { isLoading } = useSWR(currentTab.get, () => fetcher(currentTab.get), { revalidateOnFocus: false })
const [openedCreateModal, { open: openCreateModal, close: closeCreateModal }] = useDisclosure(false)
const [openCreateModel, setOpenCreateModal] = useState(false)
const { colorScheme } = useMantineColorScheme()
@@ -108,49 +108,52 @@ export default function FuelPage() {
return (
<>
<ModalCreate openedCreateModal={openedCreateModal} closeCreateModal={closeCreateModal} currentTab={currentTab} />
<ModalCreate openedCreateModal={openCreateModel} closeCreateModal={() => setOpenCreateModal(false)} currentTab={currentTab} />
<Tabs defaultValue={tables[0].value} w='100%' onChange={(tab) => setCurrentTab(tables.find(table => table.value === tab) || tables[0])}>
<Tabs.List>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
<TabList defaultValue={tables[0].value} selectedValue={currentTab.value}>
{tables.map((table, index) => (
<Tabs.Tab key={index} value={table.value} leftSection={table.icon}>
<Tab key={index} value={table.value} icon={table.icon} onClick={() => setCurrentTab(table)}>
{table.label}
</Tabs.Tab>
</Tab>
))}
</Tabs.List>
</TabList>
<Flex p='sm'>
<Button leftSection={<IconPlus />} onClick={openCreateModal}>
<div style={{ display: 'flex', padding: '1rem' }}>
<Button appearance='primary' icon={<IconPlus />} onClick={() => setOpenCreateModal(true)}>
Добавить
</Button>
</Flex>
</div>
{tables.map((table, index) => (
<Tabs.Panel key={index} value={table.value} w='100%' h='100%'>
{isLoading ?
<Flex w='100%' justify={'center'} p='md'>
<Loader />
</Flex>
:
<>
<AgGridReact
//rowData={data}
rowData={[
Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {}),
Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {})
]}
columnDefs={Object.keys(table.headers).map((header) => ({
field: header
})) as ColDef[]}
defaultColDef={{
flex: 1,
}}
/>
</>
}
</Tabs.Panel>
))}
</Tabs>
{tables.map((table, index) => {
if (table.value === currentTab.value) {
return (
isLoading ?
<div style={{ display: 'flex', width: '100%', justifyContent: 'center', padding: '1rem' }}>
<Spinner />
</div>
:
<>
<AgGridReact
key={index}
//rowData={data}
rowData={[
Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {}),
Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {})
]}
columnDefs={Object.keys(table.headers).map((header) => ({
field: header
})) as ColDef[]}
defaultColDef={{
flex: 1,
}}
/>
</>
)
}
}
)}
</div>
</>
)
}
@@ -164,7 +167,9 @@ const ModalCreate = ({
closeCreateModal: () => void
currentTab: ITableSchema
}) => {
const { register, handleSubmit, reset, watch, formState: { errors, isSubmitting, dirtyFields, isValid } } = useForm({
const { register, handleSubmit,
//formState: { errors, isSubmitting, dirtyFields, isValid }
} = useForm({
mode: 'onChange',
})
@@ -174,9 +179,7 @@ const ModalCreate = ({
return (
<Modal withinPortal opened={openedCreateModal} onClose={closeCreateModal}>
<LoadingOverlay visible={isSubmitting} />
<Flex direction='column' gap='sm' component='form' onSubmit={handleSubmit(onSubmit)}>
<form style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }} onSubmit={handleSubmit(onSubmit)}>
{currentTab.post_include.map((header, index) => {
switch (header.field_type) {
case 'date':
@@ -185,23 +188,27 @@ const ModalCreate = ({
)
case 'text':
return (
<TextInput key={index} label={header.field} {...register(header.field, {
required: true
})} />
<Field key={index} label={header.field}>
<Input {...register(header.field, {
required: true
})} />
</Field>
)
default:
return (
<TextInput key={index} label={header.field} {...register(header.field, {
required: true
})} />
<Field key={index} label={header.field}>
<Input {...register(header.field, {
required: true
})} />
</Field>
)
}
})}
<Button mt='xl' type='submit'>
<Button style={{ marginTop: '2rem' }} appearance="primary" type='submit'>
Добавить
</Button>
</Flex>
</form>
</Modal>
)
}

View File

@@ -1,6 +1,6 @@
import { Card, Flex, SimpleGrid, Text } from "@mantine/core";
import { IconBuildingFactory2, IconFiles, IconMap, IconReport, IconServer, IconShield, IconUsers } from "@tabler/icons-react";
import { ReactNode } from "react";
import { CompoundButton, Text } from "@fluentui/react-components";
import { BuildingColor, CloudColor, DocumentColor, FormColor, MapFilled, PeopleListColor, ShieldColor } from "@fluentui/react-icons";
//import { IconBuildingFactory2, IconFiles, IconMap, IconReport, IconServer, IconShield, IconUsers } from "@tabler/icons-react";
import { useNavigate } from "react-router-dom";
export default function Main() {
@@ -8,47 +8,56 @@ export default function Main() {
interface CustomCardProps {
link: string;
icon: ReactNode;
icon: any;
label: string;
secondaryLabel?: string;
}
const CustomCard = ({
link,
icon,
label
label,
secondaryLabel
}: CustomCardProps) => {
return (
<Card
<CompoundButton
onClick={() => navigate(link)}
withBorder
style={{ cursor: 'pointer', userSelect: 'none' }}
icon={icon}
secondaryContent={secondaryLabel}
>
<Flex mih='50'>
{icon}
</Flex>
<Text fw={500} size="lg" mt="md">
<Text weight={'bold'} size={400}>
{label}
</Text>
</Card>
</CompoundButton>
)
}
return (
<Flex w={'100%'} h={'100%'} direction='column' gap='sm' p='sm'>
<Text size="xl" fw={700}>
<div style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: '1rem',
padding: '1rem'
}}>
<Text size={600} weight='bold'>
Главная
</Text>
<SimpleGrid cols={{ xs: 1, md: 3 }}>
<CustomCard link="/user" icon={<IconUsers size='50' color="#6495ED" />} label="Пользователи" />
<CustomCard link="/role" icon={<IconShield size='50' color="#6495ED" />} label="Роли" />
<CustomCard link="/documents" icon={<IconFiles size='50' color="#6495ED" />} label="Документы" />
<CustomCard link="/reports" icon={<IconReport size='50' color="#6495ED" />} label="Отчеты" />
<CustomCard link="/servers" icon={<IconServer size='50' color="#6495ED" />} label="Серверы" />
<CustomCard link="/boilers" icon={<IconBuildingFactory2 size='50' color="#6495ED" />} label="Котельные" />
<CustomCard link="/map-test" icon={<IconMap size='50' color="#6495ED" />} label="ИКС" />
</SimpleGrid>
</Flex>
<div style={{
display: 'flex',
gap: '1rem',
flexWrap: 'wrap'
}}>
<CustomCard link="/user" icon={<PeopleListColor color="#6495ED" />} label="Пользователи" secondaryLabel="Управление пользователями"/>
<CustomCard link="/role" icon={<ShieldColor color="#6495ED" />} label="Роли" />
<CustomCard link="/documents" icon={<DocumentColor color="#6495ED" />} label="Документы" secondaryLabel="Обзор файлов/документов"/>
<CustomCard link="/reports" icon={<FormColor color="#6495ED" />} label="Отчеты" secondaryLabel="Просмотр и создание отчетных документов"/>
<CustomCard link="/servers" icon={<CloudColor color="#6495ED" />} label="Серверы" secondaryLabel="Мониторинг серверов"/>
<CustomCard link="/boilers" icon={<BuildingColor color="#6495ED" />} label="Котельные" />
<CustomCard link="/map-test" icon={<MapFilled color="#6495ED" />} label="ИКС" secondaryLabel="Инженерно-картографическая система"/>
</div>
</div>
)
}

View File

@@ -1,14 +1,19 @@
import { Container, Stack, Tabs } from '@mantine/core'
import MapComponent from '../components/map/MapComponent'
import { useEffect } from 'react'
import { initializeObjectsState } from '../store/objects'
import { deleteMapTab, setCurrentTab, useAppStore } from '../store/app'
import { initializeMapState, useMapStore } from '../store/map'
import { v4 as uuidv4 } from 'uuid'
import { useEffect } from "react";
import { v4 as uuidv4 } from "uuid";
import { Tab, TabList } from "@fluentui/react-tabs";
import MapComponent from "../components/map/MapComponent";
import {
useAppStore,
setCurrentTab,
deleteMapTab,
} from "../store/app";
import { initializeMapState, useMapStore } from "../store/map";
import { initializeObjectsState } from "../store/objects";
function MapTest() {
const { mapTab, currentTab } = useAppStore()
const { id } = useMapStore()
const { mapTab, currentTab } = useAppStore();
const { id } = useMapStore();
const tabs = [
{
@@ -18,65 +23,65 @@ function MapTest() {
district: 146,
},
// {
// id: uuidv4(),
// year: 2023,
// region: 11,
// district: 146,
// id: uuidv4(),
// year: 2023,
// region: 11,
// district: 146,
// },
]
];
useEffect(() => {
tabs.map(tab => useAppStore.setState((state) => {
initializeObjectsState(tab.id, tab.region, tab.district, null, tab.year)
initializeMapState(tab.id)
tabs.forEach((tab) => {
useAppStore.setState((state) => {
initializeObjectsState(tab.id, tab.region, tab.district, null, tab.year);
initializeMapState(tab.id);
return {
mapTab: {
...state.mapTab,
[tab.id]: {
year: tab.year,
region: tab.region,
district: tab.district
}
}
}
}))
return {
mapTab: {
...state.mapTab,
[tab.id]: {
year: tab.year,
region: tab.region,
district: tab.district,
},
},
};
});
});
setCurrentTab(tabs[0].id)
setCurrentTab(tabs[0].id);
return () => {
tabs.map(tab => deleteMapTab(tab.id))
}
}, [])
tabs.forEach((tab) => deleteMapTab(tab.id));
};
}, []);
return (
<Container fluid w='100%' pos='relative' p={0}>
<Tabs h='100%' variant='default' value={currentTab} onChange={setCurrentTab} keepMounted={true}>
<Stack gap={0} h='100%'>
<Tabs.List>
{Object.entries(mapTab).map(([key]) => (
<Tabs.Tab value={key} key={key}>
{id[key]?.mapLabel}
</Tabs.Tab>
))}
</Tabs.List>
<div style={{ height: "100%", width: "100%", position: "relative" }}>
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<TabList
selectedValue={currentTab}
onTabSelect={(_, data) => setCurrentTab(data.value as string)}
>
{Object.entries(mapTab).map(([key]) => (
<Tabs.Panel value={key} key={key} h='100%' pos='relative'>
<MapComponent
key={key}
id={key}
active={currentTab === key}
/>
</Tabs.Panel>
<Tab value={key} key={key}>
{id[key]?.mapLabel ?? `Tab ${key}`}
</Tab>
))}
</Stack>
</TabList>
</Tabs>
</Container>
)
<div style={{ flexGrow: 1, position: "relative" }}>
{Object.entries(mapTab).map(([key]) =>
currentTab === key ? (
<div key={key} style={{ height: "100%", position: "relative" }}>
<MapComponent key={key} id={key} active={true} />
</div>
) : null
)}
</div>
</div>
</div>
);
}
export default MapTest
export default MapTest;

View File

@@ -1,5 +1,5 @@
import { Card } from '@fluentui/react-components';
import { useEffect, useState } from 'react'
import { Card, Flex } from '@mantine/core';
function CardComponent({
url,
@@ -7,10 +7,10 @@ function CardComponent({
}: { url: string, is_alive: boolean }) {
return (
<Card>
<Flex p='sm' direction='column'>
<div>
<p>{url}</p>
<p>{JSON.stringify(is_alive)}</p>
</Flex>
</div>
</Card>
)
}
@@ -38,11 +38,15 @@ export default function MonitorPage() {
return (
<div>
<Flex direction='column' gap='sm'>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem'
}}>
{servers.length > 0 && servers.map((server: { name: string, status: boolean }) => (
<CardComponent url={server.name} is_alive={server.status} />
))}
</Flex>
</div>
</div>
)
}

View File

@@ -1,15 +1,29 @@
import { Flex, Text } from "@mantine/core";
import { Text } from "@fluentui/react-components";
import { makeStyles } from "@fluentui/react-components";
import { IconError404 } from "@tabler/icons-react";
const useStyles = makeStyles({
root: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}
})
export default function NotFound() {
const classes = useStyles()
return (
<Flex w={'100%'} h={'100%'} p='sm' gap='sm' align='center' justify='center'>
<Flex direction='column' gap='sm' align='center'>
<div style={{
width: '100%',
height: '100%',
}}>
<div className={classes.root}>
<IconError404 size={100} />
<Text size="xl" fw={500} ta='center'>
<Text size={500} weight='medium' align='center'>
Запрашиваемая страница не найдена.
</Text>
</Flex>
</Flex>
</div>
</div>
)
}

View File

@@ -1,10 +1,10 @@
import { ActionIcon, Button, Flex, Group, Stack, Text, TextInput } from "@mantine/core"
import { useEffect, useState } from "react"
import createReport, { listCommands } from 'docx-templates'
import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone'
import { IconFileTypeDocx, IconPlus, IconUpload, IconX } from "@tabler/icons-react"
import { CommandSummary } from "docx-templates/lib/types"
import { Control, Controller, FieldValues, SubmitHandler, useFieldArray, useForm, UseFormRegister } from "react-hook-form"
import { Button, Field, Input, Text } from "@fluentui/react-components"
const xslTemplate = `<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
@@ -1024,11 +1024,11 @@ const FormLoop = ({
})
return (
<Stack align="center">
<Stack w='100%' key={command.code}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }} key={command.code}>
{
fields.map((field, index) => (
<Flex w='100%' justify='space-between' align='flex-end' key={field.id}>
<div style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'flex-end' }} key={field.id}>
{command.children &&
command.children.map(c =>
renderCommand(
@@ -1040,27 +1040,25 @@ const FormLoop = ({
`${command.code}.${index}.${c.code}`
)
)}
<Button variant='subtle' onClick={() => {
<Button appearance='subtle' onClick={() => {
remove(index)
}}>
<IconX />
</Button>
</Flex>
</div>
))
}
</Stack>
</div>
<ActionIcon onClick={() => {
<Button icon={<IconPlus />} onClick={() => {
if (command.children) {
append(command.children.map(c => c.code).reduce((acc, key) => {
acc[key] = '';
return acc;
}, {} as Record<string, string>))
}
}}>
<IconPlus />
</ActionIcon>
</Stack>
}} />
</div>
)
}
@@ -1074,18 +1072,17 @@ const renderCommand = (
) => {
if (command.type === 'INS') {
return (
<TextInput
label={label}
key={key}
{...register(name)}
/>
<Field label={label}
key={key}>
<Input {...register(name)} />
</Field>
)
}
if (command.type === 'IMAGE') {
return (
<Stack gap={0}>
<Text size='sm' fw={500}>{command.code}</Text>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Text size={200} weight="semibold">{command.code}</Text>
<Controller
key={key}
name={name}
@@ -1108,7 +1105,7 @@ const renderCommand = (
}}
maxFiles={1}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<div style={{ display: 'flex', justifyContent: 'center', gap: '2rem', minHeight: '220px', pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
@@ -1120,15 +1117,15 @@ const renderCommand = (
</Dropzone.Idle>
<div>
<Text size="xl" inline>
<Text size={300}>
Перетащите файлы сюда или нажмите, чтобы выбрать их
</Text>
</div>
</Group>
</div>
</Dropzone>
)}
/>
</Stack>
</div>
)
}
}
@@ -1240,21 +1237,21 @@ const TemplateForm = ({
if (commandList) {
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Stack>
<div style={{ display: 'flex', flexDirection: 'column' }}>
{commandList.map(command => {
if (command.type === 'FOR') {
return (
<Stack gap={0} key={command.code}>
<Text size='sm' fw={500}>{command.code}</Text>
<div style={{ display: 'flex', flexDirection: 'column' }} key={command.code}>
<Text size={200} weight='semibold'>{command.code}</Text>
<FormLoop control={control} register={register} command={command} />
</Stack>
</div>
)
} else {
return renderCommand(control, register, command, command.code, command.code, command.code)
}
})}
<Button ml='auto' w='fit-content' type='submit'>Сохранить</Button>
</Stack>
<Button style={{ marginLeft: 'auto', width: 'fit-content' }} type='submit'>Сохранить</Button>
</div>
</form>
)
}
@@ -1262,13 +1259,13 @@ const TemplateForm = ({
const PrintReport = () => {
return (
<Stack p='sm' gap='sm' w='100%'>
<div style={{ display: 'flex', flexDirection: 'column', padding: '1rem', gap: '1rem', width: '100%' }}>
<TemplateForm templateUrl="/template_table.docx" />
<Flex gap='sm'>
<div style={{ display: 'flex', gap: '1rem' }}>
<Button onClick={handleGenerateExcel}>Сохранить в Excel</Button>
</Flex>
</Stack>
</div>
</div>
)
}

View File

@@ -3,8 +3,8 @@ import { useCities, useReport, useReportExport } from "../hooks/swrHooks"
import { useDebounce } from "@uidotdev/usehooks"
import { ICity } from "../interfaces/fuel"
import { mutate } from "swr"
import { ActionIcon, Autocomplete, Badge, Button, CloseButton, Flex, ScrollAreaAutosize, Table } from "@mantine/core"
import { IconRefresh } from "@tabler/icons-react"
import { Badge, Button, Combobox, createTableColumn, DataGrid, DataGridBody, DataGridCell, DataGridHeader, DataGridHeaderCell, DataGridRow, Option, TableCellLayout, TableColumnDefinition } from "@fluentui/react-components"
export default function Reports() {
const [download, setDownload] = useState(false)
@@ -40,99 +40,125 @@ export default function Reports() {
}
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Flex component="form" gap={'sm'}>
<div style={{
width: '100%',
height: '100%',
padding: '1rem'
}}>
<form style={{
display: 'flex',
gap: '0.5rem'
}}>
{/* <SearchableSelect /> */}
<Autocomplete
placeholder="Населенный пункт"
flex={'1'}
data={cities ? cities.map((item: ICity) => ({ label: item.name, value: item.id.toString() })) : []}
onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearch(value)}
onOptionSubmit={(value) => setSelectedOption(Number(value))}
rightSection={
search !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearch('')
setSelectedOption(null)
}}
aria-label="Clear value"
/>
)
}
value={search}
/>
<Combobox clearable placeholder="Населенный пункт" onOptionSelect={(_, data) => {
setSelectedOption(Number(data.optionValue))
setSearch(data.optionText ?? "")
}} value={search} onChange={(e) => setSearch(e.currentTarget.value)}>
{cities && Array.isArray(cities) && cities.map((item: ICity) => (
<Option key={item.id} value={item.id.toString()}>
{item.name}
</Option>
))}
</Combobox>
<ActionIcon size='auto' variant='transparent' onClick={() => refreshReport()}>
<IconRefresh />
</ActionIcon>
<Button icon={<IconRefresh />} appearance="subtle" onClick={() => refreshReport()}>
</Button>
<Button disabled={!selectedOption} onClick={() => exportReport()}>
Экспорт
</Button>
</Flex>
</form>
{report &&
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{[
{ field: 'id', headerName: '№', width: 70 },
...Object.keys(report).map(key => ({
field: key,
headerName: key.charAt(0).toUpperCase() + key.slice(1),
width: 150
}))
].map(column => (
<Table.Th key={column.headerName}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{[...new Set(Object.keys(report).flatMap(key => Object.keys(report[key])))].map(id => {
const row: Record<string, unknown> = { id: Number(id) };
Object.keys(report).forEach(key => {
row[key] = report[key][id];
});
return (<Table.Tr key={row.id as number}>
{[
{ field: 'id', headerName: '№', width: 70 },
...Object.keys(report).map(key => ({
field: key,
headerName: key.charAt(0).toUpperCase() + key.slice(1),
width: 150
}))
].map(column => {
if (column.field === 'Активность') {
return (
row['Активность'] ? (
<Table.Td key={`${row.id}-${column.headerName}`}>
<Badge fullWidth variant="light">
Активен
</Badge>
</Table.Td>
<div style={{
display: 'flex',
width: '100%',
overflow: 'auto'
}}>
{report &&
<ReportTable report={report} />
}
</div>
) : (
<Table.Td key={`${row.id}-${column.headerName}`}>
<Badge color="gray" fullWidth variant="light">
Отключен
</Badge>
</Table.Td>
)
)
}
return (
<Table.Td key={`${row.id}-${column.headerName}`}>{row[column.field] as string}</Table.Td>
)
})}
</Table.Tr>)
})}
</Table.Tbody>
</Table>
}
</ScrollAreaAutosize>
</div>
)
}
}
interface ReportType {
[key: string]: Record<string, unknown>;
}
function ReportTable({ report }: { report: ReportType }) {
// Build column definitions
const columns: TableColumnDefinition<any>[] = [
createTableColumn({
columnId: "id",
renderHeaderCell: () => {
return "№"
},
renderCell: (item) => <TableCellLayout>{item.id}</TableCellLayout>,
}),
...Object.keys(report).map((key) =>
createTableColumn({
columnId: key,
renderHeaderCell: () => {
return key.charAt(0).toUpperCase() + key.slice(1)
},
renderCell: (item: any) => {
if (key === "Активность") {
return (
<TableCellLayout>
{item["Активность"] ? (
<Badge color="success">Активен</Badge>
) : (
<Badge color="danger">Отключен</Badge>
)}
</TableCellLayout>
);
}
return <TableCellLayout>{item[key] as string}</TableCellLayout>;
},
})
),
];
// Build rows from report (same logic you used)
const items = [...new Set(Object.keys(report).flatMap((key) => Object.keys(report[key])))]
.map((id) => {
const row: Record<string, unknown> = { id: Number(id) };
Object.keys(report).forEach((key) => {
row[key] = report[key][id];
});
return row;
});
return (
<DataGrid
items={items}
columns={columns}
sortable
focusMode='row_unstable'
resizableColumns
resizableColumnsOptions={{ autoFitColumns: false }}
size='extra-small'
>
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell }) => (
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
)}
</DataGridRow>
</DataGridHeader>
<DataGridBody>
{({ item, rowId }) => (
<DataGridRow key={rowId}>
{({ renderCell }) => <DataGridCell>{renderCell(item)}</DataGridCell>}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>
);
}

View File

@@ -1,8 +1,8 @@
import { useRoles } from '../hooks/swrHooks'
import { CreateField } from '../interfaces/create'
import RoleService from '../services/RoleService'
import { Loader, Stack } from '@mantine/core'
import CustomTable from '../components/CustomTable'
import { Spinner } from '@fluentui/react-components'
export default function Roles() {
const { roles, isError, isLoading } = useRoles()
@@ -13,30 +13,34 @@ export default function Roles() {
]
if (isError) return <div>Произошла ошибка при получении данных.</div>
if (isLoading) return <Loader />
if (isLoading) return <Spinner />
return (
<Stack w={'100%'} h={'100%'} p='sm'>
<div style={{
width: '100%',
height: '100%',
padding: '1rem'
}} >
<CustomTable
createFields={createFields}
submitHandler={RoleService.createRole}
data={roles} columns={[
{
accessorKey: 'id',
name: 'id',
header: 'id',
cell: (info) => info.getValue(),
type: 'number'
},
{
accessorKey: 'name',
name: 'name',
header: 'Название',
cell: (info) => info.getValue(),
type: 'string'
},
{
accessorKey: 'description',
name: 'description',
header: 'Описание',
cell: (info) => info.getValue(),
type: 'string'
},
]} />
</Stack>
</div>
)
}

View File

@@ -3,46 +3,52 @@ import ServersView from "../components/ServersView"
import ServerIpsView from "../components/ServerIpsView"
import ServerHardware from "../components/ServerHardware"
import ServerStorage from "../components/ServerStorages"
import { Flex, ScrollAreaAutosize, Tabs } from "@mantine/core"
import { Tab, TabList } from "@fluentui/react-components"
export default function Servers() {
const [currentTab, setCurrentTab] = useState<string | null>('0')
const tabs = [{
id: 'servers',
name: 'Серверы',
content: <ServersView />
},
{
id: 'ips',
name: 'IP-адреса',
content: <ServerIpsView />
},
{
id: 'hardware',
name: 'Hardware',
content: <ServerHardware />
},
{
id: 'storage',
name: 'Хранилище',
content: <ServerStorage />
}
]
const [selectedTab, setSelectedTab] = useState<string | unknown>(tabs[0].id)
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Flex gap='sm' direction='column'>
<Tabs value={currentTab} onChange={setCurrentTab}>
<Tabs.List>
<Tabs.Tab value="0">Серверы</Tabs.Tab>
<Tabs.Tab value="1">IP-адреса</Tabs.Tab>
<Tabs.Tab value="3">Hardware</Tabs.Tab>
<Tabs.Tab value="4">Storages</Tabs.Tab>
</Tabs.List>
<div style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
gap: '1rem',
padding: '1rem'
}}>
<TabList selectedValue={selectedTab} onTabSelect={(_, data) => setSelectedTab(data.value)}>
{tabs.map(tab => (
<Tab value={tab.id}>{tab.name}</Tab>
))}
</TabList>
<Tabs.Panel value="0" pt='sm'>
<ServersView />
</Tabs.Panel>
<div>
{tabs.find(tab => tab.id === selectedTab)?.content}
</div>
</div>
<Tabs.Panel value="1" pt='sm'>
<ServerIpsView />
</Tabs.Panel>
<Tabs.Panel value="2" pt='sm'>
<ServerHardware />
</Tabs.Panel>
<Tabs.Panel value="3" pt='sm'>
<ServerStorage />
</Tabs.Panel>
</Tabs>
</Flex>
{/* <BarChart
xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]}
series={[{ data: [4, 3, 5] }, { data: [1, 6, 3] }, { data: [2, 5, 6] }]}
width={500}
height={300}
/> */}
</ScrollAreaAutosize>
)
}

View File

@@ -5,7 +5,6 @@ import { CreateField } from "../interfaces/create"
import { IUser } from "../interfaces/user"
import FormFields from "../components/FormFields"
import AuthService from "../services/AuthService"
import { Flex, ScrollAreaAutosize } from "@mantine/core"
export default function Settings() {
const { token } = useAuthStore()
@@ -39,13 +38,20 @@ export default function Settings() {
]
return (
<ScrollAreaAutosize
w={'100%'}
h={'100%'}
p='sm'
<div
style={{
width: '100%',
height: '100%',
padding: '1rem'
}}
>
{currentUser &&
<Flex direction='column' gap='sm' w='100%'>
<div style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: '1rem'
}}>
<FormFields
fields={profileFields}
defaultValues={currentUser}
@@ -61,8 +67,8 @@ export default function Settings() {
submitHandler={(data) => AuthService.updatePassword({ id: currentUser.id, ...data })}
title="Смена пароля"
/>
</Flex>
</div>
}
</ScrollAreaAutosize>
</div>
)
}

View File

@@ -3,8 +3,9 @@ import { IRole } from "../interfaces/role"
import { useEffect, useState } from "react"
import { CreateField } from "../interfaces/create"
import UserService from "../services/UserService"
import { Flex, Loader, Stack } from "@mantine/core"
import CustomTable from "../components/CustomTable"
import { Spinner } from "@fluentui/react-components"
import { IUser } from "../interfaces/user"
export default function Users() {
const { users, isError, isLoading } = useUsers()
@@ -13,12 +14,20 @@ export default function Users() {
const [roleOptions, setRoleOptions] = useState<{ label: string, value: string }[]>()
const [data, setData] = useState<IUser[]>([])
useEffect(() => {
if (Array.isArray(roles)) {
setRoleOptions(roles.map((role: IRole) => ({ label: role.name, value: role.id.toString() })))
}
}, [roles])
useEffect(() => {
if (users) {
setData(users)
}
}, [users])
const createFields: CreateField[] = [
{ key: 'email', headerName: 'E-mail', type: 'string', required: true, defaultValue: '' },
{ key: 'login', headerName: 'Логин', type: 'string', required: true, defaultValue: '' },
@@ -36,57 +45,71 @@ export default function Users() {
if (isLoading) {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<Loader />
</Flex>
<div>
<Spinner />
</div>
)
}
return (
<Stack w={'100%'} h={'100%'} p='xs'>
{Array.isArray(roleOptions) &&
<div style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
padding: '1rem'
}}>
{Array.isArray(roleOptions) && Array.isArray(data) &&
<CustomTable
createFields={createFields}
submitHandler={UserService.createUser}
data={users}
data={data}
onEditCell={(rowId, columnId, value) => {
console.log(rowId, columnId, value)
setData((prev) =>
prev.map((row) =>
row.id === rowId ? { ...row, [columnId]: value } : row
)
)
}}
columns={[
{
accessorKey: 'email',
name: 'email',
header: 'E-mail',
cell: (info) => info.getValue(),
type: "string"
},
{
accessorKey: 'login',
name: 'login',
header: 'Логин',
cell: (info) => info.getValue(),
type: "string"
},
{
accessorKey: 'phone',
name: 'phone',
header: 'Телефон',
cell: (info) => info.getValue(),
type: "string"
},
{
accessorKey: 'name',
name: 'name',
header: 'Имя',
cell: (info) => info.getValue(),
type: "string"
},
{
accessorKey: 'surname',
name: 'surname',
header: 'Фамилия',
cell: (info) => info.getValue(),
type: "string"
},
{
accessorKey: 'is_active',
name: 'is_active',
header: 'Активен',
cell: (info) => info.getValue(),
type: "boolean"
},
{
accessorKey: 'role_id',
name: 'role_id',
header: 'Роль',
cell: (info) => info.getValue(),
type: "dictionary" //TODO: dictionary getter by id
}
]} />
}
</Stack>
</div>
)
}

View File

@@ -1,8 +1,8 @@
import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form';
import AuthService from '../../services/AuthService';
import { Button, Flex, Loader, Paper, Text, TextInput, Transition } from '@mantine/core';
import { IconCheck } from '@tabler/icons-react';
import { Button, Input, Spinner, Text } from '@fluentui/react-components';
interface PasswordResetProps {
email: string;
@@ -11,7 +11,7 @@ interface PasswordResetProps {
function PasswordReset() {
const [success, setSuccess] = useState(false)
const { register, handleSubmit, watch, setError, formState: { errors, isSubmitting } } = useForm<PasswordResetProps>({
const { register, handleSubmit, watch, setError, formState: { isSubmitting } } = useForm<PasswordResetProps>({
defaultValues: {
email: ''
}
@@ -31,65 +31,81 @@ function PasswordReset() {
}
return (
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Flex direction='column' gap='sm'>
<Text size="xl" fw={500}>
Восстановление пароля
</Text>
<div style={{
display: 'flex',
margin: 'auto',
flexDirection: 'column',
gap: '1rem',
maxWidth: '400px',
width: '100%',
height: 'min-content',
borderRadius: '1rem',
border: '1px solid #00000030',
padding: '2rem'
}}>
<Text size={600} weight='medium'>
Восстановление пароля
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
{!success &&
<Transition mounted={!success} transition='fade'>
{(styles) =>
<Flex style={styles} direction='column' gap={'md'}>
<Text>
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
{!success &&
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem'
}}>
<Text>
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
</Text>
<TextInput
label='E-mail'
required
{...register('email', { required: 'Введите E-mail' })}
error={errors.email?.message}
/>
<Input
placeholder='E-mail'
required
{...register('email', { required: 'Введите E-mail' })}
//error={errors.email?.message}
/>
<Flex gap='sm'>
<Button flex={1} type="submit" disabled={isSubmitting || watch('email').length == 0} variant='filled'>
{isSubmitting ? <Loader size={16} /> : 'Восстановить пароль'}
</Button>
<div style={{
display: 'flex',
width: '100%',
justifyContent: 'space-between'
}}>
<Button type="submit" disabled={isSubmitting || watch('email').length == 0} appearance='primary'>
{isSubmitting ? <Spinner /> : 'Восстановить пароль'}
</Button>
<Button flex={1} component='a' href="/auth/signin" type="button" variant='light'>
Назад
</Button>
</Flex>
<Button as='a' href="/auth/signin" type="button" appearance='subtle'>
Назад
</Button>
</div>
</Flex>
}
</Transition>
}
{success &&
<Transition mounted={!success} transition='scale'>
{(styles) =>
<Flex style={styles} direction='column' gap='sm'>
<Flex align='center' gap='sm'>
<IconCheck />
<Text>
На указанный адрес было отправлено письмо с новыми данными для авторизации.
</Text>
</Flex>
<Flex gap='sm'>
<Button component='a' href="/auth/signin" type="button">
Войти
</Button>
</Flex>
</Flex>
}
</Transition>
}
</form>
</Flex>
</Paper>
</div>
}
{success &&
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '1rem'
}}>
<IconCheck />
<Text>
На указанный адрес было отправлено письмо с новыми данными для авторизации.
</Text>
</div>
<div style={{ display: 'flex' }}>
<Button as='a' href="/auth/signin" type="button">
Войти
</Button>
</div>
</div>
}
</form>
</div>
)
}

View File

@@ -5,7 +5,8 @@ import { login, setUserData } from '../../store/auth';
import { useNavigate } from 'react-router-dom';
import AuthService from '../../services/AuthService';
import UserService from '../../services/UserService';
import { Button, Flex, Loader, Paper, Text, TextInput } from '@mantine/core';
import { Button, Field, Input, Link, Spinner, Text } from '@fluentui/react-components';
import { pages } from '../../constants/app';
const SignIn = () => {
const { register, handleSubmit, setError, formState: { errors, isSubmitting, isValid } } = useForm<LoginFormData>({
@@ -46,53 +47,63 @@ const SignIn = () => {
message: (err as { detail: string })?.detail
})
}
}
};
return (
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Flex direction='column' gap='sm'>
<Text size="xl" fw={500}>
Вход
</Text>
<div style={{
display: 'flex',
margin: 'auto',
flexDirection: 'column',
gap: '1rem',
maxWidth: '400px',
width: '100%',
height: 'min-content',
borderRadius: '1rem',
border: '1px solid #00000030',
padding: '2rem'
}}>
<Text align='center' size={500} weight='bold'>
Вход
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
<Flex direction='column' gap='sm'>
<TextInput
label='Логин'
<form onSubmit={handleSubmit(onSubmit)}>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem'
}}>
<Field label={'Логин'} validationState={errors.username?.message ? 'error' : 'none'}>
<Input
required
{...register('username', { required: 'Введите логин' })}
error={errors.username?.message}
/>
</Field>
<TextInput
label='Пароль'
type='password'
<Field label={'Пароль'} validationState={errors.password?.message ? 'error' : 'none'}>
<Input
required
type='password'
{...register('password', { required: 'Введите пароль' })}
error={errors.password?.message}
/>
</Field>
<Flex justify='flex-end' gap='sm'>
<Button component='a' href='/auth/password-reset' variant='transparent'>
Восстановить пароль
</Button>
</Flex>
<Link href='/auth/password-reset'>
Восстановить пароль
</Link>
<Flex gap='sm'>
<Button disabled={!isValid} type="submit" flex={1} variant='filled'>
{isSubmitting ? <Loader size={16} /> : 'Вход'}
</Button>
<Button disabled={!isValid} type="submit" appearance='primary' icon={isSubmitting ? <Spinner size='extra-tiny' /> : undefined}>
Вход
</Button>
{/* <Button component='a' flex={1} href='/auth/signup' type="button" variant='light'>
Регистрация
</Button> */}
</Flex>
</Flex>
</form>
</Flex>
</Paper>
{pages.find(page => page.path === '/auth/signup')?.enabled &&
<Button as='a' href='/auth/signup' type="button" appearance='subtle'>
Регистрация
</Button>}
</div>
</form>
</div>
);
};

View File

@@ -1,7 +1,7 @@
import { useForm, SubmitHandler } from 'react-hook-form';
import UserService from '../../services/UserService';
import { IUser } from '../../interfaces/user';
import { Button, Flex, Loader, Paper, Text, TextInput } from '@mantine/core';
import { Button, Field, Input, Spinner, Text } from '@fluentui/react-components';
const SignUp = () => {
const { register, handleSubmit, formState: { errors, isValid, isSubmitting } } = useForm<IUser>({
@@ -26,66 +26,76 @@ const SignUp = () => {
};
return (
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Flex direction='column' gap='sm'>
<Text size="xl" fw={500}>
Регистрация
</Text>
<div style={{
display: 'flex',
margin: 'auto',
flexDirection: 'column',
gap: '1rem',
maxWidth: '400px',
width: '100%',
height: 'min-content',
borderRadius: '1rem',
border: '1px solid #00000030',
padding: '2rem'
}}>
<Text align='center' size={500} weight='bold'>
Регистрация
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
<Flex direction='column' gap='sm'>
<TextInput
label='Email'
<form onSubmit={handleSubmit(onSubmit)}>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem'
}}>
<Field label={'Email'} validationState={errors.email?.message ? 'error' : 'none'}>
<Input
required
{...register('email', { required: 'Email обязателен' })}
error={errors.email?.message}
/>
</Field>
<TextInput
label='Логин'
<Field label={'Логин'} validationState={errors.login?.message ? 'error' : 'none'}>
<Input
required
{...register('login', { required: 'Логин обязателен' })}
error={errors.login?.message}
/>
</Field>
<TextInput
label='Телефон'
<Field label={'Телефон'} validationState={errors.phone?.message ? 'error' : 'none'}>
<Input
required
{...register('phone')}
error={errors.phone?.message}
{...register('phone', { required: 'Телефон обязателен' })}
/>
</Field>
<TextInput
label='Имя'
<Field label={'Имя'} validationState={errors.name?.message ? 'error' : 'none'}>
<Input
required
{...register('name')}
error={errors.name?.message}
{...register('name', { required: 'Имя обязательно' })}
/>
</Field>
<TextInput
label='Фамилия'
<Field label={'Фамилия'} validationState={errors.surname?.message ? 'error' : 'none'}>
<Input
required
{...register('surname')}
error={errors.surname?.message}
{...register('surname', { required: 'Фамилия обязательна' })}
/>
</Field>
<TextInput
label='Пароль'
type="password"
<Field label={'Пароль'} validationState={errors.password?.message ? 'error' : 'none'}>
<Input
required
{...register('password', { required: 'Пароль обязателен' })}
error={errors.password?.message}
/>
</Field>
<Flex gap='sm'>
<Button disabled={!isValid} type="submit" flex={1} variant='filled'>
{isSubmitting ? <Loader size={16} /> : 'Зарегистрироваться'}
</Button>
</Flex>
</Flex>
</form>
</Flex>
</Paper>
<Button disabled={!isValid} type="submit" appearance='primary'>
{isSubmitting ? <Spinner /> : 'Зарегистрироваться'}
</Button>
</div>
</form>
</div>
);
};