76 Commits

Author SHA1 Message Date
2bf657e8ed Update 2025-01-10 11:38:00 +09:00
59fded5cab Temporary disable nodes (prisma) 2024-12-20 10:15:56 +09:00
242ed1aee2 Rename client_app -> web_client 2024-12-20 10:01:46 +09:00
75d6420d6b Remove unused setter 2024-12-20 09:45:38 +09:00
7e8d1f50c8 Add CLIENT_PORT env 2024-12-20 09:39:12 +09:00
f04fc5f575 Remove unused settings icon; Update .env.example 2024-12-19 17:52:22 +09:00
d2cb6d9cac MapComponent update 2024-12-19 17:41:56 +09:00
a31ede2669 MapLayers reactive controls; MapSource: add customMapSource for tiles; 2024-12-19 17:41:11 +09:00
b1df6ca398 Added TCBParameter tables; MapToolbar: add edit, remove test; 2024-12-19 17:40:11 +09:00
b54c2f0e20 Disable dev pages; FolderViewer table view; CustomTable type safety; 2024-12-19 17:37:01 +09:00
65b7e275fe SignIn type safety; remove unused formatNumericValue; 2024-12-19 17:06:46 +09:00
59dddfa02c Move pages const into a separate file 2024-12-19 17:02:58 +09:00
5218ee851f Add edit ToolType; Users type; Reports cleanup; Type safery 2024-12-19 17:00:11 +09:00
71055e7cd0 Types cleanup; type safe refactoring; Remove unused clickhouse service; ems: GET all lines; Roles types; 2024-12-19 16:53:50 +09:00
9054c576a1 satMapsProvider defaults; selectedYear defaulted to null; TCBParameter tables reorder; 2024-12-18 11:53:30 +09:00
dec796b75e EMS: move DB connection to env 2024-12-18 11:28:38 +09:00
87866e4e51 Map 2024-12-16 10:50:35 +09:00
eeae97288a Remove @mui, move states into zustand store 2024-12-10 10:51:29 +09:00
e9595f9703 Update 2024-12-06 12:42:34 +09:00
bd0a317e76 Object data 2024-11-26 18:00:18 +09:00
a4513e7e7a Drop @mui, addded ems api 2024-11-15 17:00:23 +09:00
f51835584d Better map 2024-10-29 15:08:23 +09:00
115c6ec417 Map testing, custom table based on Tanstack Table 2024-10-25 10:02:40 +09:00
edb6ae00fb Remove old tests 2024-10-10 09:10:12 +09:00
974fc12b34 mantine 2024-10-09 16:51:37 +09:00
b88d83cd74 Fix build errors 2024-09-26 12:09:38 +09:00
108dc5082c Disabled signup; Map test 2024-09-26 12:02:01 +09:00
33f41aaab0 Tile generation 2024-09-10 17:26:05 +09:00
8c8c619143 Tile generation 2024-09-09 17:49:32 +09:00
ddacbcd837 Map experiments 2024-09-06 17:56:42 +09:00
3994989994 Map update 2024-09-05 17:14:48 +09:00
ab88fd5ea5 Map caching, clickhouse test service 2024-08-26 16:11:37 +09:00
579bbf7764 Map caching in Redis 2024-08-23 17:50:53 +09:00
97b44a4db7 Rename; Added EMS server; redis compose 2024-08-20 17:34:21 +09:00
61339f4c26 Revert 2024-08-01 14:04:59 +09:00
8d68119ded Build script 2024-08-01 14:01:58 +09:00
00af65ecdb Copy after build 2024-08-01 13:58:31 +09:00
3a090bf1ad Copy dist 2024-08-01 13:56:39 +09:00
878f206189 Remove dist from volumes 2024-08-01 13:53:13 +09:00
748aa81a99 Settings & FormFields 2024-08-01 11:06:25 +09:00
1e802b4550 Refactored forms 2024-07-30 17:39:57 +09:00
a1a5c2b3a6 Remove spread fill to avoid duplicate values on autocomplete 2024-07-22 17:00:47 +09:00
a3b0b1b222 Cleanup 2024-07-22 16:43:52 +09:00
424217a895 Cleanup 2024-07-22 16:43:13 +09:00
0ac0534486 Cleanup, shared create modal 2024-07-22 16:42:17 +09:00
e1f9dc762c Make dist as volume at compose 2024-07-22 16:16:02 +09:00
153806f392 Remove unused imports 2024-07-22 15:58:25 +09:00
ae2213b188 Reports: city autocomplete, mutation, refresh 2024-07-22 15:51:37 +09:00
748cf89b35 Build ahead of Docker image pull 2024-07-19 16:57:29 +09:00
492fbd7d89 Update README 2024-07-19 16:32:34 +09:00
e3af090119 Build & serve 2024-07-19 16:13:29 +09:00
53e9a8cadf Cleanup 2024-07-19 14:43:58 +09:00
a3043afa7b axiosInstance config 2024-07-19 10:46:34 +09:00
ca2d97f975 define Docker 2024-07-18 17:36:52 +09:00
cf3fda43e4 Update 2024-07-18 11:48:56 +09:00
4283bd20bb DataGrid cell autocomplete 2024-07-15 17:48:48 +09:00
e566e23f6d Servers: servers, ips, hw, storages 2024-07-15 12:39:53 +09:00
416e2e39b5 Tables, cards, (servers) 2024-07-12 17:44:44 +09:00
f9de1124c3 Cleanup 2024-07-10 14:37:00 +09:00
a65a431b09 Testing: fetch servers by region 2024-07-09 16:44:41 +09:00
6f4aa1903d Servers API 2024-07-09 11:58:45 +09:00
c74c911eea Minor fixes 2024-07-05 18:00:31 +09:00
d298de0a72 Multiple files upload 2024-07-05 17:35:13 +09:00
3727fcabb3 docx, xlsx, pdf viewers, dropzone for uploading, cleanup 2024-07-05 17:11:29 +09:00
261196afef Reports test 2024-07-04 15:18:06 +09:00
2c71e4f6af File uploading 2024-07-02 10:51:02 +09:00
704276037c upstream to API changes 2024-07-01 17:51:50 +09:00
e70d94afec Documents page, WIP 2024-06-28 17:56:34 +09:00
7ba886e966 Add Document API calls 2024-06-28 16:03:12 +09:00
af1d497715 Rename interfaces, AppBar changes 2024-06-28 12:33:07 +09:00
c41e59cd86 DashboardLayout changes, refactoring, useSWR 2024-06-27 17:32:12 +09:00
18fb120777 Refactored store 2024-06-25 15:56:00 +09:00
85f97e9e0e Roles: useDataFetching + DataGrid 2024-06-24 17:48:52 +09:00
c2688855c3 Auth: react-hook-form 2024-06-24 17:48:01 +09:00
62695acf74 Auth: SignIn, SignUp (TODO: rewrite into react-hook-form) 2024-06-24 17:06:41 +09:00
d6906503d1 Layout, Pages, Dashboard, MUI 2024-06-20 16:59:59 +09:00
168 changed files with 2450627 additions and 6492 deletions

11
.env.example Normal file
View File

@ -0,0 +1,11 @@
REDIS_HOST=redis_db
REDIS_PORT=6379
REDIS_PASSWORD=
POSTGRES_HOST=localhost
POSTGRES_DB=ems
POSTGRES_USER=ems
POSTGRES_PASSWORD=
POSTGRES_PORT=5432
CLIENT_PORT=5173
EMS_PORT=5000
MONITOR_PORT=1234

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
.vscode
__pycache__
.env
redis_data
psql_data

View File

@ -1,47 +0,0 @@
from fastapi import APIRouter, BackgroundTasks
import backend_fastapi.schemas as schemas
from backend_fastapi.repositories import get_stored_roles, add_role, add_user, get_role_all, update_role, delete_role, update_user,delete_user,get_users
from typing import List
router = APIRouter()
@router.post("/role")
async def create_role(role: schemas.RoleCreate) -> schemas.Role:
return await add_role(role)
@router.get("/stored_role")
async def get_stored_role() -> List[schemas.Role]:
return await get_stored_roles()
@router.get("/role")
async def get_role(limit:int=10, page:int=0) -> List[schemas.Role]:
return await get_role_all(limit, page)
@router.patch("/role")
async def change_role(role: schemas.Role, id: int) -> None:
return await update_role(role, id)
@router.delete("/role")
async def remove_role(id: int) -> schemas.Role:
return await delete_role(id)
@router.post("/user")
async def create_user(user: schemas.UserCreate) -> schemas.User:
import hashlib
user.hashed_password = hashlib.sha256(user.hashed_password.encode('utf-8')).hexdigest()
return await add_user(user)
@router.get('/user')
async def show_users(limit:int=10, page:int=0):
return await get_users(limit, page)
@router.patch('/user')
async def change_users(user: schemas.User, id: int):
import hashlib
if user.hashed_password:
user.hashed_password = hashlib.sha256(user.hashed_password.encode('utf-8')).hexdigest()
return await update_user(user, id)
@router.delete('/user')
async def remove_users(id: int):
return await delete_user(id)

View File

@ -1,29 +0,0 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
import os
from dotenv import load_dotenv
load_dotenv()
class Model(DeclarativeBase):
pass
async_engine = create_async_engine(
os.getenv("SQL_URL"),
connect_args={"check_same_thread": False}
)
async_session = async_sessionmaker(
async_engine,
autoflush=True,
autocommit=False,
expire_on_commit =False
)
async def connect() -> None:
async with async_engine.begin() as conn:
await conn.run_sync(Model.metadata.create_all, checkfirst=True)
async def disconnect() -> None:
if async_engine:
await async_engine.dispose()

View File

@ -1,26 +0,0 @@
from fastapi import APIRouter
import backend_fastapi.schemas as schemas
import backend_fastapi.repositories as repo
import asyncio
router = APIRouter()
@router.get("/values")
async def get_values() -> list[schemas.Value]:
return await repo.get_values()
@router.get("/objects")
async def get_objects() -> list[schemas.Object]:
return await repo.get_objects()
@router.get("/report")
async def get_report() -> None:
import pandas as pd
values, objects = await asyncio.gather(repo.get_values(), repo.get_objects())
values = [schemas.Value.model_validate(value).model_dump() for value in values]
objects = [schemas.Object.model_validate(object).model_dump() for object in objects]
df_values= pd.DataFrame(data=values)
df_objects= pd.DataFrame(data=objects)
df_type = df_values['id_param'].where(df_values['id_param'] == 3).notnull()
print(df_values[["id_object","value"]][df_type].set_index("id_object").join(df_objects[["id","id_city"]].set_index("id"),how='inner'))

View File

@ -1,47 +0,0 @@
from .database import Model
from sqlalchemy.orm import mapped_column, Mapped, relationship
from sqlalchemy import String, Boolean, ForeignKey, Date
from sqlalchemy.dialects.mssql import SQL_VARIANT, UNIQUEIDENTIFIER
from uuid import UUID
from datetime import date
class Role(Model):
__tablename__ = "roles"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255),nullable=False)
class User(Model):
__tablename__ = "users"
id: Mapped[int] = mapped_column( primary_key=True)
firstname: Mapped[str] = mapped_column(String(255),nullable=False)
lastname: Mapped[str] = mapped_column(String(255),nullable=False)
email: Mapped[str] = mapped_column(String(255),nullable=False)
hashed_password: Mapped[str] = mapped_column(String(255),nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean,default=True)
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"))
role: Mapped["Role"] = relationship()
class Object(Model):
__tablename__ = "vObjects"
id: Mapped[UUID] = mapped_column(UNIQUEIDENTIFIER, primary_key=True)
id_parent: Mapped[UUID|None] = mapped_column(UNIQUEIDENTIFIER,nullable=True)
year: Mapped[int | None] = mapped_column(nullable=False)
id_city: Mapped[int] = mapped_column(nullable=False)
class Value(Model):
__tablename__ = "vValues"
id: Mapped[int] = mapped_column( primary_key=True)
id_object: Mapped[UUID] = mapped_column(ForeignKey("vObjects.id"))
id_param:Mapped[int] = mapped_column(nullable=False)
value:Mapped[str|None] = mapped_column(String(250))
date_s: Mapped[date] = mapped_column(Date,nullable=False)
date_po: Mapped[date|None] = mapped_column(Date,nullable=True)
id_user:Mapped[int] = mapped_column(nullable=False)
# class Parameter(Model):
# __tablename__ = "vParameters"

View File

@ -1,3 +0,0 @@
import pandas as pd
import asyncio

View File

@ -1,106 +0,0 @@
from sqlalchemy import select, update, delete
from fastapi.exceptions import HTTPException
from backend_fastapi.stored import exec_procedure
import backend_fastapi.models as models
from .database import async_session
import backend_fastapi.schemas as schemas
async def add_role(role: schemas.RoleCreate):
async with async_session() as session:
model = models.Role(name = role.name)
session.add(model)
await session.flush()
await session.commit()
return model
async def get_role_all(limit:int=10,page:int=0):
async with async_session() as session:
result = await session.scalars(select(models.Role).order_by(models.Role.id).limit(limit).offset(page*limit))
return result.all()
async def delete_role(id: int):
async with async_session() as session:
data = await session.scalars(select(models.Role).filter(models.Role.id == id))
result = data.one_or_none()
if not result:
raise HTTPException(status_code=404, detail="Item not found")
await session.execute(delete(models.Role).filter(models.Role.id == id))
await session.commit()
return result
async def update_role(role: schemas.Role, id: int):
async with async_session() as session:
data = await session.scalars(select(models.Role).filter(models.Role.id == id))
result = data.one_or_none()
if not result:
raise HTTPException(status_code=404, detail="Item not found")
query = update(models.Role).filter(models.Role.id == id).values(name = role.name)
await session.execute(query)
await session.commit()
return {f"{id=} был обновлен"}
async def add_user(user: schemas.UserCreate):
async with async_session() as session:
model = models.User(
**user.model_dump()
)
session.add(model)
await session.flush()
await session.commit()
return model
async def get_users(limit:int=10,page:int=0):
async with async_session() as session:
result = await session.scalars(select(models.User).order_by(models.User.id).limit(limit).offset(page*limit))
return result.all()
async def delete_user(id: int):
async with async_session() as session:
data = await session.scalars(select(models.User).filter(models.User.id == id))
result = data.one_or_none()
if not result:
raise HTTPException(status_code=404, detail="Item not found")
await session.execute(delete(models.User).filter(models.User.id == id))
await session.commit()
return result
async def update_user(user: schemas.User, id: int):
async with async_session() as session:
data = await session.scalars(select(models.User).filter(models.User.id == id))
result = data.one_or_none()
if not result:
raise HTTPException(status_code=404, detail="Item not found")
query = update(models.User).filter(models.User.id == id)\
.values(
firstname = user.firstname,
lastname = user.lastname,
email = user.email,
hashed_password= user.hashed_password,
is_active = user.is_active,
role_id = user.role_id,
)
await session.execute(query)
await session.commit()
return {f"{id=} был обновлен"}
async def get_objects():
async with async_session() as session:
result = await session.scalars(select(models.Object))
return result.all()
async def get_values():
async with async_session() as session:
result = await session.scalars(select(models.Value))
return result.all()
async def get_stored_roles():
return await exec_procedure('get_roles')

View File

@ -1,46 +0,0 @@
from pydantic import BaseModel, ConfigDict, EmailStr
from uuid import UUID
from datetime import date
class RoleBase(BaseModel):
name: str
class UserBase(BaseModel):
firstname: str
lastname: str
email: EmailStr
hashed_password: str
role_id: int
is_active: bool = True
class RoleCreate(RoleBase):
pass
class UserCreate(UserBase):
pass
class User(UserBase):
model_config = ConfigDict(from_attributes=True)
id: int
class Role(RoleBase):
model_config = ConfigDict(from_attributes=True)
id: int
class Object(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
id_city: int
id_parent: UUID | None
year: int | None
class Value(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
id_object: UUID
id_param: int
value: str|None
date_s: date
date_po: date|None
id_user: int

View File

@ -1,23 +0,0 @@
from sqlalchemy import text
from .database import async_session
async def exec_procedure(proc_name, params:dict = None, database: str = None):
async with async_session() as session:
sql_params = ""
if params:
sql_params = ",".join([f"@{key} = :{key}"
for key, value in params.items()])
dbstr = ""
if database:
dbstr = f"[{database}]."
sql_string = text(f'''
DECLARE @return_value int;
EXEC @return_value = {dbstr}[dbo].[{proc_name}] {sql_params};
SELECT 'Return Value' = @return_value;
''')
datas = await session.execute(
sql_string, params)
await session.commit()
return [dict(data._mapping) for data in datas.fetchall()]

14
client/.env.example Normal file
View File

@ -0,0 +1,14 @@
# API авторизации
VITE_API_AUTH_URL=
# API info
VITE_API_INFO_URL=
# API fuel
VITE_API_FUEL_URL=
# API servers
VITE_API_SERVERS_URL=
# API EMS
VITE_API_EMS_URL=

View File

@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
stats.html
# Editor directories and files
.vscode/*

15
client/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM node:lts-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 5173
CMD ["npm", "run", "serve"]

18
client/README.md Normal file
View File

@ -0,0 +1,18 @@
# Experimental Frontend
## Структура проекта
- `src/assets/` - Статические ассеты
- `src/components/` - Компоненты
- `src/constants/` - Константы
- `src/layouts/` - Макеты для разных частей, пока есть MainLayout, используемый всеми роутами
- `src/pages/` - Страницы
- `src/services/` - сервисы / API
## UI
В основном, используется Material UI https://mui.com/material-ui
Для кастомных компонентов следует создать директорию в `src/components/НазваниеКомпонента` со стилями, если необходимо
## Env vars
`.env.example` должен описывать используемые переменные, в работе же используется `.env.local` или `.env`

BIN
client/bun.lockb Normal file

Binary file not shown.

View File

@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/logo2.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>ИС</title>
</head>
<body>
<div id="root"></div>

12589
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

83
client/package.json Normal file
View File

@ -0,0 +1,83 @@
{
"name": "frontend_reactjs",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"serve": "serve -s dist -l 5173"
},
"dependencies": {
"-": "^0.0.1",
"@dnd-kit/core": "^6.3.1",
"@fontsource/inter": "^5.0.19",
"@fontsource/open-sans": "^5.0.28",
"@hello-pangea/dnd": "^17.0.0",
"@js-preview/docx": "^1.6.2",
"@js-preview/excel": "^1.7.8",
"@js-preview/pdf": "^2.0.2",
"@mantine/carousel": "^7.13.0",
"@mantine/charts": "^7.13.0",
"@mantine/code-highlight": "^7.13.0",
"@mantine/core": "^7.13.0",
"@mantine/dates": "^7.13.0",
"@mantine/dropzone": "^7.13.0",
"@mantine/form": "^7.13.0",
"@mantine/hooks": "^7.13.0",
"@mantine/modals": "^7.13.0",
"@mantine/notifications": "^7.13.0",
"@mantine/nprogress": "^7.13.0",
"@mantine/spotlight": "^7.13.0",
"@mantine/tiptap": "^7.13.0",
"@tabler/icons-react": "^3.17.0",
"@tanstack/react-table": "^8.20.5",
"@techstark/opencv-js": "^4.10.0-release.1",
"@tiptap/extension-link": "^2.7.3",
"@tiptap/react": "^2.7.3",
"@tiptap/starter-kit": "^2.7.3",
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@^3.5.0",
"@uidotdev/usehooks": "^2.4.1",
"autoprefixer": "^10.4.19",
"axios": "^1.7.2",
"buffer": "^6.0.3",
"dayjs": "^1.11.13",
"embla-carousel-react": "^8.3.0",
"file-type": "^19.0.0",
"ol": "^10.0.0",
"ol-ext": "^4.0.23",
"proj4": "^2.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.0",
"react-router-dom": "^6.23.1",
"recharts": "^2.12.7",
"swr": "^2.2.5",
"uuid": "^11.0.3",
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/proj4": "^2.5.5",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.47",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"rollup-plugin-visualizer": "^5.12.0",
"sass-embedded": "^1.79.5",
"serve": "^14.2.3",
"tailwindcss": "^3.4.4",
"typescript": "^5.2.2",
"vite": "^5.3.5",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.0"
}
}

14
client/postcss.config.js Normal file
View File

@ -0,0 +1,14 @@
export default {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
}

34
client/public/logo1.svg Normal file
View File

@ -0,0 +1,34 @@
<svg width="252" height="252" viewBox="0 0 252 252" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M61 36C47.1929 36 36 47.1929 36 61V191C36 204.807 47.1929 216 61 216H191C204.807 216 216 204.807 216 191V61C216 47.1929 204.807 36 191 36H61ZM73 54C61.9543 54 53 62.9543 53 74V179C53 190.046 61.9543 199 73 199H178C189.046 199 198 190.046 198 179V74C198 62.9543 189.046 54 178 54H73Z" fill="#D9D9D9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M61 36C47.1929 36 36 47.1929 36 61V191C36 204.807 47.1929 216 61 216H191C204.807 216 216 204.807 216 191V61C216 47.1929 204.807 36 191 36H61ZM73 54C61.9543 54 53 62.9543 53 74V179C53 190.046 61.9543 199 73 199H178C189.046 199 198 190.046 198 179V74C198 62.9543 189.046 54 178 54H73Z" fill="url(#paint0_linear_2_2)"/>
<path d="M67.4996 81.0781L66.5525 172H115.5V126C115.5 119.096 121.096 113.5 128 113.5H236V80H67.434C67.481 80.3523 67.5034 80.7123 67.4996 81.0781Z" fill="#D9D9D9"/>
<path d="M67.4996 81.0781L66.5525 172H115.5V126C115.5 119.096 121.096 113.5 128 113.5H236V80H67.434C67.481 80.3523 67.5034 80.7123 67.4996 81.0781Z" fill="url(#paint1_linear_2_2)"/>
<path d="M236 138.5H158.178L191.678 172H236V138.5Z" fill="#D9D9D9"/>
<path d="M236 138.5H158.178L191.678 172H236V138.5Z" fill="url(#paint2_linear_2_2)"/>
<path d="M156.322 172L140.5 156.178V172H156.322Z" fill="#D9D9D9"/>
<path d="M156.322 172L140.5 156.178V172H156.322Z" fill="url(#paint3_linear_2_2)"/>
<path d="M15 172H51.5517L52.5004 80.9219C52.5037 80.6096 52.526 80.3019 52.5662 80H15V172Z" fill="#D9D9D9"/>
<path d="M15 172H51.5517L52.5004 80.9219C52.5037 80.6096 52.526 80.3019 52.5662 80H15V172Z" fill="url(#paint4_linear_2_2)"/>
<defs>
<linearGradient id="paint0_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
<linearGradient id="paint1_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
<linearGradient id="paint2_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
<linearGradient id="paint3_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
<linearGradient id="paint4_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

34
client/public/logo2.svg Normal file
View File

@ -0,0 +1,34 @@
<svg width="252" height="252" viewBox="0 0 252 252" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M61 36C47.1929 36 36 47.1929 36 61V191C36 204.807 47.1929 216 61 216H191C204.807 216 216 204.807 216 191V61C216 47.1929 204.807 36 191 36H61ZM73 54C61.9543 54 53 62.9543 53 74V179C53 190.046 61.9543 199 73 199H178C189.046 199 198 190.046 198 179V74C198 62.9543 189.046 54 178 54H73Z" fill="#D9D9D9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M61 36C47.1929 36 36 47.1929 36 61V191C36 204.807 47.1929 216 61 216H191C204.807 216 216 204.807 216 191V61C216 47.1929 204.807 36 191 36H61ZM73 54C61.9543 54 53 62.9543 53 74V179C53 190.046 61.9543 199 73 199H178C189.046 199 198 190.046 198 179V74C198 62.9543 189.046 54 178 54H73Z" fill="url(#paint0_linear_2_2)"/>
<path d="M67.4996 81.0781L66.5525 172H115.5V126C115.5 119.096 121.096 113.5 128 113.5H236V80H67.434C67.481 80.3523 67.5034 80.7123 67.4996 81.0781Z" fill="#D9D9D9"/>
<path d="M67.4996 81.0781L66.5525 172H115.5V126C115.5 119.096 121.096 113.5 128 113.5H236V80H67.434C67.481 80.3523 67.5034 80.7123 67.4996 81.0781Z" fill="url(#paint1_linear_2_2)"/>
<path d="M236 138.5H158.178L191.678 172H236V138.5Z" fill="#D9D9D9"/>
<path d="M236 138.5H158.178L191.678 172H236V138.5Z" fill="url(#paint2_linear_2_2)"/>
<path d="M156.322 172L140.5 156.178V172H156.322Z" fill="#D9D9D9"/>
<path d="M156.322 172L140.5 156.178V172H156.322Z" fill="url(#paint3_linear_2_2)"/>
<path d="M15 172H51.5517L52.5004 80.9219C52.5037 80.6096 52.526 80.3019 52.5662 80H15V172Z" fill="#D9D9D9"/>
<path d="M15 172H51.5517L52.5004 80.9219C52.5037 80.6096 52.526 80.3019 52.5662 80H15V172Z" fill="url(#paint4_linear_2_2)"/>
<defs>
<linearGradient id="paint0_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
<linearGradient id="paint1_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
<linearGradient id="paint2_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
<linearGradient id="paint3_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
<linearGradient id="paint4_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

53
client/src/App.tsx Normal file
View File

@ -0,0 +1,53 @@
import { BrowserRouter as Router, Route, Routes, Navigate } from "react-router-dom"
import NotFound from "./pages/NotFound"
import MainLayout from "./layouts/MainLayout"
import { initAuth, useAuthStore } from "./store/auth"
import { useEffect, useState } from "react"
import DashboardLayout from "./layouts/DashboardLayout"
import { Box, Loader } from "@mantine/core"
import { pages } from "./constants/app"
function App() {
const auth = useAuthStore()
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
initAuth()
}, [])
// Once auth is there, set loading to false and render the app
useEffect(() => {
if (auth) {
setIsLoading(false)
}
}, [auth])
if (isLoading) {
return (
<Loader />
)
} else {
return (
<Box w='100%' h='100vh'>
<Router>
<Routes>
<Route element={<MainLayout />}>
{pages.filter((page) => !page.dashboard).filter((page) => page.enabled).map((page, index) => (
<Route key={`ml-${index}`} path={page.path} element={page.component} />
))}
</Route>
<Route element={auth.isAuthenticated ? <DashboardLayout></DashboardLayout> : <Navigate to={"/auth/signin"} />}>
{pages.filter((page) => page.dashboard).filter((page) => page.enabled).map((page, index) => (
<Route key={`dl-${index}`} path={page.path} element={page.component} />
))}
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Router>
</Box>
)
}
}
export default App

23
client/src/actions/map.ts Normal file
View File

@ -0,0 +1,23 @@
import { Coordinate } from "ol/coordinate";
import { IGeometryType } from "../interfaces/map";
export const uploadCoordinates = async (coordinates: Coordinate[], type: IGeometryType) => {
try {
const response = await fetch(`${import.meta.env.VITE_API_EMS_URL}/nodes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ coordinates, object_id: 1, type: type }) // Replace with actual object_id
});
if (response.ok) {
const data = await response.json();
console.log('Node created:', data);
} else {
console.error('Failed to upload coordinates');
}
} catch (error) {
console.error('Error:', error);
}
};

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,23 @@
import { Divider, Flex, Text } from '@mantine/core';
import { PropsWithChildren } from 'react'
interface CardInfoProps extends PropsWithChildren {
label: string;
}
export default function CardInfo({
children,
label
}: CardInfoProps) {
return (
<Flex direction='column' gap='sm' p='sm'>
<Text fw={600}>
{label}
</Text>
<Divider />
{children}
</Flex>
)
}

View File

@ -0,0 +1,26 @@
import { Chip } from '@mantine/core';
import { ReactElement } from 'react'
interface CardInfoChipProps {
status: boolean;
label: string;
iconOn: ReactElement
iconOff: ReactElement
}
export default function CardInfoChip({
status,
label,
iconOn,
iconOff
}: CardInfoChipProps) {
return (
<Chip
icon={status ? iconOn : iconOff}
color={status ? "success" : "error"}
variant='outline'
>
{label}
</Chip>
)
}

View File

@ -0,0 +1,22 @@
import { Flex, Text } from '@mantine/core';
interface CardInfoLabelProps {
label: string;
value: string | number;
}
export default function CardInfoLabel({
label,
value
}: CardInfoLabelProps) {
return (
<Flex justify='space-between' align='center'>
<Text>
{label}
</Text>
<Text fw={600}>
{value}
</Text>
</Flex>
)
}

View File

@ -0,0 +1,51 @@
.resize_handler {
position: absolute;
opacity: 0;
top: 0;
right: 0;
height: 100%;
width: 5px;
background: #27bbff;
cursor: col-resize;
user-select: none;
touch-action: none;
border-radius: 6px;
}
.resize_handler:hover {
opacity: 1;
}
.tr {
display: flex;
//width: 100%;
//max-width: 100%;
width: fit-content;
}
.th {
position: relative;
}
.th,
.td {
display: flex;
width: auto;
}
.thead {
display: flex;
width: 100%;
}
.table {
display: flex;
flex-direction: column;
width: 100%;
}
.tbody {
display: flex;
flex-direction: column;
width: 100%;
}

View File

@ -0,0 +1,111 @@
import { Input, Table } from '@mantine/core';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
import styles from './CustomTable.module.scss'
// Sample data
type DataType = {
id: number,
name: string,
age: number
}
// Define columns
const columns: ColumnDef<DataType>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: (info) => info.getValue(),
maxSize: Number.MAX_SAFE_INTEGER,
},
{
accessorKey: 'age',
header: 'Age',
cell: (info) => info.getValue(),
},
];
const CustomTable = () => {
const [data, setData] = useState<DataType[]>([
{ id: 1, name: 'John Doe', age: 25 },
{ id: 2, name: 'Jane Smith', age: 30 },
{ id: 3, name: 'Sam Green', age: 22 },
]);
const [editingCell, setEditingCell] = useState<{ rowIndex: string | number | null, columnId: string | number | null }>({ rowIndex: null, columnId: null });
const tableColumns = useMemo<ColumnDef<typeof data[0]>[]>(() => columns, []);
const table = useReactTable({
data,
columns: tableColumns,
getCoreRowModel: getCoreRowModel(),
columnResizeMode: "onChange",
});
// Function to handle cell edit
const handleEditCell = (
rowIndex: number,
columnId: keyof DataType,
value: DataType[keyof DataType]
) => {
const updatedData = [...data];
(updatedData[rowIndex][columnId] as DataType[keyof DataType]) = value;
setData(updatedData);
//setEditingCell({ rowIndex: null, columnId: null });
};
return (
<Table striped withColumnBorders highlightOnHover className={styles.table}>
<Table.Thead className={styles.thead}>
{table.getHeaderGroups().map(headerGroup => (
<Table.Tr key={headerGroup.id} className={styles.tr}>
{headerGroup.headers.map((header) => (
<Table.Th key={header.id} className={styles.th} w={header.getSize()}>
{flexRender(header.column.columnDef.header, header.getContext())}
<div
className={styles.resize_handler}
onMouseDown={header.getResizeHandler()} //for desktop
onTouchStart={header.getResizeHandler()}
>
</div>
</Table.Th>
))}
</Table.Tr>
))}
</Table.Thead>
<Table.Tbody className={styles.tbody}>
{table.getRowModel().rows.map((row, rowIndex) => (
<Table.Tr key={row.id} className={styles.tr}>
{row.getVisibleCells().map(cell => {
const isEditing = editingCell.rowIndex === rowIndex && editingCell.columnId === cell.column.id;
return (
<Table.Td
key={cell.id}
onDoubleClick={() => setEditingCell({ rowIndex, columnId: cell.column.id })}
style={{ width: cell.column.getSize() }}
className={styles.td}
>
{isEditing ? (
<Input
type='text'
value={data[rowIndex][cell.column.id as keyof DataType]}
onChange={(e) => handleEditCell(rowIndex, (cell.column.id as keyof DataType), e.target.value)}
onBlur={() => setEditingCell({ rowIndex: null, columnId: null })}
autoFocus
/>
) : (
flexRender(cell.column.columnDef.cell, cell.getContext())
)}
</Table.Td>
);
})}
</Table.Tr>
))}
</Table.Tbody>
</Table>
);
};
export default CustomTable;

View File

@ -0,0 +1,292 @@
import { useDocuments, useDownload, useFolders } from '../hooks/swrHooks'
import { IDocument, IDocumentFolder } from '../interfaces/documents'
import React, { useEffect, useState } from 'react'
import DocumentService from '../services/DocumentService'
import { mutate } from 'swr'
import FileViewer from './modals/FileViewer'
import { ActionIcon, Anchor, Breadcrumbs, Button, Divider, FileButton, Flex, Loader, MantineStyleProp, RingProgress, ScrollAreaAutosize, Stack, Table, Text } from '@mantine/core'
import { IconCancel, IconDownload, IconFile, IconFileFilled, IconFilePlus, IconFileUpload, IconFolderFilled, IconX } from '@tabler/icons-react'
interface DocumentProps {
doc: IDocument;
}
const FileItemStyle: MantineStyleProp = {
cursor: 'pointer',
display: 'flex',
width: '100%',
flexDirection: 'row',
gap: '8px',
alignItems: 'center',
padding: '8px'
}
const handleSave = async (file: Blob, filename: string) => {
const link = document.createElement('a')
link.href = window.URL.createObjectURL(file)
link.download = filename
link.click()
link.remove()
window.URL.revokeObjectURL(link.href)
}
function ItemDocument({ doc }: DocumentProps) {
const [shouldFetch, setShouldFetch] = useState(false)
const { file, isLoading } = useDownload(shouldFetch ? doc?.document_folder_id : null, shouldFetch ? doc?.id : null)
useEffect(() => {
if (shouldFetch) {
if (file) {
handleSave(file, doc.name)
setShouldFetch(false)
}
}
}, [shouldFetch, file, doc.name])
return (
<Flex>
<ActionIcon
onClick={(e) => {
e.stopPropagation()
if (!isLoading) {
setShouldFetch(true)
}
}}
variant='subtle'>
{isLoading ?
<Loader size='sm' />
:
<IconDownload />
}
</ActionIcon>
</Flex>
)
}
export default function FolderViewer() {
const [currentFolder, setCurrentFolder] = useState<IDocumentFolder | null>(null)
const [breadcrumbs, setBreadcrumbs] = useState<IDocumentFolder[]>([])
const { folders, isLoading: foldersLoading } = useFolders()
const { documents, isLoading: documentsLoading } = useDocuments(currentFolder?.id)
const [uploadProgress, setUploadProgress] = useState(0)
const [isUploading, setIsUploading] = useState(false)
const [fileViewerModal, setFileViewerModal] = useState(false)
const [currentFileNo, setCurrentFileNo] = useState<number>(-1)
const [dragOver, setDragOver] = useState(false)
const [filesToUpload, setFilesToUpload] = useState<File[]>([])
const handleFolderClick = (folder: IDocumentFolder) => {
setCurrentFolder(folder)
setBreadcrumbs((prev) => [...prev, folder])
}
const handleDocumentClick = async (index: number) => {
setCurrentFileNo(index)
setFileViewerModal(true)
}
const handleBreadcrumbClick = (index: number) => {
const newBreadcrumbs = breadcrumbs.slice(0, index + 1);
setBreadcrumbs(newBreadcrumbs)
setCurrentFolder(newBreadcrumbs[newBreadcrumbs.length - 1])
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(true)
}
const handleDragLeave = () => {
setDragOver(false)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(false)
const files = Array.from(e.dataTransfer.files)
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
}
const handleFileInput = (files: File[] | null) => {
if (files !== null) {
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
}
}
const uploadFiles = async () => {
setIsUploading(true)
if (filesToUpload.length > 0 && currentFolder && currentFolder.id) {
const formData = new FormData()
for (const file of filesToUpload) {
formData.append('files', file)
}
try {
await DocumentService.uploadFiles(currentFolder.id, formData, setUploadProgress);
setIsUploading(false);
setFilesToUpload([]);
mutate(`/info/documents/${currentFolder.id}`);
} catch (error) {
console.error(error);
setIsUploading(false);
}
}
}
if (foldersLoading || documentsLoading) {
return (
<Loader />
)
}
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p={'sm'}>
{fileViewerModal &&
<FileViewer
open={fileViewerModal}
setOpen={setFileViewerModal}
currentFileNo={currentFileNo}
setCurrentFileNo={setCurrentFileNo}
docs={documents}
/>
}
<Stack>
<Breadcrumbs>
<Anchor
onClick={() => {
setCurrentFolder(null)
setBreadcrumbs([])
}}
>
Главная
</Anchor>
{breadcrumbs.map((breadcrumb, index) => (
<Anchor
key={breadcrumb.id}
onClick={() => handleBreadcrumbClick(index)}
>
{breadcrumb.name}
</Anchor>
))}
</Breadcrumbs>
{currentFolder &&
<Flex direction='column' gap='sm'>
<Flex direction='column' gap='sm' p='sm' style={{
border: filesToUpload.length > 0 ? '1px dashed gray' : 'none',
borderRadius: '8px',
}}>
<Flex gap='sm'>
<FileButton multiple onChange={handleFileInput}>
{(props) => <Button variant='filled' leftSection={isUploading ? <Loader /> : <IconFilePlus />} {...props}>Добавить</Button>}
</FileButton>
{filesToUpload.length > 0 &&
<>
<Button
variant='filled'
leftSection={isUploading ? <RingProgress sections={[{ value: uploadProgress, color: 'blue' }]} /> : <IconFileUpload />}
onClick={uploadFiles}
>
Загрузить все
</Button>
<Button
variant='outline'
leftSection={<IconCancel />}
onClick={() => {
setFilesToUpload([])
}}
>
Отмена
</Button>
</>
}
</Flex>
<Divider />
{filesToUpload.length > 0 &&
<Flex direction='column'>
{filesToUpload.map((file, index) => (
<Flex key={index} p='8px'>
<Flex gap='sm' direction='row' align='center'>
<IconFile />
<Text>{file.name}</Text>
</Flex>
<ActionIcon onClick={() => {
setFilesToUpload(prev => {
return prev.filter((_, i) => i != index)
})
}} ml='auto' variant='subtle'>
<IconX />
</ActionIcon>
</Flex>
))}
</Flex>
}
</Flex>
</Flex>
}
<Table
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
bg={dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit'}
highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Название</Table.Th>
<Table.Th p={0}>Дата создания</Table.Th>
<Table.Th p={0}></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{currentFolder ? (
documents?.map((doc: IDocument, index: number) => (
<Table.Tr key={doc.id} onClick={() => handleDocumentClick(index)} style={{ cursor: 'pointer' }}>
<Table.Td p={0}>
<Flex style={FileItemStyle}>
<IconFileFilled />
{doc.name}
</Flex>
</Table.Td>
<Table.Td p={0}>
{new Date(doc.create_date).toLocaleDateString()}
</Table.Td>
<Table.Td p={0}>
<ItemDocument
doc={doc}
/>
</Table.Td>
</Table.Tr>
))
) : (
folders?.map((folder: IDocumentFolder) => (
<Table.Tr key={folder.id} onClick={() => handleFolderClick(folder)} style={{ cursor: 'pointer' }}>
<Table.Td p={0}>
<Flex style={FileItemStyle}>
<IconFolderFilled />
{folder.name}
</Flex>
</Table.Td>
<Table.Td p={0} align='left'>
{new Date(folder.create_date).toLocaleDateString()}
</Table.Td>
<Table.Td p={0}>
</Table.Td>
</Table.Tr>
))
)}
</Table.Tbody>
</Table>
</Stack>
</ScrollAreaAutosize>
)
}

View File

@ -0,0 +1,94 @@
import { SubmitHandler, useForm } from 'react-hook-form'
import { CreateField } from '../interfaces/create'
import { AxiosResponse } from 'axios';
import { Button, Loader, Stack, Text, TextInput } from '@mantine/core';
interface Props {
title?: string;
submitHandler?: (data: any) => Promise<AxiosResponse<any, any>>;
fields: CreateField[];
submitButtonText?: string;
mutateHandler?: any;
defaultValues?: {};
watchValues?: string[];
}
function FormFields({
title = '',
submitHandler,
fields,
submitButtonText = 'Сохранить',
mutateHandler,
defaultValues
}: Props) {
const getDefaultValues = (fields: CreateField[]) => {
let result: { [key: string]: string | boolean } = {}
fields.forEach((field: CreateField) => {
result[field.key] = field.defaultValue || defaultValues?.[field.key as keyof {}]
})
return result
}
const { register, handleSubmit, reset, watch, formState: { errors, isSubmitting, dirtyFields, isValid } } = useForm({
mode: 'onChange',
defaultValues: defaultValues ? getDefaultValues(fields) : {}
})
const onSubmit: SubmitHandler<any> = async (data) => {
fields.forEach((field: CreateField) => {
if (field.include === false) {
delete data[field.key]
}
})
try {
const submitResponse = await submitHandler?.(data)
mutateHandler?.(JSON.stringify(submitResponse?.data))
reset(submitResponse?.data)
} catch (error) {
console.error(error)
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap='sm' w='100%'>
{title.length > 0 &&
<Text size="xl" fw={500}>
{title}
</Text>
}
{fields.map((field: CreateField) => {
return (
<TextInput
key={field.key}
label={field.headerName || field.key.charAt(0).toUpperCase() + field.key.slice(1)}
//placeholder="Your name"
type={field.inputType ? field.inputType : 'text'}
{...register(field.key, {
required: field.required ? `${field.headerName} обязателен` : false,
validate: (val: string | boolean) => {
if (field.watch) {
if (watch(field.watch) != val) {
return field.watchMessage || ''
}
}
},
})}
radius="md"
required={field.required || false}
error={errors[field.key]?.message}
errorProps={errors[field.key]}
/>
)
})}
<Button disabled={isSubmitting || Object.keys(dirtyFields).length === 0 || !isValid} type='submit'>
{isSubmitting ? <Loader size={16} /> : submitButtonText}
</Button>
</Stack>
</form>
)
}
export default FormFields

View File

@ -0,0 +1,41 @@
import { IServer } from '../interfaces/servers'
import { useServerIps } from '../hooks/swrHooks'
import { Flex, Table } from '@mantine/core'
function ServerData({ id }: IServer) {
const { serverIps } = useServerIps(id, 0, 10)
const serverIpsColumns = [
{ field: 'id', headerName: 'ID', type: 'number' },
{ field: 'server_id', headerName: 'Server ID', type: 'number' },
{ field: 'name', headerName: 'Название', type: 'string' },
{ field: 'is_actual', headerName: 'Действителен', type: 'boolean' },
{ field: 'ip', headerName: 'IP', type: 'string' },
{ field: 'servername', headerName: 'Название сервера', type: 'string' },
]
return (
<Flex direction='column' p='sm'>
{serverIps &&
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{serverIpsColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
{serverIpsColumns.map(column => (
<Table.Td key={column.field}>{serverIps ? serverIps[column.field] : ''}</Table.Td>
))}
</Table.Tr>
</Table.Tbody>
</Table>
}
</Flex>
)
}
export default ServerData

View File

@ -0,0 +1,73 @@
import { useState } from 'react'
import { useHardwares, useServers } from '../hooks/swrHooks'
import { Autocomplete, CloseButton, Loader, Table } from '@mantine/core'
import { IServer } from '../interfaces/servers'
export default function ServerHardware() {
const [selectedOption, setSelectedOption] = useState<number | undefined>(undefined)
const { servers } = useServers()
const { hardwares, isLoading: serversLoading } = useHardwares(selectedOption, 0, 10)
const hardwareColumns = [
{ field: 'id', headerName: 'ID', type: 'number' },
{ field: 'name', headerName: 'Название', type: 'string' },
{ field: 'server_id', headerName: 'Server ID', type: 'number' },
{ field: 'servername', headerName: 'Название сервера', type: 'string' },
{ field: 'os_info', headerName: 'ОС', type: 'string' },
{ field: 'ram', headerName: 'ОЗУ', type: 'string' },
{ field: 'processor', headerName: 'Проц.', type: 'string' },
{ field: 'storages_count', headerName: 'Кол-во хранилищ', type: 'number' },
]
return (
<>
<form>
<Autocomplete
placeholder="Сервер"
flex={'1'}
data={servers ? servers.map((item: IServer) => ({ 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(undefined)
}}
aria-label="Clear value"
/>
)
}
//value={search}
/>
</form>
{serversLoading ?
<Loader />
:
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{hardwareColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
{hardwareColumns.map(column => (
<Table.Td key={column.field}>{hardwares ? hardwares[column.field] : ''}</Table.Td>
))}
</Table.Tr>
</Table.Tbody>
</Table>
}
</>
)
}

View File

@ -0,0 +1,77 @@
import { useEffect, useState } from 'react'
import { useServerIps, useServers } from '../hooks/swrHooks'
import { Autocomplete, CloseButton, Loader, Table } from '@mantine/core'
import { IServer } from '../interfaces/servers'
export default function ServerIpsView() {
const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { servers } = useServers()
const { serverIps, isLoading: serversLoading } = useServerIps(selectedOption, 0, 10)
const serverIpsColumns = [
{ field: 'id', headerName: 'ID', type: 'number' },
{ field: 'server_id', headerName: 'Server ID', type: 'number' },
{ field: 'name', headerName: 'Название', type: 'string' },
{ field: 'is_actual', headerName: 'Действителен', type: 'boolean' },
{ field: 'ip', headerName: 'IP', type: 'string' },
{ field: 'servername', headerName: 'Название сервера', type: 'string' },
]
useEffect(() => {
console.log(serverIps)
}, [serverIps])
return (
<>
<form>
<Autocomplete
placeholder="Сервер"
flex={'1'}
data={servers ? servers.map((item: IServer) => ({ 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}
/>
</form>
{serversLoading ?
<Loader />
:
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{serverIpsColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr
//bg={selectedRows.includes(element.position) ? 'var(--mantine-color-blue-light)' : undefined}
>
{serverIpsColumns.map(column => (
<Table.Td key={column.field}>{servers ? servers[column.field] : ''}</Table.Td>
))}
</Table.Tr>
</Table.Tbody>
</Table>
}
</>
)
}

View File

@ -0,0 +1,43 @@
import { useState } from 'react'
import { IRegion } from '../interfaces/fuel'
import { useStorages } from '../hooks/swrHooks'
import { Loader, Table } from '@mantine/core'
export default function ServerStorage() {
const [selectedOption] = useState<IRegion | null>(null)
const { storages, isLoading: serversLoading } = useStorages(selectedOption?.id, 0, 10)
const storageColumns = [
{ field: 'id', headerName: 'ID', type: 'number' },
{ field: 'hardware_id', headerName: 'Hardware ID', type: 'number' },
{ field: 'name', headerName: 'Название', type: 'string' },
{ field: 'size', headerName: 'Размер', type: 'string' },
{ field: 'storage_type', headerName: 'Тип хранилища', type: 'string' },
]
return (
<>
{serversLoading ?
<Loader />
:
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{storageColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
{storageColumns.map(column => (
<Table.Td key={column.field}>{storages ? storages[column.field] : ''}</Table.Td>
))}
</Table.Tr>
</Table.Tbody>
</Table>
}
</>
)
}

View File

@ -0,0 +1,77 @@
import { useState } from 'react'
import { IRegion } from '../interfaces/fuel'
import { useRegions, useServers } from '../hooks/swrHooks'
import { useDebounce } from '@uidotdev/usehooks'
import { Autocomplete, CloseButton, Table } from '@mantine/core'
export default function ServersView() {
const [search, setSearch] = useState<string | undefined>("")
const debouncedSearch = useDebounce(search, 500)
const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { regions } = useRegions(10, 1, debouncedSearch)
const { servers } = useServers(selectedOption, 0, 10)
const serversColumns = [
//{ field: 'id', headerName: 'ID', type: "number" },
{
field: 'name', headerName: 'Название', type: "string", editable: true,
},
{
field: 'region_id',
editable: true,
headerName: 'region_id',
flex: 1
}
]
return (
<>
<form>
<Autocomplete
placeholder="Район"
flex={'1'}
data={regions ? regions.map((item: IRegion) => ({ 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}
/>
</form>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{serversColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
{serversColumns.map(column => (
<Table.Td key={column.field}>{servers ? servers[column.field] : ''}</Table.Td>
))}
</Table.Tr>
</Table.Tbody>
</Table>
</>
)
}

View File

@ -0,0 +1,125 @@
import { useState } from 'react'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Accordion, NavLink, Text } from '@mantine/core';
import { setCurrentObjectId, useObjectsStore } from '../../store/objects';
import { IconChevronDown } from '@tabler/icons-react';
import { setSelectedObjectType } from '../../store/map';
const ObjectTree = () => {
const { selectedDistrict, selectedYear } = useObjectsStore()
const [existingCount, setExistingCount] = useState(0)
const [planningCount, setPlanningCount] = useState(0)
const { data: existingObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) {
let count = 0
res.forEach(el => {
count = count + el.count
})
setExistingCount(count)
}
return res
}),
{
revalidateOnFocus: false
}
)
const { data: planningObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) {
let count = 0
res.forEach(el => {
count = count + el.count
})
setPlanningCount(count)
}
return res
}),
{
revalidateOnFocus: false
}
)
if (selectedDistrict) {
return (
<Accordion multiple chevronPosition='left'>
<TypeTree label='Существующие' value={'existing'} count={existingCount} objectList={existingObjectsList} planning={0} />
<TypeTree label='Планируемые' value={'planning'} count={planningCount} objectList={planningObjectsList} planning={1} />
</Accordion>
)
} else {
return (
<Text size='xs'>Выберите регион и населённый пункт, чтобы увидеть список объектов.</Text>
)
}
}
interface TypeTreeProps {
label: string;
value: string;
count: number;
objectList: unknown;
planning: number;
}
const TypeTree = ({
label,
objectList,
count,
planning
}: TypeTreeProps) => {
return (
<NavLink p={0} label={`${label} ${count ? `(${count})` : ''}`}>
{Array.isArray(objectList) && objectList.map(list => (
<ObjectList key={list.id} label={list.name} id={list.id} planning={planning} count={list.count} />
))}
</NavLink>
)
}
interface IObjectList {
label: string;
id: number;
planning: number;
count: number;
}
const ObjectList = ({
label,
id,
planning,
count
}: IObjectList) => {
const { selectedDistrict, selectedYear } = useObjectsStore()
const { data } = useSWR(
selectedDistrict && selectedYear ? `/general/objects/list?type=${id}&city_id=${selectedDistrict}&year=${selectedYear}&planning=${planning}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false,
revalidateIfStale: false
}
)
return (
<NavLink onClick={() => {
setSelectedObjectType(id)
}} rightSection={<IconChevronDown size={14} />} p={0} label={`${label} ${count ? `(${count})` : ''}`}>
{Array.isArray(data) && data.map((type) => (
<NavLink key={type.object_id} label={type.caption ? type.caption : 'Без названия'} p={0} onClick={() => setCurrentObjectId(type.object_id)} />
))}
</NavLink>
)
}
export default ObjectTree

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
import { transform } from "ol/proj"
const mapExtent = [11388546.533293726, 7061866.113051185, 18924313.434856508, 13932243.11199202]
const mapCenter = transform([129.7578941, 62.030804], 'EPSG:4326', 'EPSG:3857')
export {
mapExtent,
mapCenter
}

View File

@ -0,0 +1,61 @@
import { Checkbox, Flex, NavLink, Slider, Stack } from '@mantine/core'
import BaseLayer from 'ol/layer/Base'
import Map from 'ol/Map'
import React, { useEffect, useState } from 'react'
interface MapLayersProps {
map: React.MutableRefObject<Map | null>
}
const MapLayers = ({
map
}: MapLayersProps) => {
return (
<Stack gap='0'>
{map.current?.getLayers().getArray() && map.current?.getLayers().getArray().map((layer, index) => (
<LayerSetting key={index} index={index} layer={layer} />
))}
</Stack>
)
}
interface LayerSettingProps {
index: number;
layer: BaseLayer;
}
const LayerSetting = ({
index,
layer
}: LayerSettingProps) => {
const [opacity, setOpacity] = useState(layer.getOpacity())
const [visible, setVisible] = useState(layer.getLayerState().visible)
useEffect(() => {
layer.setVisible(visible)
}, [visible, layer])
useEffect(() => {
layer.setOpacity(opacity)
}, [opacity, layer])
return (
<Flex key={`layer-${index}`} gap='xs' align='center'>
<Checkbox
checked={visible}
onChange={(e) => setVisible(e.currentTarget.checked)}
/>
<Slider
w='100%'
min={0}
max={1}
step={0.001}
value={opacity}
onChange={(value) => setOpacity(value)}
/>
<NavLink p={0} label={layer.get('name')} onClick={() => { console.log(layer.getLayerState()) }} />
</Flex>
)
}
export default MapLayers

View File

@ -0,0 +1,38 @@
import GeoJSON from "ol/format/GeoJSON";
import { get } from "ol/proj";
import { register } from "ol/proj/proj4";
import { XYZ } from "ol/source";
import VectorSource from "ol/source/Vector";
import proj4 from "proj4";
proj4.defs('EPSG:3395', '+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs')
register(proj4);
const yandexProjection = get('EPSG:3395')?.setExtent([-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]) || 'EPSG:3395'
const googleMapsSatelliteSource = new XYZ({
url: `${import.meta.env.VITE_API_EMS_URL}/tiles/tile/google/{z}/{x}/{y}`,
attributions: 'Map data © Google'
})
const yandexMapsSatelliteSource = new XYZ({
url: `${import.meta.env.VITE_API_EMS_URL}/tiles/tile/yandex/{z}/{x}/{y}`,
attributions: 'Map data © Yandex',
projection: yandexProjection,
})
const customMapSource = new XYZ({
url: `${import.meta.env.VITE_API_EMS_URL}/tiles/tile/custom/{z}/{x}/{y}`,
attributions: 'Custom map data'
})
const regionsLayerSource = new VectorSource({
url: 'sakha_republic.geojson',
format: new GeoJSON(),
})
export {
googleMapsSatelliteSource,
yandexMapsSatelliteSource,
regionsLayerSource,
customMapSource
}

View File

@ -0,0 +1,45 @@
import { Divider, Flex, rem, Text } from '@mantine/core'
import { CSSProperties } from 'react'
import { useMapStore } from '../../../store/map';
interface IMapStatusbarProps {
mapControlsStyle: CSSProperties;
}
const MapStatusbar = ({
mapControlsStyle,
}: IMapStatusbarProps) => {
const mapState = useMapStore()
return (
<Flex gap='sm' p={'4px'} miw={'100%'} fz={'xs'} pos='absolute' bottom='0px' left='0px' style={{ ...mapControlsStyle, borderRadius: 0 }}>
<Text fz='xs' w={rem(130)}>
x: {mapState.currentCoordinate?.[0]}
</Text>
<Text fz='xs' w={rem(130)}>
y: {mapState.currentCoordinate?.[1]}
</Text>
<Divider orientation='vertical' />
<Text fz='xs'>
Z={mapState.currentZ}
</Text>
<Text fz='xs'>
X={mapState.currentX}
</Text>
<Text fz='xs'>
Y={mapState.currentY}
</Text>
<Text fz='xs' ml='auto'>
{mapState.statusText}
</Text>
</Flex>
)
}
export default MapStatusbar

View File

@ -0,0 +1,165 @@
import { FeatureLike } from "ol/Feature";
import { Text } from "ol/style";
import Fill from "ol/style/Fill";
import { FlatStyleLike } from "ol/style/flat";
import Stroke from "ol/style/Stroke";
import Style from "ol/style/Style";
import { calculateCenter } from "./mapUtils";
import CircleStyle from "ol/style/Circle";
import { MultiPoint, Point } from "ol/geom";
export const highlightStyleYellow = new Style({
stroke: new Stroke({
color: 'yellow',
width: 3,
}),
fill: new Fill({
color: 'rgba(255, 255, 0, 0.3)',
}),
});
export const highlightStyleRed = new Style({
stroke: new Stroke({
color: 'red',
width: 3,
}),
fill: new Fill({
color: 'rgba(255, 255, 0, 0.3)',
}),
});
export function overlayStyle(feature: FeatureLike) {
const styles = [new Style({
geometry: function (feature) {
const modifyGeometry = feature.get('modifyGeometry');
return modifyGeometry ? modifyGeometry.geometry : feature.getGeometry();
},
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
stroke: new Stroke({
color: '#ffcc33',
width: 2,
}),
image: new CircleStyle({
radius: 7,
fill: new Fill({
color: '#ffcc33',
}),
}),
})]
const modifyGeometry = feature.get('modifyGeometry')
const geometry = modifyGeometry ? modifyGeometry.geometry : feature.getGeometry()
const result = calculateCenter(geometry)
const center = result.center
if (center) {
styles.push(
new Style({
geometry: new Point(center),
image: new CircleStyle({
radius: 4,
fill: new Fill({
color: '#ff3333'
})
})
})
)
const coordinates = result.coordinates
if (coordinates) {
const minRadius = result.minRadius
const sqDistances = result.sqDistances
const rsq = minRadius * minRadius
if (Array.isArray(sqDistances)) {
const points = coordinates.filter(function (_coordinate, index) {
return sqDistances[index] > rsq
})
styles.push(
new Style({
geometry: new MultiPoint(points),
image: new CircleStyle({
radius: 4,
fill: new Fill({
color: '#33cc33'
})
})
})
)
}
}
}
return styles
}
const figureStyle = new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: '#3399CC',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
})
})
})
const lineStyle = new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: '#3399CC',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
}),
placement: 'line',
overflow: true,
//declutterMode: 'obstacle'
})
})
const drawingLayerStyle: FlatStyleLike = {
'fill-color': 'rgba(255, 255, 255, 0.2)',
//'stroke-color': '#ffcc33',
'stroke-color': '#000000',
'stroke-width': 2,
'circle-radius': 7,
'circle-fill-color': '#ffcc33',
}
const selectStyle = new Style({
fill: new Fill({
color: 'rgba(0, 0, 255, 0.3)',
}),
stroke: new Stroke({
color: 'rgba(255, 255, 255, 0.7)',
width: 2,
}),
})
const regionsLayerStyle = new Style({
stroke: new Stroke({
color: 'blue',
width: 1,
}),
fill: new Fill({
color: 'rgba(0, 0, 255, 0.1)',
}),
})
export {
drawingLayerStyle,
selectStyle,
regionsLayerStyle,
lineStyle,
figureStyle
}

View File

@ -0,0 +1,94 @@
import { ActionIcon, useMantineColorScheme } from '@mantine/core'
import { IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon, IconRuler, IconTransformPoint } from '@tabler/icons-react'
import { setCurrentTool, useMapStore } from '../../../store/map';
interface IToolbarProps {
onSave: () => void;
onRemove: () => void;
}
const MapToolbar = ({
onSave,
onRemove,
}: IToolbarProps) => {
const mapState = useMapStore()
const { colorScheme } = useMantineColorScheme();
return (
<ActionIcon.Group orientation='vertical' pos='absolute' top='8px' right='8px' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<ActionIcon size='lg' variant='transparent' onClick={onSave}>
<IconExclamationCircle />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' onClick={onRemove}>
<IconArrowBackUp />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Edit' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Edit')
}}>
<IconTransformPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Point' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Point')
}}>
<IconPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'LineString' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('LineString')
}}>
<IconLine />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Polygon' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Polygon')
}}>
<IconPolygon />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Circle' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Circle')
}}>
<IconCircle />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Mover' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Mover')
}}
>
<IconArrowsMove />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Measure' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Measure')
}}>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
)
}
export default MapToolbar

View File

@ -0,0 +1,34 @@
import { Checkbox, Group, RenderTreeNodePayload, Text } from "@mantine/core";
import { IconChevronDown } from "@tabler/icons-react";
export const MapTreeCheckbox = ({
node,
expanded,
hasChildren,
elementProps,
tree,
}: RenderTreeNodePayload) => {
const checked = tree.isNodeChecked(node.value);
const indeterminate = tree.isNodeIndeterminate(node.value);
return (
<Group gap="xs" {...elementProps}>
<Checkbox.Indicator
checked={checked}
indeterminate={indeterminate}
onClick={() => (!checked ? tree.checkNode(node.value) : tree.uncheckNode(node.value))}
/>
<Group gap={5} onClick={() => tree.toggleExpanded(node.value)}>
<Text size="xs">{node.label}</Text>
{hasChildren && (
<IconChevronDown
size={14}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
</Group>
</Group>
);
};

View File

@ -0,0 +1,199 @@
import { FeatureLike } from "ol/Feature";
import { LineString, Point, Polygon } from "ol/geom";
import Geometry, { Type } from "ol/geom/Geometry";
import { Fill, RegularShape, Stroke, Style, Text } from "ol/style";
import CircleStyle from "ol/style/Circle";
import { getArea, getLength } from 'ol/sphere'
import { Modify } from "ol/interaction";
import { getMeasureShowSegments } from "../../../store/map";
export const style = new Style({
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.5)',
lineDash: [10, 10],
width: 2,
}),
image: new CircleStyle({
radius: 5,
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.7)',
}),
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
}),
});
export const labelStyle = new Style({
text: new Text({
font: '14px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.7)',
}),
padding: [3, 3, 3, 3],
textBaseline: 'bottom',
offsetY: -15,
}),
image: new RegularShape({
radius: 8,
points: 3,
angle: Math.PI,
displacement: [0, 10],
fill: new Fill({
color: 'rgba(0, 0, 0, 0.7)',
}),
}),
});
export const tipStyle = new Style({
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
padding: [2, 2, 2, 2],
textAlign: 'left',
offsetX: 15,
}),
});
export const modifyStyle = new Style({
image: new CircleStyle({
radius: 5,
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.7)',
}),
fill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
}),
text: new Text({
text: 'Drag to modify',
font: '12px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.7)',
}),
padding: [2, 2, 2, 2],
textAlign: 'left',
offsetX: 15,
}),
});
export const segmentStyle = new Style({
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
padding: [2, 2, 2, 2],
textBaseline: 'bottom',
offsetY: -12,
}),
image: new RegularShape({
radius: 6,
points: 3,
angle: Math.PI,
displacement: [0, 8],
fill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
}),
});
const formatLength = function (line: Geometry) {
const length = getLength(line);
let output;
if (length > 100) {
output = Math.round((length / 1000) * 100) / 100 + ' km';
} else {
output = Math.round(length * 100) / 100 + ' m';
}
return output;
};
const formatArea = function (polygon: Geometry) {
const area = getArea(polygon);
let output;
if (area > 10000) {
output = Math.round((area / 1000000) * 100) / 100 + ' km\xB2';
} else {
output = Math.round(area * 100) / 100 + ' m\xB2';
}
return output;
};
export function measureStyleFunction(
feature: FeatureLike,
drawType?: Type,
tip?: string,
setTipPoint?: React.Dispatch<React.SetStateAction<Point | null>>,
modify?: React.MutableRefObject<Modify>
) {
const styles = [];
const geometry = feature.getGeometry();
const type = geometry?.getType();
const segmentStyles = [segmentStyle];
const segments = getMeasureShowSegments()
if (!geometry) return
let point, label, line;
if (!drawType || drawType === type || type === 'Point') {
styles.push(style);
if (type === 'Polygon') {
point = (geometry as Polygon).getInteriorPoint();
label = formatArea(geometry as Polygon);
line = new LineString((geometry as Polygon).getCoordinates()[0]);
} else if (type === 'LineString') {
point = new Point((geometry as Polygon).getLastCoordinate());
label = formatLength(geometry as LineString);
line = geometry;
}
}
if (segments && line) {
let count = 0;
(line as LineString).forEachSegment(function (a, b) {
const segment = new LineString([a, b]);
const label = formatLength(segment);
if (segmentStyles.length - 1 < count) {
segmentStyles.push(segmentStyle.clone());
}
const segmentPoint = new Point(segment.getCoordinateAt(0.5));
segmentStyles[count].setGeometry(segmentPoint);
segmentStyles[count].getText()?.setText(label);
styles.push(segmentStyles[count]);
count++;
});
}
if (label) {
labelStyle.setGeometry(point as Geometry);
labelStyle.getText()?.setText(label);
styles.push(labelStyle);
}
if (
tip &&
type === 'Point' &&
!modify?.current.getOverlay()?.getSource()?.getFeatures().length
) {
setTipPoint?.(geometry as Point);
tipStyle.getText()?.setText(tip);
styles.push(tipStyle);
}
return styles;
}

View File

@ -0,0 +1,25 @@
import { Flex } from '@mantine/core'
import { IObjectData, IObjectType } from '../../interfaces/objects'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
const ObjectData = (object_data: IObjectData) => {
const { data: typeData } = useSWR(
object_data.type ? `/general/types/all` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
return (
<Flex gap='sm' direction='column'>
{Array.isArray(typeData) && (typeData.find(type => Number(type.id) === Number(object_data.type)) as IObjectType).name}
</Flex>
)
}
export default ObjectData

View File

@ -0,0 +1,87 @@
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { IObjectParam, IParam } from '../../interfaces/objects'
import TCBParameter from './TCBParameter'
import TableValue from './TableValue'
interface ObjectParameterProps {
showLabel?: boolean,
param: IObjectParam,
}
const ObjectParameter = ({
param
}: ObjectParameterProps) => {
const { data: paramData } = useSWR(
`/general/params/all?param_id=${param.id_param}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res[0] as IParam),
{
revalidateOnFocus: false,
revalidateIfStale: false
}
)
const Parameter = (type: string, name: string, value: unknown, vtable: string, unit: string | null) => {
switch (type) {
case 'bit':
return (
<TableValue value={value} name={name} type='boolean' />
)
case 'bigint':
return (
<TableValue value={value} name={name} type='number' />
)
case 'tinyint':
return (
<TableValue value={value} name={name} type='number' />
)
// TODO: Calculate from calc procedures
case 'calculate':
return (
<TableValue value={value} name={name} type='value' />
)
case 'GTCB':
return (
<TCBParameter value={value as string} vtable={vtable} name={name} />
)
case 'TCB':
return (
<TCBParameter value={value as string} vtable={vtable} name={name} />
)
case type.match(/varchar\((\d+)\)/)?.input:
return (
<TableValue value={value} name={name} type='string' />
)
case type.match(/numeric\((\d+),(\d+)\)/)?.input:
return (
<TableValue value={value} name={name} type='number' unit={unit} />
)
case 'year':
return (
<TableValue value={value} name={name} type='number' />
)
case 'uniqueidentifier':
return (
<TableValue value={value} name={name} type='value'/>
)
default:
return (
<div>
{name}
{value as string}
</div>
)
}
}
return (
<>
{paramData &&
Parameter(paramData.format, paramData.name, param.value, paramData.vtable, paramData.unit)
}
</>
)
}
export default ObjectParameter

View File

@ -0,0 +1,59 @@
import { Flex, LoadingOverlay } from '@mantine/core';
import { IObjectParam } from '../../../interfaces/objects';
import ObjectParameter from '../ObjectParameter';
import useSWR from 'swr';
import { BASE_URL } from '../../../constants';
import { fetcher } from '../../../http/axiosInstance';
import { useObjectsStore } from '../../../store/objects';
const ObjectParameters = () => {
const { currentObjectId } = useObjectsStore()
const { data: valuesData, isValidating: valuesValidating } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false,
revalidateIfStale: false
}
)
return (
<Flex gap={'sm'} direction={'column'} pos='relative'>
<LoadingOverlay visible={valuesValidating} />
{Array.isArray(valuesData) &&
Object.entries(
valuesData.reduce((acc, param) => {
if (!acc[param.id_param]) {
acc[param.id_param] = [];
}
acc[param.id_param].push(param);
return acc;
}, {} as Record<string, IObjectParam[]>)
).map(([id_param, params]) => {
// Step 1: Sort the parameters by date_s (start date) and date_po (end date)
const sortedParams = (params as IObjectParam[]).sort((b, a) => {
const dateA = new Date(a.date_s || 0);
const dateB = new Date(b.date_s || 0);
return dateA.getTime() - dateB.getTime();
});
return sortedParams.length > 1 ? (
sortedParams.map((param: IObjectParam) => {
if (param.date_po == null) {
return (
<ObjectParameter key={id_param} param={param} showLabel={false} />
)
}
}
)
) : (
<ObjectParameter key={id_param} param={sortedParams[0]} />
);
})
}
</Flex>
)
}
export default ObjectParameters

View File

@ -0,0 +1,25 @@
import useSWR from 'swr'
import { BASE_URL } from '../../constants'
import { fetcher } from '../../http/axiosInstance'
import { Flex } from '@mantine/core'
const RegionSelect = () => {
const { data } = useSWR(`/gis/regions/borders`, (url) => fetcher(url, BASE_URL.ems), {
revalidateOnFocus: false,
revalidateIfStale: false
})
return (
<Flex align='center' justify='center'>
{Array.isArray(data) &&
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width='100%' height='100vh' transform='scale(1, -1)'>
{data.map((el, index) => (
<path key={`path-${index}`} d={el.path} fill="white" stroke="black" />
))}
</svg>
}
</Flex>
)
}
export default RegionSelect

View File

@ -0,0 +1,100 @@
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Text } from '@mantine/core'
import TableValue from './TableValue'
interface ITCBParameterProps {
value: string;
vtable: string;
inactive?: boolean;
name: string;
}
const TCBParameter = ({
value,
vtable,
name
}: ITCBParameterProps) => {
//Get value
const { data: tcbValue } = useSWR(
`/general/params/tcb?id=${value}&vtable=${vtable}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res[0]),
{
revalidateOnFocus: false,
revalidateIfStale: false
}
)
const tables = [
'PipesTypes',
'vAddRepairEvent',
'vBoilers',
'vBoilersAppointment',
'vBoilersBalance',
'vBoilersCondition',
'vBoilersFuels',
'vBoilersHotWater',
'vBoilersPerimeter',
'vBoilersPeriods',
'vBoilersScheme',
'vBoilersState',
'vBoilersTypes',
'vBuildingManagement',
'vBuildingOwner',
'vCanalization',
'vColdWaterTypes',
'vConditionEquipment',
'vCovering',
'vDryer',
'vElectroSupplyTypes',
'vEquipmentsTypes',
'vFoundation',
'vFuelsFeed',
'vGasSupplyTypes',
'vHeatingTypes',
'vHeatTransfer',
'vHotWaterTypes',
'vMaterialsWall',
'vNormative',
'vPipeDiameters',
'vPipeOutDiameters',
'vPipesBearingType',
'vPipesCovering',
'vPipesGround',
'vPipesIsolation',
'vPipesLayer',
'vPipesMaterial',
'vRepairEvent',
'vRoof',
'vRPSType',
'vStreets',
'vTechStatus',
'vTrash',
'vVentilation',
'vWallingEquipment',
'tTypes',
]
const TCBValue = (vtable: string) => {
if (tables.includes(vtable)) {
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
} else {
return (
<Text>
{JSON.stringify(name)}
{JSON.stringify(tcbValue)}
</Text>
)
}
}
return (
TCBValue(vtable)
)
}
export default TCBParameter

View File

@ -0,0 +1,71 @@
import { Checkbox, ComboboxData, Grid, NumberInput, Select, Text, Textarea } from '@mantine/core';
import useSWR from 'swr';
import { fetcher } from '../../http/axiosInstance';
import { BASE_URL } from '../../constants';
import { useObjectsStore } from '../../store/objects';
interface TableValueProps {
name: string;
value: unknown;
type: 'value' | 'boolean' | 'number' | 'select' | 'string';
unit?: string | null | undefined;
vtable?: string;
}
const TableValue = ({
name,
value,
type,
unit,
vtable
}: TableValueProps) => {
const { selectedDistrict } = useObjectsStore()
//Get available values
const { data: tcbAll, isValidating } = useSWR(
type === 'select' && selectedDistrict ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedDistrict}` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) {
return res.map((el) => ({
label: el.name || "",
value: JSON.stringify(el.id)
})) as ComboboxData
}
}),
{
revalidateOnFocus: false,
revalidateIfStale: false
}
)
return (
<Grid>
<Grid.Col span={4} style={{ display: 'flex', alignItems: 'center' }}>
<Text size='xs' style={{ textWrap: 'wrap' }}>{name as string}</Text>
</Grid.Col>
<Grid.Col span={8}>
{type === 'boolean' ?
<Checkbox defaultChecked={value as boolean} />
:
type === 'number' ?
<NumberInput
size='xs'
value={value as number}
onChange={() => { }}
suffix={unit ? ` ${unit}` : ''}
/>
:
type === 'select' && !isValidating && tcbAll ?
<Select size='xs' data={tcbAll} defaultValue={JSON.stringify(value)} />
:
type === 'string' ?
<Textarea size='xs' defaultValue={value as string} autosize minRows={1} />
:
<Text size='xs'>{value as string}</Text>
}
</Grid.Col>
</Grid>
)
}
export default TableValue

View File

@ -0,0 +1,45 @@
import { ScrollAreaAutosize, Tabs } from '@mantine/core';
export interface ITabsPane {
title: string;
value: string;
view: JSX.Element;
}
export interface TabsPaneProps {
defaultTab: string;
tabs: ITabsPane[];
}
const TabsPane = ({
defaultTab,
tabs
}: TabsPaneProps) => {
return (
<Tabs defaultValue={defaultTab} mah='50%' h={'100%'} style={{
display: 'grid',
gridTemplateRows: 'min-content auto'
}}>
<ScrollAreaAutosize>
<Tabs.List>
{tabs.map((tab) => (
<Tabs.Tab key={tab.value} value={tab.value}>
{tab.title}
</Tabs.Tab>
))}
</Tabs.List>
</ScrollAreaAutosize>
<ScrollAreaAutosize h='100%' offsetScrollbars p='xs'>
{tabs.map(tab => (
<Tabs.Panel key={tab.value} value={tab.value}>
{tab.view}
</Tabs.Panel>
))}
</ScrollAreaAutosize>
</Tabs>
)
}
export default TabsPane

View File

@ -0,0 +1,590 @@
import { Coordinate, distance, rotate } from "ol/coordinate";
import { containsExtent, Extent, getCenter, getHeight, getWidth } from "ol/extent";
import Feature from "ol/Feature";
import GeoJSON from "ol/format/GeoJSON";
import { Circle, Geometry, LineString, Polygon, SimpleGeometry } from "ol/geom";
import VectorLayer from "ol/layer/Vector";
import VectorImageLayer from "ol/layer/VectorImage";
import Map from "ol/Map";
import { addCoordinateTransforms, addProjection, get, getTransform, Projection, ProjectionLike, transform } from "ol/proj";
import VectorSource from "ol/source/Vector";
import proj4 from "proj4";
import { selectStyle } from "./MapStyles";
import { Type } from "ol/geom/Geometry";
import { Draw, Modify, Snap, Translate } from "ol/interaction";
import { noModifierKeys } from "ol/events/condition";
import { IGeometryType, IRectCoords } from "../../interfaces/map";
import { uploadCoordinates } from "../../actions/map";
import { ImageStatic } from "ol/source";
import ImageLayer from "ol/layer/Image";
import { IFigure, ILine } from "../../interfaces/gis";
import { fromCircle, fromExtent } from "ol/geom/Polygon";
import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles";
import { getCurrentTool, getMeasureClearPrevious, getMeasureType, getTipPoint, setStatusText } from "../../store/map";
import { MutableRefObject } from "react";
import { getSelectedCity, getSelectedYear, setSelectedRegion } from "../../store/objects";
const calculateAngle = (coords: [number, number][]) => {
const [start, end] = coords;
const dx = end[0] - start[0];
const dy = end[1] - start[1];
return Math.atan2(dy, dx); // Angle in radians
}
export function processLine(
line: ILine,
scaling: number,
mapCenter: Coordinate,
linesLayer: MutableRefObject<VectorLayer<VectorSource>>
) {
const x1 = line.x1 * scaling
const y1 = line.y1 * scaling
const x2 = line.x2 * scaling
const y2 = line.y2 * scaling
const center = [mapCenter[0], mapCenter[1]]
const testCoords: [number, number][] = [
[center[0] + x1, center[1] - y1],
[center[0] + x2, center[1] - y2],
]
const feature = new Feature(new LineString(testCoords))
feature.set('type', line.type)
feature.set('geometry_type', 'line')
feature.set('planning', line.planning)
feature.set('object_id', line.object_id)
feature.set('rotation', calculateAngle(testCoords))
linesLayer.current?.getSource()?.addFeature(feature)
}
export function processFigure(
figure: IFigure,
scaling: number,
mapCenter: Coordinate,
figuresLayer: MutableRefObject<VectorLayer<VectorSource>>
) {
if (figure.figure_type_id == 1) {
const width = figure.width * scaling
const height = figure.height * scaling
const left = figure.left * scaling
const top = figure.top * scaling
const centerX = mapCenter[0] + left + (width / 2)
const centerY = mapCenter[1] - top - (height / 2)
const radius = width / 2;
const circleGeom = new Circle([centerX, centerY], radius)
const ellipseGeom = fromCircle(circleGeom, 64)
ellipseGeom.scale(1, height / width)
const feature = new Feature(ellipseGeom)
feature.set('type', figure.type)
feature.set('object_id', figure.object_id)
feature.set('planning', figure.planning)
figuresLayer.current?.getSource()?.addFeature(feature)
}
if (figure.figure_type_id == 3) {
const x = figure.left * scaling
const y = figure.top * scaling
const center = [mapCenter[0] + x, mapCenter[1] - y]
const coords = figure.points?.split(' ').map(pair => {
const [x, y] = pair.split(';').map(Number)
return [
center[0] + (x * scaling),
center[1] - (y * scaling)
]
})
if (coords) {
const polygon = new Polygon([coords])
const feature = new Feature({
geometry: polygon
})
feature.set('object_id', figure.object_id)
feature.set('planning', figure.planning)
feature.set('type', figure.type)
figuresLayer.current?.getSource()?.addFeature(feature)
}
}
if (figure.figure_type_id == 4) {
const width = figure.width * scaling
const height = figure.height * scaling
const left = figure.left * scaling
const top = figure.top * scaling
const halfWidth = width / 2
const halfHeight = height / 2
const center = [mapCenter[0] + left + halfWidth, mapCenter[1] - top - halfHeight]
const testCoords = [
[center[0] - halfWidth, center[1] - halfHeight],
[center[0] - halfWidth, center[1] + halfHeight],
[center[0] + halfWidth, center[1] + halfHeight],
[center[0] + halfWidth, center[1] - halfHeight],
[center[0] - halfWidth, center[1] - halfHeight]
]
const geometry1 = new Polygon([testCoords])
const anchor1 = center
geometry1.rotate(-figure.angle * Math.PI / 180, anchor1)
const feature1 = new Feature(geometry1)
feature1.set('object_id', figure.object_id)
feature1.set('planning', figure.planning)
feature1.set('type', figure.type)
feature1.set('angle', figure.angle)
figuresLayer.current?.getSource()?.addFeature(feature1)
}
}
// Function to update the image layer with a new source when extent changes
export const updateImageSource = (
imageUrl: string,
imageLayer: React.MutableRefObject<ImageLayer<ImageStatic>>,
polygonFeature: Feature<Polygon>,
setPolygonExtent: (value: React.SetStateAction<Extent | undefined>) => void,
setRectCoords: React.Dispatch<React.SetStateAction<IRectCoords | undefined>>
) => {
const newExtent = polygonFeature.getGeometry()?.getExtent();
const bottomLeft = polygonFeature.getGeometry()?.getCoordinates()[0][0]
const topLeft = polygonFeature.getGeometry()?.getCoordinates()[0][1]
const topRight = polygonFeature.getGeometry()?.getCoordinates()[0][2]
const bottomRight = polygonFeature.getGeometry()?.getCoordinates()[0][3]
setRectCoords({
bl: bottomLeft,
tl: topLeft,
tr: topRight,
br: bottomRight
})
setPolygonExtent(newExtent)
if (newExtent && bottomLeft && bottomRight && topRight && topLeft) {
const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight)
const newImageSource = new ImageStatic({
url: imageUrl,
imageExtent: originalExtent,
projection: rotateProjection('EPSG:3857', calculateRotationAngle(bottomLeft, bottomRight), originalExtent)
});
imageLayer.current.setSource(newImageSource);
}
};
export const addInteractions = (
drawingLayerSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>,
translate: React.MutableRefObject<Translate | null>,
draw: React.MutableRefObject<Draw | null>,
map: React.MutableRefObject<Map | null>,
snap: React.MutableRefObject<Snap | null>,
measureDraw: React.MutableRefObject<Draw | null>,
measureSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>,
measureModify: React.MutableRefObject<Modify>,
) => {
const currentTool = getCurrentTool()
const clearPrevious = getMeasureClearPrevious()
const measureType = getMeasureType()
const tipPoint = getTipPoint()
if (currentTool !== 'Measure' && currentTool !== 'Mover' && currentTool !== 'Edit') {
draw.current = new Draw({
source: drawingLayerSource.current,
type: currentTool as Type,
condition: noModifierKeys
})
draw.current.on('drawend', function (s) {
console.log(s.feature.getGeometry()?.getType())
let type: IGeometryType = 'POLYGON'
switch (s.feature.getGeometry()?.getType()) {
case 'LineString':
type = 'LINE'
break
case 'Polygon':
type = 'POLYGON'
break
default:
type = 'POLYGON'
break
}
const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates() as Coordinate[]
uploadCoordinates(coordinates, type)
})
map?.current?.addInteraction(draw.current)
snap.current = new Snap({ source: drawingLayerSource.current })
map?.current?.addInteraction(snap.current)
}
if (currentTool == 'Measure') {
const drawType = measureType;
const activeTip =
'Кликните, чтобы продолжить рисовать ' +
(drawType === 'Polygon' ? 'многоугольник' : 'линию');
const idleTip = 'Кликните, чтобы начать измерение';
let tip = idleTip;
measureDraw.current = new Draw({
source: measureSource.current,
type: drawType,
style: function (feature) {
return measureStyleFunction(feature, drawType, tip);
},
});
measureDraw.current.on('drawstart', function () {
if (clearPrevious) {
measureSource.current.clear();
}
measureModify.current.setActive(false);
tip = activeTip;
});
measureDraw.current.on('drawend', function () {
modifyStyle.setGeometry(tipPoint as Geometry);
measureModify.current.setActive(true);
map.current?.once('pointermove', function () {
modifyStyle.setGeometry('');
});
tip = idleTip;
});
measureModify.current.setActive(true);
map.current?.addInteraction(measureDraw.current);
}
if (currentTool == 'Mover') {
translate.current = new Translate()
map?.current?.addInteraction(translate.current)
}
if (currentTool == 'Edit') {
//const modify = new Modify()
//map?.current?.addInteraction(translate.current)
}
}
export function regionsInit(
map: React.MutableRefObject<Map | null>,
selectedRegion: React.MutableRefObject<Feature<Geometry> | null>,
regionsLayer: React.MutableRefObject<VectorImageLayer<Feature<Geometry>, VectorSource<Feature<Geometry>>>>,
) {
regionsLayer.current.once('change', function () {
if (getSelectedCity() || getSelectedYear()) return
const extent = regionsLayer.current.getSource()?.getExtent()
if (extent && !extent?.every(val => Math.abs(val) === Infinity)) {
map.current?.getView().fit(fromExtent(extent) as SimpleGeometry, { duration: 500, maxZoom: 18, padding: [60, 60, 60, 60] })
}
})
map.current?.on('click', function (e) {
if (selectedRegion.current !== null) {
selectedRegion.current = null
}
if (map.current) {
map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) {
if (layer === regionsLayer.current) {
selectedRegion.current = feature as Feature
// Zoom to the selected feature
zoomToFeature(map, selectedRegion.current)
if (feature.get('id')) {
setSelectedRegion(feature.get('id'))
}
return true
} else return false
});
}
})
// Show current selected region
map.current?.on('pointermove', function (e) {
if (selectedRegion.current !== null) {
selectedRegion.current.setStyle(undefined)
selectedRegion.current = null
}
if (map.current) {
map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) {
if (layer === regionsLayer.current) {
selectedRegion.current = feature as Feature
selectedRegion.current.setStyle(selectStyle)
if (feature.get('district')) {
setStatusText(feature.get('district'))
}
return true
} else return false
})
}
})
// Hide regions layer when fully visible
map.current?.on('moveend', function () {
const viewExtent = map.current?.getView().calculateExtent(map.current.getSize())
const features = regionsLayer.current.getSource()?.getFeatures()
let isViewCovered = false
features?.forEach((feature: Feature) => {
const featureExtent = feature?.getGeometry()?.getExtent()
if (viewExtent && featureExtent) {
if (containsExtent(featureExtent, viewExtent)) {
isViewCovered = true
}
}
})
regionsLayer.current.setVisible(!isViewCovered)
})
}
const zoomToFeature = (map: React.MutableRefObject<Map | null>, feature: Feature) => {
const geometry = feature.getGeometry()
const extent = geometry?.getExtent()
if (map.current && extent) {
map.current.getView().fit(extent, {
duration: 300,
maxZoom: 19,
})
}
}
// Function to save features to localStorage
export const saveFeatures = (layerRef: MutableRefObject<VectorLayer<VectorSource> | null>) => {
const features = layerRef.current?.getSource()?.getFeatures()
if (features && features.length > 0) {
const geoJSON = new GeoJSON()
const featuresJSON = geoJSON.writeFeatures(features)
localStorage.setItem('savedFeatures', featuresJSON)
}
}
// Function to load features from localStorage
export const loadFeatures = (layerSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>) => {
const savedFeatures = localStorage.getItem('savedFeatures')
if (savedFeatures) {
const geoJSON = new GeoJSON()
const features = geoJSON.readFeatures(savedFeatures, {
featureProjection: 'EPSG:4326', // Ensure the projection is correct
})
layerSource.current?.addFeatures(features) // Add features to the vector source
//drawingLayer.current?.getSource()?.changed()
}
}
function rotateProjection(projection: ProjectionLike, angle: number, extent: Extent) {
function rotateCoordinate(coordinate: Coordinate, angle: number, anchor: Coordinate) {
const coord = rotate(
[coordinate[0] - anchor[0], coordinate[1] - anchor[1]],
angle
);
return [coord[0] + anchor[0], coord[1] + anchor[1]];
}
function rotateTransform(coordinate: Coordinate) {
return rotateCoordinate(coordinate, angle, getCenter(extent));
}
function normalTransform(coordinate: Coordinate) {
return rotateCoordinate(coordinate, -angle, getCenter(extent));
}
const normalProjection = get(projection);
if (normalProjection) {
const rotatedProjection = new Projection({
code: normalProjection.getCode() + ":" + angle.toString() + ":" + extent.toString(),
units: normalProjection.getUnits(),
extent: extent
});
addProjection(rotatedProjection);
addCoordinateTransforms(
"EPSG:4326",
rotatedProjection,
function (coordinate) {
return rotateTransform(transform(coordinate, "EPSG:4326", projection));
},
function (coordinate) {
return transform(normalTransform(coordinate), projection, "EPSG:4326");
}
);
addCoordinateTransforms(
"EPSG:3857",
rotatedProjection,
function (coordinate) {
return rotateTransform(transform(coordinate, "EPSG:3857", projection));
},
function (coordinate) {
return transform(normalTransform(coordinate), projection, "EPSG:3857");
}
);
// also set up transforms with any projections defined using proj4
if (typeof proj4 !== "undefined") {
const projCodes = Object.keys(proj4.defs);
projCodes.forEach(function (code) {
const proj4Projection = get(code) as Projection;
if (proj4Projection) {
if (!getTransform(proj4Projection, rotatedProjection)) {
addCoordinateTransforms(
proj4Projection,
rotatedProjection,
function (coordinate) {
return rotateTransform(
transform(coordinate, proj4Projection, projection)
);
},
function (coordinate) {
return transform(
normalTransform(coordinate),
projection,
proj4Projection
);
}
);
}
}
});
}
return rotatedProjection;
}
}
const calculateCentroid = (bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) => {
const x = (bottomLeft[0] + topLeft[0] + topRight[0] + bottomRight[0]) / 4;
const y = (bottomLeft[1] + topLeft[1] + topRight[1] + bottomRight[1]) / 4;
return [x, y];
}
function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) {
// Calculate the difference in x and y coordinates between bottom right and bottom left
const deltaX = bottomRight[0] - bottomLeft[0];
const deltaY = bottomRight[1] - bottomLeft[1];
// Calculate the angle using atan2
const angle = -Math.atan2(deltaY, deltaX);
return angle;
}
function calculateExtent(bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) {
const width = distance(bottomLeft, bottomRight);
const height = distance(bottomLeft, topLeft);
// Calculate the centroid of the polygon
const [centerX, centerY] = calculateCentroid(bottomLeft, topLeft, topRight, bottomRight);
// Define the extent based on the center and dimensions
const extent = [
centerX - width / 2, // minX
centerY - height / 2, // minY
centerX + width / 2, // maxX
centerY + height / 2 // maxY
];
return extent;
}
function getTilesPerSide(zoom: number) {
return Math.pow(2, zoom)
}
function normalize(value: number, min: number, max: number) {
return (value - min) / (max - min)
}
function getTileIndex(normalized: number, tilesPerSide: number) {
return Math.floor(normalized * tilesPerSide)
}
function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) {
const tilesPerSide = getTilesPerSide(zoom);
const minX = extent[0]
const minY = extent[1]
const maxX = extent[2]
const maxY = extent[3]
// Normalize the coordinates
const xNormalized = normalize(x, minX, maxX);
const yNormalized = normalize(y, minY, maxY);
// Get tile indices
const tileX = getTileIndex(xNormalized, tilesPerSide);
const tileY = getTileIndex(1 - yNormalized, tilesPerSide);
return { tileX, tileY };
}
function calculateCenter(geometry: SimpleGeometry) {
let center, coordinates, minRadius;
const type = geometry.getType();
if (type === 'Polygon') {
let x = 0;
let y = 0;
let i = 0;
coordinates = (geometry as Polygon).getCoordinates()[0].slice(1);
coordinates.forEach(function (coordinate) {
x += coordinate[0];
y += coordinate[1];
i++;
});
center = [x / i, y / i];
} else if (type === 'LineString') {
center = (geometry as LineString).getCoordinateAt(0.5);
coordinates = geometry.getCoordinates();
} else {
center = getCenter(geometry.getExtent());
}
let sqDistances;
if (coordinates) {
sqDistances = coordinates.map(function (coordinate: Coordinate) {
const dx = coordinate[0] - center[0];
const dy = coordinate[1] - center[1];
return dx * dx + dy * dy;
});
minRadius = Math.sqrt(Math.max(...sqDistances)) / 3;
} else {
minRadius =
Math.max(
getWidth(geometry.getExtent()),
getHeight(geometry.getExtent()),
) / 3;
}
return {
center: center,
coordinates: coordinates,
minRadius: minRadius,
sqDistances: sqDistances,
};
}
export {
rotateProjection,
calculateRotationAngle,
calculateExtent,
calculateCentroid,
getTilesPerSide,
normalize,
getTileIndex,
getGridCellPosition,
calculateCenter
}

View File

@ -0,0 +1,256 @@
import { useEffect, useRef } from 'react'
import { useDownload, useFileType } from '../../hooks/swrHooks';
import jsPreviewExcel from "@js-preview/excel"
import '@js-preview/excel/lib/index.css'
import jsPreviewDocx from "@js-preview/docx"
import '@js-preview/docx/lib/index.css'
import jsPreviewPdf from '@js-preview/pdf'
import { IDocument } from '../../interfaces/documents';
import { IconAlertTriangle, IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
import { Button, Flex, Grid, Loader, Modal, ScrollAreaAutosize, Text } from '@mantine/core';
interface Props {
open: boolean;
setOpen: (state: boolean) => void;
docs: IDocument[];
currentFileNo: number;
setCurrentFileNo: (state: number) => void;
}
interface ViewerProps {
url: string
}
function PdfViewer({
url
}: ViewerProps) {
const previewContainerRef = useRef(null)
const pdfPreviewer = jsPreviewPdf
useEffect(() => {
if (previewContainerRef && previewContainerRef.current) {
pdfPreviewer.init(previewContainerRef.current)
.preview(url)
}
return () => {
if (previewContainerRef && previewContainerRef.current) {
previewContainerRef.current = null
}
}
}, [previewContainerRef])
return (
<div ref={previewContainerRef} style={{
width: '100%',
height: '100%',
}} />
)
}
function DocxViewer({
url
}: ViewerProps) {
const previewContainerRef = useRef(null)
useEffect(() => {
if (previewContainerRef && previewContainerRef.current) {
jsPreviewDocx.init(previewContainerRef.current, {
breakPages: true,
inWrapper: true,
ignoreHeight: true,
})
.preview(url)
}
return () => {
if (previewContainerRef && previewContainerRef.current) {
previewContainerRef.current = null
}
}
}, [])
return (
<div ref={previewContainerRef} style={{
width: '100%',
height: '100%',
}} />
)
}
function ExcelViewer({
url
}: ViewerProps) {
const previewContainerRef = useRef(null)
useEffect(() => {
if (previewContainerRef && previewContainerRef.current) {
jsPreviewExcel.init(previewContainerRef.current)
.preview(url)
}
return () => {
if (previewContainerRef && previewContainerRef.current) {
previewContainerRef.current = null
}
}
}, [])
return (
<div ref={previewContainerRef} style={{
width: '100%',
height: '100%',
}} />
)
}
function ImageViewer({
url
}: ViewerProps) {
return (
<Flex style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
objectFit: 'contain',
width: '100%',
height: '100%'
}}>
<img alt='image-preview' src={url} style={{
display: 'flex',
maxWidth: '100%',
maxHeight: '100%'
}} />
</Flex>
)
}
export default function FileViewer({
open,
setOpen,
docs,
currentFileNo,
setCurrentFileNo
}: Props) {
const { file, isLoading: fileIsLoading } = useDownload(currentFileNo >= 0 ? docs[currentFileNo]?.document_folder_id : null, currentFileNo >= 0 ? docs[currentFileNo]?.id : null)
const { fileType, isLoading: fileTypeIsLoading } = useFileType(currentFileNo >= 0 ? docs[currentFileNo]?.name : null, currentFileNo >= 0 ? file : null)
const handleSave = async () => {
const url = window.URL.createObjectURL(file)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', docs[currentFileNo].name)
document.body.appendChild(link)
link.click()
link.remove()
}
return (
<Modal.Root fullScreen opened={open} onClose={() => setOpen(false)} scrollAreaComponent={ScrollAreaAutosize.Autosize}>
<Modal.Overlay />
<Modal.Content style={{
display: 'grid',
gridTemplateRows: 'min-content auto',
width: '100vw',
height: '100vh'
}}>
<Modal.Header>
<Modal.Title component='div' w='100%'>
<Flex align='center'>
<Text mr='auto'>{currentFileNo != -1 && docs[currentFileNo].name}</Text>
<Grid>
<Grid.Col span='auto'>
<Button
variant='transparent'
onClick={() => {
if (currentFileNo >= 0 && currentFileNo > 0) {
setCurrentFileNo(currentFileNo - 1)
}
}}
disabled={currentFileNo >= 0 && currentFileNo === 0}
>
<IconChevronLeft />
</Button>
</Grid.Col>
<Grid.Col span='auto'>
<Button
variant='transparent'
onClick={() => {
if (currentFileNo >= 0 && currentFileNo < docs.length) {
setCurrentFileNo(currentFileNo + 1)
}
}}
disabled={currentFileNo >= 0 && currentFileNo >= docs.length - 1}
>
<IconChevronRight />
</Button>
</Grid.Col>
</Grid>
<Button
autoFocus
variant='subtle'
onClick={handleSave}
>
Сохранить
</Button>
</Flex>
</Modal.Title>
<Modal.CloseButton ml='xl' />
</Modal.Header>
<Modal.Body style={{ display: 'flex', flexGrow: 1, height: '100%', width: '100vw' }}>
{fileIsLoading || fileTypeIsLoading ?
<Flex style={{
display: 'flex',
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center'
}}>
<Loader />
</Flex>
:
fileType === 'application/pdf' ?
<PdfViewer url={window.URL.createObjectURL(file)} />
:
fileType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ?
<ExcelViewer url={window.URL.createObjectURL(file)} />
:
fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ?
<DocxViewer url={window.URL.createObjectURL(file)} />
:
fileType?.startsWith('image/') ?
<ImageViewer url={window.URL.createObjectURL(file)} />
:
fileType && file ?
<Flex style={{ display: 'flex', gap: '16px', flexDirection: 'column', p: '16px' }}>
<Flex style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<IconAlertTriangle />
<Text>
Предпросмотр данного файла невозможен.
</Text>
</Flex>
<Flex>
<Button variant='contained' onClick={() => {
handleSave()
}}>
Сохранить
</Button>
</Flex>
</Flex>
:
null
}
</Modal.Body>
</Modal.Content>
</Modal.Root>
)
}

View File

@ -0,0 +1,160 @@
import { IconBuildingFactory2, IconComponents, IconDeviceDesktopAnalytics, IconFiles, IconHome, IconLogin, IconLogin2, IconMap, IconPassword, IconReport, IconServer, IconSettings, IconShield, IconTable, IconUsers } from "@tabler/icons-react";
import SignIn from "../pages/auth/SignIn";
import SignUp from "../pages/auth/SignUp";
import PasswordReset from "../pages/auth/PasswordReset";
import TableTest from "../pages/TableTest";
import ComponentTest from "../pages/ComponentTest";
import MonitorPage from "../pages/MonitorPage";
import Settings from "../pages/Settings";
import Main from "../pages/Main";
import Users from "../pages/Users";
import Roles from "../pages/Roles";
import Documents from "../pages/Documents";
import Reports from "../pages/Reports";
import Servers from "../pages/Servers";
import Boilers from "../pages/Boilers";
import MapTest from "../pages/MapTest";
// Определение страниц с путями и компонентом для рендера
const pages = [
{
label: "",
path: "/auth/signin",
icon: <IconLogin2 />,
component: <SignIn />,
drawer: false,
dashboard: false,
enabled: true,
},
{
label: "",
path: "/auth/signup",
icon: <IconLogin />,
component: <SignUp />,
drawer: false,
dashboard: false,
enabled: false,
},
{
label: "",
path: "/auth/password-reset",
icon: <IconPassword />,
component: <PasswordReset />,
drawer: false,
dashboard: false,
enabled: true,
},
{
label: "Настройки",
path: "/settings",
icon: <IconSettings />,
component: <Settings />,
drawer: false,
dashboard: true,
enabled: true,
},
{
label: "Главная",
path: "/",
icon: <IconHome />,
component: <Main />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Пользователи",
path: "/user",
icon: <IconUsers />,
component: <Users />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Роли",
path: "/role",
icon: <IconShield />,
component: <Roles />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Документы",
path: "/documents",
icon: <IconFiles />,
component: <Documents />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Отчеты",
path: "/reports",
icon: <IconReport />,
component: <Reports />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Серверы",
path: "/servers",
icon: <IconServer />,
component: <Servers />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Котельные",
path: "/boilers",
icon: <IconBuildingFactory2 />,
component: <Boilers />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "ИКС",
path: "/map-test",
icon: <IconMap />,
component: <MapTest />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Монитор",
path: "/monitor",
icon: <IconDeviceDesktopAnalytics />,
component: <MonitorPage />,
drawer: true,
dashboard: true,
enabled: false,
},
{
label: "Table test",
path: "/table-test",
icon: <IconTable />,
component: <TableTest />,
drawer: true,
dashboard: true,
enabled: false,
},
{
label: "Component test",
path: "/component-test",
icon: <IconComponents />,
component: <ComponentTest />,
drawer: true,
dashboard: true,
enabled: false,
},
]
export {
pages
}

View File

@ -0,0 +1,12 @@
export const USER_DATA_KEY = 'userData';
export const TOKEN_AUTH_KEY = 'authToken'
export const TOKEN_ISSUED_DATE_KEY = 'tokenIssuedDate';
export const TOKEN_EXPIRY_DURATION = 7 * 24 * 60 * 60 * 1000;
export const BASE_URL = {
auth: import.meta.env.VITE_API_AUTH_URL,
info: import.meta.env.VITE_API_INFO_URL,
fuel: import.meta.env.VITE_API_FUEL_URL,
servers: import.meta.env.VITE_API_SERVERS_URL,
ems: import.meta.env.VITE_API_EMS_URL,
}

View File

@ -0,0 +1,302 @@
import useSWR, { SWRConfiguration } from "swr";
import RoleService from "../services/RoleService";
import UserService from "../services/UserService";
import { fetcher } from "../http/axiosInstance";
import { fileTypeFromBlob } from "file-type/core";
import { BASE_URL } from "../constants";
const swrOptions: SWRConfiguration = {
revalidateOnFocus: false,
}
export function useRoles() {
const { data, error, isLoading } = useSWR(`/auth/roles`, RoleService.getRoles, swrOptions)
return {
roles: data?.data,
isLoading,
isError: error
}
}
export function useUsers() {
const { data, error, isLoading } = useSWR(`/auth/user`, UserService.getUsers, swrOptions)
return {
users: data?.data,
isLoading,
isError: error
}
}
export function useCompanies(limit?: number, offset?: number) {
const { data, error, isLoading } = useSWR(`/info/companies?limit=${limit || 10}&offset=${offset || 0}`, fetcher, swrOptions)
return {
companies: data,
isLoading,
isError: error
}
}
export function useFolders(limit?: number, offset?: number) {
const { data, error, isLoading } = useSWR(
`/info/document_folder?limit=${limit || 10}&offset=${offset || 0}`,
fetcher,
swrOptions
)
return {
folders: data,
isLoading,
isError: error
}
}
export function useDocuments(folder_id?: number) {
const { data, error, isLoading } = useSWR(
folder_id ? `/info/documents/${folder_id}` : null,
fetcher,
swrOptions
)
return {
documents: data,
isLoading,
isError: error
}
}
export function useDownload(folder_id?: number | null, id?: number | null) {
const { data, error, isLoading } = useSWR(
folder_id && id ? `/info/document/${folder_id}&${id}` : null,
folder_id && id ? (url) => fetcher(url, BASE_URL.info, "blob") : null,
swrOptions
)
return {
file: data,
isLoading,
isError: error
}
}
export function useFileType(fileName?: string | null, file?: Blob | null) {
const { data, error, isLoading } = useSWR(
fileName && file ? `/filetype/${fileName}` : null,
file ? () => fileTypeFromBlob(file) : null,
swrOptions
)
return {
fileType: data?.mime,
isLoading,
isError: error
}
}
export function useReport(city_id?: number | null) {
const { data, error, isLoading } = useSWR(
city_id ? `/info/reports/${city_id}?to_export=false` : null,
(url) => fetcher(url, BASE_URL.info),
swrOptions
)
return {
report: data ? JSON.parse(data) : [],
isLoading,
isError: error
}
}
export function useReportExport(city_id?: number | null, to_export?: boolean) {
const { data, error, isLoading } = useSWR(
city_id && to_export ? `/info/reports/${city_id}?to_export=${to_export}` : null,
(url) => fetcher(url, BASE_URL.info, 'blob'),
swrOptions
)
return {
reportExported: data ? data : null,
isLoading,
isError: error
}
}
// API general (fuel)
export function useAddress(limit?: number, page?: number) {
const { data, error, isLoading } = useSWR(
`/general/address?limit=${limit || 10}&page=${page || 1}`,
(url) => fetcher(url, BASE_URL.fuel),
swrOptions
)
return {
address: data,
isLoading,
isError: error
}
}
export function useRegions(limit?: number, page?: number, search?: string | null) {
const { data, error, isLoading } = useSWR(
`/general/regions?limit=${limit || 10}&page=${page || 1}${search ? `&search=${search}` : ''}`,
(url) => fetcher(url, BASE_URL.fuel),
swrOptions
)
return {
regions: data,
isLoading,
isError: error
}
}
export function useCities(limit?: number, page?: number, search?: string | null) {
const { data, error, isLoading } = useSWR(
`/general/cities?limit=${limit || 10}&page=${page || 1}${search ? `&search=${search}` : ''}`,
(url) => fetcher(url, BASE_URL.fuel),
swrOptions
)
return {
cities: data,
isLoading,
isError: error
}
}
export function useBoilers(limit?: number, page?: number, search?: string) {
const { data, error, isLoading } = useSWR(
`/general/boilers?limit=${limit || 10}&page=${page || 1}${search ? `&search=${search}` : ''}`,
(url) => fetcher(url, BASE_URL.fuel),
swrOptions
)
return {
boilers: data,
isLoading,
isError: error
}
}
// Servers
export function useServers(region_id?: number | null, offset?: number, limit?: number) {
const { data, error, isLoading } = useSWR(
region_id ? `/api/servers?region_id=${region_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/servers?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
swrOptions
)
return {
servers: data,
isLoading,
isError: error
}
}
export function useServersInfo(region_id?: number | null, offset?: number, limit?: number) {
const { data, error, isLoading } = useSWR(
region_id ? `/api/servers_info?region_id=${region_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/servers_info?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
swrOptions
)
return {
serversInfo: data,
isLoading,
isError: error
}
}
export function useServer(server_id?: number) {
const { data, error, isLoading } = useSWR(
server_id ? `/api/server/${server_id}` : null,
(url) => fetcher(url, BASE_URL.servers),
swrOptions
)
return {
server: data,
isLoading,
isError: error
}
}
export function useServerIps(server_id?: number | null, offset?: number, limit?: number) {
const { data, error, isLoading } = useSWR(
server_id ? `/api/server_ips?server_id=${server_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/server_ips?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
swrOptions
)
return {
serverIps: data,
isLoading,
isError: error
}
}
// Hardware
export function useHardwares(server_id?: number, offset?: number, limit?: number) {
const { data, error, isLoading } = useSWR(
server_id ? `/api/hardwares?server_id=${server_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/hardwares?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
swrOptions
)
return {
hardwares: data,
isLoading,
isError: error
}
}
export function useHardware(hardware_id?: number) {
const { data, error, isLoading } = useSWR(
hardware_id ? `/api/hardware/${hardware_id}` : null,
(url) => fetcher(url, BASE_URL.servers),
swrOptions
)
return {
hardware: data,
isLoading,
isError: error
}
}
// Storage
export function useStorages(hardware_id?: number, offset?: number, limit?: number) {
const { data, error, isLoading } = useSWR(
hardware_id ? `/api/storages?hardware_id=${hardware_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/storages?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
swrOptions
)
return {
storages: data,
isLoading,
isError: error
}
}
export function useStorage(storage_id?: number) {
const { data, error, isLoading } = useSWR(
storage_id ? `/api/storage/${storage_id}` : null,
(url) => fetcher(url, BASE_URL.servers),
swrOptions
)
return {
storage: data,
isLoading,
isError: error
}
}

View File

@ -0,0 +1,27 @@
import axios, { ResponseType } from 'axios';
import { useAuthStore } from '../store/auth';
const axiosInstance = axios.create();
axiosInstance.interceptors.request.use(
(config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export const fetcher = (url: string, baseURL?: string, responseType?: ResponseType) => axiosInstance.get(url, {
baseURL: baseURL || import.meta.env.VITE_API_INFO_URL,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
responseType: responseType ? responseType : "json"
}).then(res => res.data)
export default axiosInstance;

View File

@ -0,0 +1,39 @@
export interface User {
id: number;
}
export interface UserData extends User {
email: string;
login: string;
phone: string;
name: string;
surname: string;
is_active: boolean;
role_id: number;
}
export interface UserCreds extends User {
password: string;
}
export interface AuthState {
isAuthenticated: boolean;
token: string | null;
userData: UserData | null;
}
export interface LoginFormData {
username: string;
password: string;
grant_type: string;
scope?: string;
client_id?: string;
client_secret?: string;
}
export interface ApiResponse {
access_token: string;
data: JSON;
status: number;
statusText: string;
}

View File

@ -0,0 +1,20 @@
import { Validate } from "react-hook-form";
export type CreateFieldType = 'string' | 'number' | 'date' | 'dateTime' | 'boolean' | 'singleSelect' | 'actions' | 'custom'
export type InputType = 'password'
export interface CreateField {
key: string;
headerName?: string;
type: CreateFieldType;
required?: boolean;
defaultValue?: any;
inputType?: InputType;
validate?: Validate<string, boolean>;
/** Watch for field */
watch?: string;
/** Message on watch */
watchMessage?: string;
/** Should field be included in the request */
include?: boolean;
}

View File

@ -0,0 +1,65 @@
// owner_id relates to other companies
export interface ICompany {
name: string;
fullname: string;
description: string;
owner_id: number;
}
export interface IDepartment {
name: string;
fullname: string;
description: string;
company_id: number;
owner_id: number;
}
export interface IDocumentFolder {
id: number;
name: string;
description: string;
create_date: string;
}
export interface IDocument {
id: number;
document_folder_id: number,
name: string;
description: string;
department_id: number;
create_date: string;
}
export interface IBank {
name: string;
bik: string;
corschet: string;
activ: boolean;
id_1c: string;
}
export interface IOrganization {
full_name: string;
name: string;
inn: string;
ogrn: string;
kpp: string;
okopf: string;
legal_address: string;
actual_address: string;
mail_address: string;
id_budget: number;
fio_dir: string;
phone: string;
email: string;
comment: string;
id_bank: string;
id_1c: string;
active: boolean;
}
export interface IOrganizationBank {
id_organization: string;
id_banks: string;
rasch_schet: string;
}

View File

@ -0,0 +1,17 @@
export interface IRegion {
id: number;
name: string;
}
export interface ICity {
id: number;
name: string;
}
export interface IBoiler {
id_object: string;
boiler_name: string;
boiler_code: string;
id_city: number;
activity: boolean;
}

View File

@ -0,0 +1,36 @@
export interface IFigure {
object_id: string,
figure_type_id: number,
left: number,
top: number,
width: number,
height: number,
angle: number,
points: string | null,
label_left: number | null,
label_top: number | null,
label_angle: number | null,
label_size: number | null,
year: number,
type: number,
planning: boolean
}
export interface ILine {
object_id: string,
x1: number,
y1: number,
x2: number,
y2: number,
points: string | null,
label_offset: number,
group_id: string,
show_label: boolean,
forced_lengths: string,
label_sizes: string | null,
label_angels: string | null,
label_positions: string | null,
year: number,
type: number,
planning: boolean
}

View File

@ -0,0 +1,12 @@
import { Coordinate } from "ol/coordinate";
export type SatelliteMapsProvider = 'google' | 'yandex' | 'custom' | 'static'
export type IGeometryType = 'LINE' | 'POLYGON'
export interface IRectCoords {
bl: Coordinate | undefined,
tl: Coordinate | undefined,
tr: Coordinate | undefined,
br: Coordinate | undefined
}

View File

@ -0,0 +1,44 @@
export interface IObjectList {
id: number,
name: string,
count: number
}
export interface IObjectData {
object_id: string,
id_city: number,
year: number,
id_parent: number | null,
type: number,
planning: boolean,
activity: boolean,
kvr: string | null,
jur: string | null,
fuel: string | null,
boiler_id: string | null
}
export interface IObjectParam {
id_object: string,
id_param: number,
value: string,
date_s: string | null,
date_po: string | null,
id_user: number
}
export interface IParam {
id: number,
id_group: number | null,
name: string,
format: string,
vtable: string,
unit: string | null,
exact_format: string | null,
inHistory: string | null
}
export interface IObjectType {
id: number,
name: string
}

View File

@ -0,0 +1,3 @@
export interface PreferencesState {
darkMode: boolean;
}

View File

@ -0,0 +1,10 @@
export interface IRole {
name: string;
description?: string | null;
id: number;
}
export interface IRoleCreate {
name: string;
description?: string | null;
}

View File

@ -0,0 +1,33 @@
export interface IServer {
id: number;
name: string;
region_id: number;
}
export interface IServersInfo extends IServer {
servers_count: number;
IPs_count: number;
status: string;
}
export interface IServerIP {
name: string;
is_actual: boolean;
ip: string;
server_id: number;
}
export interface IHardware {
name: string;
os_info: string;
ram: string;
processor: string;
server_id: number;
}
export interface IStorage {
name: string;
size: string;
storage_type: string;
hardware_id: number;
}

View File

@ -0,0 +1,11 @@
export interface IUser {
id: number;
password: string;
email: string;
login: string;
phone: string;
name: string;
surname: string;
is_active: boolean;
role_id: number;
}

View File

@ -0,0 +1,141 @@
import { AppShell, Avatar, Burger, Button, Flex, Group, Image, Menu, NavLink, rem, Text, useMantineColorScheme } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { Outlet, useNavigate } from 'react-router-dom';
import { IconChevronDown, IconLogout, IconSettings, IconMoon, IconSun } from '@tabler/icons-react';
import { getUserData, logout, useAuthStore } from '../store/auth';
import { useEffect, useState } from 'react';
import { UserData } from '../interfaces/auth';
import { pages } from '../constants/app';
function DashboardLayout() {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
const navigate = useNavigate()
const getPageTitle = () => {
const currentPath = location.pathname
const allPages = [...pages]
const currentPage = allPages.find(page => page.path === currentPath)
return currentPage ? currentPage.label : "Панель управления"
}
const authStore = useAuthStore()
const [userData, setUserData] = useState<UserData>()
useEffect(() => {
if (authStore) {
const stored = getUserData()
if (stored) {
setUserData(stored)
}
}
}, [authStore])
const { colorScheme, setColorScheme } = useMantineColorScheme();
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: desktopOpened ? 200 : 50,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened },
}}
>
<AppShell.Header>
<Flex h="100%" px="md" w='100%' align='center' gap='sm'>
<Group>
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
</Group>
<Group w='auto'>
{getPageTitle()}
</Group>
<Group id='header-portal' w='auto' ml='auto'>
</Group>
<Group style={{ flexShrink: 0 }}>
<Menu
width={260}
position="bottom-end"
transitionProps={{ transition: 'pop-top-right' }}
withinPortal
>
<Menu.Target>
<Button variant='transparent'>
<Group gap={7}>
<Avatar name={`${userData?.name} ${userData?.surname}`} radius="xl" size={30} />
<Text fw={500} size="sm" lh={1} mr={3}>
{`${userData?.name} ${userData?.surname}`}
</Text>
<IconChevronDown style={{ width: rem(12), height: rem(12) }} stroke={1.5} />
</Group>
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>{userData?.login}</Menu.Label>
<Menu.Item
leftSection={
colorScheme === 'dark' ? <IconMoon style={{ width: rem(16), height: rem(16) }} stroke={1.5} /> : <IconSun style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
}
onClick={() => colorScheme === 'dark' ? setColorScheme('light') : setColorScheme('dark')}
>
Тема: {colorScheme === 'dark' ? 'тёмная' : 'светлая'}
</Menu.Item>
<Menu.Item
leftSection={
<IconSettings style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
}
onClick={() => navigate('/settings')}
>
Настройки профиля
</Menu.Item>
<Menu.Item
onClick={() => {
logout()
navigate("/auth/signin")
}}
leftSection={<IconLogout style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
>
Выход
</Menu.Item>
<Menu.Item>
<Flex gap='sm' align='center'>
<Image src={'/logo2.svg'} w={32} />
<Text>0.1.0</Text>
</Flex>
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Flex>
</AppShell.Header>
<AppShell.Navbar style={{ transition: "width 0.2s ease" }}>
{pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item) => (
<NavLink
key={item.path}
onClick={() => navigate(item.path)}
label={item.label}
leftSection={item.icon}
active={location.pathname === item.path}
style={{ textWrap: 'nowrap' }}
/>
))}
</AppShell.Navbar>
<AppShell.Main>
<Flex w={{
sm: desktopOpened ? 'calc(100% - 200px)' : 'calc(100% - 50px)',
base: '100%'
}} h={'calc(100% - 60px)'} style={{ transition: "width 0.2s ease" }} pos={'fixed'}>
<Outlet />
</Flex>
</AppShell.Main>
</AppShell>
)
}
export default DashboardLayout

View File

@ -0,0 +1,12 @@
// Layout for fullscreen pages
import { Flex } from "@mantine/core";
import { Outlet } from "react-router-dom";
export default function MainLayout() {
return (
<Flex align='center' justify='center' h='100%' w='100%'>
<Outlet />
</Flex>
)
}

25
client/src/main.tsx Normal file
View File

@ -0,0 +1,25 @@
import "@fontsource/inter";
import '@mantine/core/styles.css';
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { createTheme, DEFAULT_THEME, MantineProvider, mergeMantineTheme } from '@mantine/core';
const overrides = createTheme({
// Set this color to `--mantine-color-body` CSS variable
white: '#F0F0F0',
colors: {
// ...
},
})
const theme = mergeMantineTheme(DEFAULT_THEME, overrides);
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<MantineProvider theme={theme}>
<App />
</MantineProvider>
</React.StrictMode>,
)

View File

@ -0,0 +1,85 @@
import { useEffect, useState } from 'react'
import { useBoilers } from '../hooks/swrHooks'
import { Badge, ScrollAreaAutosize, Table, Text } from '@mantine/core'
import { IBoiler } from '../interfaces/fuel'
function Boilers() {
const [boilersPage, setBoilersPage] = useState(1)
const [boilerSearch, setBoilerSearch] = useState("")
const [debouncedBoilerSearch, setDebouncedBoilerSearch] = useState("")
const { boilers } = useBoilers(10, boilersPage, debouncedBoilerSearch)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedBoilerSearch(boilerSearch)
}, 500)
return () => {
clearTimeout(handler)
}
}, [boilerSearch])
useEffect(() => {
setBoilersPage(1)
setBoilerSearch("")
}, [])
const boilersColumns = [
{ field: 'id_object', headerName: 'ID', type: "number" },
{ field: 'boiler_name', headerName: 'Название', type: "string", flex: 1 },
{ field: 'boiler_code', headerName: 'Код', type: "string", flex: 1 },
{ field: 'id_city', headerName: 'Город', type: "string", flex: 1 },
{ field: 'activity', headerName: 'Активен', type: "boolean", flex: 1 },
]
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Text size="xl" fw={600}>
Котельные
</Text>
{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>
}
</ScrollAreaAutosize>
)
}
export default Boilers

View File

@ -0,0 +1,12 @@
import { Flex } from '@mantine/core'
import ServerHardware from '../components/ServerHardware'
const ComponentTest = () => {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<ServerHardware />
</Flex>
)
}
export default ComponentTest

View File

@ -0,0 +1,7 @@
import FolderViewer from '../components/FolderViewer'
export default function Documents() {
return (
<FolderViewer />
)
}

54
client/src/pages/Main.tsx Normal file
View File

@ -0,0 +1,54 @@
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 { useNavigate } from "react-router-dom";
export default function Main() {
const navigate = useNavigate()
interface CustomCardProps {
link: string;
icon: ReactNode;
label: string;
}
const CustomCard = ({
link,
icon,
label
}: CustomCardProps) => {
return (
<Card
onClick={() => navigate(link)}
withBorder
style={{ cursor: 'pointer', userSelect: 'none' }}
>
<Flex mih='50'>
{icon}
</Flex>
<Text fw={500} size="lg" mt="md">
{label}
</Text>
</Card>
)
}
return (
<Flex w={'100%'} h={'100%'} direction='column' gap='sm' p='sm'>
<Text size="xl" fw={700}>
Главная
</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>
)
}

View File

@ -0,0 +1,9 @@
import MapComponent from '../components/map/MapComponent'
function MapTest() {
return (
<MapComponent />
)
}
export default MapTest

View File

@ -0,0 +1,48 @@
import { useEffect, useState } from 'react'
import { Card, Flex } from '@mantine/core';
function CardComponent({
url,
is_alive
}: { url: string, is_alive: boolean }) {
return (
<Card>
<Flex p='sm' direction='column'>
<p>{url}</p>
<p>{JSON.stringify(is_alive)}</p>
</Flex>
</Card>
)
}
export default function MonitorPage() {
const [servers, setServers] = useState([])
useEffect(() => {
const eventSource = new EventSource(`${import.meta.env.VITE_API_MONITOR_URL}/watch`);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setServers(data)
}
eventSource.onerror = (error) => {
console.error('Error with SSE connection:', error)
eventSource.close()
}
return () => {
eventSource.close()
};
}, [])
return (
<div>
<Flex direction='column' gap='sm'>
{servers.length > 0 && servers.map((server: { name: string, status: boolean }) => (
<CardComponent url={server.name} is_alive={server.status} />
))}
</Flex>
</div>
)
}

View File

@ -0,0 +1,15 @@
import { Flex, Text } from "@mantine/core";
import { IconError404 } from "@tabler/icons-react";
export default function NotFound() {
return (
<Flex w={'100%'} h={'100%'} p='sm' gap='sm' align='center' justify='center'>
<Flex direction='column' gap='sm' align='center'>
<IconError404 size={100} />
<Text size="xl" fw={500} ta='center'>
Запрашиваемая страница не найдена.
</Text>
</Flex>
</Flex>
)
}

View File

@ -0,0 +1,138 @@
import { useEffect, useState } from "react"
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"
export default function Reports() {
const [download, setDownload] = useState(false)
const [search, setSearch] = useState<string | undefined>("")
const debouncedSearch = useDebounce(search, 500)
const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { cities } = useCities(10, 1, debouncedSearch)
const { report } = useReport(selectedOption)
const { reportExported } = useReportExport(selectedOption, download)
const refreshReport = async () => {
mutate(`/info/reports/${selectedOption}?to_export=false`)
}
useEffect(() => {
if (selectedOption && reportExported && download) {
const url = window.URL.createObjectURL(reportExported)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'report.xlsx')
document.body.appendChild(link);
link.click();
link.remove();
setDownload(false)
}
}, [selectedOption, reportExported, download])
const exportReport = async () => {
setDownload(true)
}
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Flex component="form" gap={'sm'}>
{/* <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}
/>
<ActionIcon size='auto' variant='transparent' onClick={() => refreshReport()}>
<IconRefresh />
</ActionIcon>
<Button disabled={!selectedOption} onClick={() => exportReport()}>
Экспорт
</Button>
</Flex>
{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: any = { id: Number(id) };
Object.keys(report).forEach(key => {
row[key] = report[key][id];
});
return (<Table.Tr key={row.id}>
{[
{ 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>
) : (
<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]}</Table.Td>
)
})}
</Table.Tr>)
})}
</Table.Tbody>
</Table>
}
</ScrollAreaAutosize>
)
}

View File

@ -0,0 +1,63 @@
import { useRoles } from '../hooks/swrHooks'
import { CreateField } from '../interfaces/create'
import RoleService from '../services/RoleService'
import FormFields from '../components/FormFields'
import { Button, Loader, Modal, ScrollAreaAutosize, Table } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { IRole } from '../interfaces/role'
export default function Roles() {
const { roles, isError, isLoading } = useRoles()
const [opened, { open, close }] = useDisclosure(false);
const createFields: CreateField[] = [
{ key: 'name', headerName: 'Название', type: 'string', required: true, defaultValue: '' },
{ key: 'description', headerName: 'Описание', type: 'string', required: false, defaultValue: '' },
]
const columns = [
{ field: 'id', headerName: 'ID', type: "number" },
{ field: 'name', headerName: 'Название', flex: 1, editable: true },
{ field: 'description', headerName: 'Описание', flex: 1, editable: true },
];
if (isError) return <div>Произошла ошибка при получении данных.</div>
if (isLoading) return <Loader />
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Button onClick={open}>
Добавить роль
</Button>
<Modal opened={opened} onClose={close} title="Создание роли" centered>
<FormFields
fields={createFields}
submitHandler={RoleService.createRole}
/>
</Modal>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{columns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{roles.map((role: IRole) => (
<Table.Tr
key={role.id}
>
{columns.map(column => (
<Table.Td key={column.field}>{role[column.field as keyof IRole]}</Table.Td>
))}
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollAreaAutosize>
)
}

View File

@ -0,0 +1,48 @@
import { useState } from "react"
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"
export default function Servers() {
const [currentTab, setCurrentTab] = useState<string | null>('0')
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>
<Tabs.Panel value="0" pt='sm'>
<ServersView />
</Tabs.Panel>
<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

@ -0,0 +1,68 @@
import UserService from "../services/UserService"
import { setUserData, useAuthStore } from "../store/auth"
import { useEffect, useState } from "react"
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()
const [currentUser, setCurrentUser] = useState<IUser>()
const fetchCurrentUser = async () => {
if (token) {
await UserService.getCurrentUser(token).then(response => {
setCurrentUser(response.data)
})
}
}
useEffect(() => {
if (token) {
fetchCurrentUser()
}
}, [token])
const profileFields: CreateField[] = [
//{ key: 'email', headerName: 'E-mail', type: 'string', required: true },
//{ key: 'login', headerName: 'Логин', type: 'string', required: true },
{ key: 'phone', headerName: 'Телефон', type: 'string', required: false },
{ key: 'name', headerName: 'Имя', type: 'string', required: true },
{ key: 'surname', headerName: 'Фамилия', type: 'string', required: true },
]
const passwordFields: CreateField[] = [
{ key: 'password', headerName: 'Новый пароль', type: 'string', required: true, inputType: 'password' },
{ key: 'password_confirm', headerName: 'Подтверждение пароля', type: 'string', required: true, inputType: 'password', watch: 'password', watchMessage: 'Пароли не совпадают', include: false },
]
return (
<ScrollAreaAutosize
w={'100%'}
h={'100%'}
p='sm'
>
{currentUser &&
<Flex direction='column' gap='sm' w='100%'>
<FormFields
fields={profileFields}
defaultValues={currentUser}
mutateHandler={(data: any) => {
setUserData(data)
}}
submitHandler={(data) => UserService.updateUser({ id: currentUser.id, ...data })}
title="Пользователь"
/>
<FormFields
fields={passwordFields}
submitHandler={(data) => AuthService.updatePassword({ id: currentUser.id, ...data })}
title="Смена пароля"
/>
</Flex>
}
</ScrollAreaAutosize>
)
}

View File

@ -0,0 +1,13 @@
import { Flex } from '@mantine/core';
import CustomTable from '../components/CustomTable';
function TableTest() {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<CustomTable />
</Flex>
)
}
export default TableTest

161
client/src/pages/Users.tsx Normal file
View File

@ -0,0 +1,161 @@
import { useRoles, useUsers } from "../hooks/swrHooks"
import { IRole } from "../interfaces/role"
import { useEffect, useState } from "react"
import { CreateField } from "../interfaces/create"
import UserService from "../services/UserService"
import FormFields from "../components/FormFields"
import { Badge, Button, Flex, Loader, Modal, Pagination, ScrollAreaAutosize, Select, Table } from "@mantine/core"
import { useDisclosure } from "@mantine/hooks"
import { IUser } from "../interfaces/user"
export default function Users() {
const { users, isError, isLoading } = useUsers()
const { roles } = useRoles()
const [roleOptions, setRoleOptions] = useState<{ label: string, value: string }[]>()
useEffect(() => {
if (Array.isArray(roles)) {
setRoleOptions(roles.map((role: IRole) => ({ label: role.name, value: role.id.toString() })))
}
}, [roles])
const [opened, { open, close }] = useDisclosure(false);
const createFields: CreateField[] = [
{ key: 'email', headerName: 'E-mail', type: 'string', required: true, defaultValue: '' },
{ key: 'login', headerName: 'Логин', type: 'string', required: true, defaultValue: '' },
{ key: 'phone', headerName: 'Телефон', type: 'string', required: false, defaultValue: '' },
{ key: 'name', headerName: 'Имя', type: 'string', required: true, defaultValue: '' },
{ key: 'surname', headerName: 'Фамилия', type: 'string', required: true, defaultValue: '' },
{ key: 'password', headerName: 'Пароль', type: 'string', required: true, defaultValue: '' },
]
const columns = [
{ field: 'id', headerName: 'ID', type: "number", flex: 1 },
{ field: 'email', headerName: 'Email', flex: 1, editable: true },
{ field: 'login', headerName: 'Логин', flex: 1, editable: true },
{ field: 'phone', headerName: 'Телефон', flex: 1, editable: true },
{ field: 'name', headerName: 'Имя', flex: 1, editable: true },
{ field: 'surname', headerName: 'Фамилия', flex: 1, editable: true },
{ field: 'is_active', headerName: 'Статус', type: "boolean", flex: 1, editable: true },
{
field: 'role_id',
headerName: 'Роль',
valueOptions: roles ? roles.map((role: IRole) => ({ label: role.name, value: role.id })) : [],
type: 'singleSelect',
flex: 1,
editable: true
},
];
if (isError) return (
<div>
Произошла ошибка при получении данных.
</div>
)
if (isLoading) {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<Loader />
</Flex>
)
}
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Button onClick={open}>
Добавить пользователя
</Button>
<Modal opened={opened} onClose={close} title="Регистрация пользователя" centered>
<FormFields
fields={createFields}
submitHandler={UserService.createUser}
/>
</Modal>
{Array.isArray(roleOptions) &&
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{columns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{users.map((user: IUser) => (
<Table.Tr
key={user.id}
//bg={selectedRows.includes(element.position) ? 'var(--mantine-color-blue-light)' : undefined}
>
{columns.map(column => {
if (column.field === 'is_active') {
return (
user.is_active ? (
<Table.Td key={column.field}>
<Badge fullWidth variant="light">
Активен
</Badge>
</Table.Td>
) : (
<Table.Td key={column.field}>
<Badge color="gray" fullWidth variant="light">
Отключен
</Badge>
</Table.Td>
)
)
}
else if (column.field === 'role_id') {
return (
<Table.Td key={column.field}>
<Select
data={roleOptions}
defaultValue={user.role_id.toString()}
variant="unstyled"
allowDeselect={false}
/>
</Table.Td>
)
}
else return (
<Table.Td key={column.field}>{user[column.field as keyof IUser]}</Table.Td>
)
})}
</Table.Tr>
))}
</Table.Tbody>
</Table>
}
<Pagination total={10} />
{/* <DataGrid
density="compact"
autoHeight
style={{ width: "100%" }}
rows={users}
columns={columns}
initialState={{
pagination: {
paginationModel: { page: 0, pageSize: 10 },
},
}}
pageSizeOptions={[10, 20, 50, 100]}
checkboxSelection
disableRowSelectionOnClick
processRowUpdate={(updatedRow) => {
return updatedRow
}}
onProcessRowUpdateError={() => {
}}
/> */}
</ScrollAreaAutosize>
)
}

View File

@ -0,0 +1,96 @@
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';
interface PasswordResetProps {
email: string;
}
function PasswordReset() {
const [success, setSuccess] = useState(false)
const { register, handleSubmit, watch, setError, formState: { errors, isSubmitting } } = useForm<PasswordResetProps>({
defaultValues: {
email: ''
}
})
const onSubmit: SubmitHandler<PasswordResetProps> = async (data) => {
await AuthService.resetPassword(data.email).then(response => {
if (response.status === 200) {
//setError('email', { message: response.data.msg })
setSuccess(true)
} else if (response.status === 422) {
setError('email', { message: response.statusText })
}
}).catch((error: Error) => {
setError('email', { message: error.message })
})
}
return (
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Flex direction='column' gap='sm'>
<Text size="xl" fw={500}>
Восстановление пароля
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
{!success &&
<Transition mounted={!success} transition='fade'>
{(styles) =>
<Flex style={styles} direction='column' gap={'md'}>
<Text>
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
</Text>
<TextInput
label='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>
<Button flex={1} component='a' href="/auth/signin" type="button" variant='light'>
Назад
</Button>
</Flex>
</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>
)
}
export default PasswordReset

View File

@ -0,0 +1,99 @@
import { useForm, SubmitHandler } from 'react-hook-form';
import { AxiosError, AxiosResponse } from 'axios';
import { ApiResponse, LoginFormData } from '../../interfaces/auth';
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';
const SignIn = () => {
const { register, handleSubmit, setError, formState: { errors, isSubmitting, isValid } } = useForm<LoginFormData>({
defaultValues: {
username: '',
password: '',
grant_type: 'password',
scope: '',
client_id: '',
client_secret: ''
}
})
const navigate = useNavigate();
const onSubmit: SubmitHandler<LoginFormData> = async (data) => {
const formBody = new URLSearchParams();
for (const key in data) {
formBody.append(key, data[key as keyof LoginFormData] as string);
}
try {
const response: AxiosResponse<ApiResponse> = await AuthService.login(formBody)
const token = response.data.access_token
const userDataResponse: AxiosResponse<ApiResponse> = await UserService.getCurrentUser(token)
setUserData(JSON.stringify(userDataResponse.data))
login(token)
navigate('/');
} catch (error: unknown) {
if ((error as AxiosError).response?.data) {
const err = (error as AxiosError).response?.data
setError('password', {
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>
<form onSubmit={handleSubmit(onSubmit)}>
<Flex direction='column' gap='sm'>
<TextInput
label='Логин'
required
{...register('username', { required: 'Введите логин' })}
error={errors.username?.message}
/>
<TextInput
label='Пароль'
type='password'
required
{...register('password', { required: 'Введите пароль' })}
error={errors.password?.message}
/>
<Flex justify='flex-end' gap='sm'>
<Button component='a' href='/auth/password-reset' variant='transparent'>
Восстановить пароль
</Button>
</Flex>
<Flex gap='sm'>
<Button disabled={!isValid} type="submit" flex={1} variant='filled'>
{isSubmitting ? <Loader size={16} /> : 'Вход'}
</Button>
{/* <Button component='a' flex={1} href='/auth/signup' type="button" variant='light'>
Регистрация
</Button> */}
</Flex>
</Flex>
</form>
</Flex>
</Paper>
);
};
export default SignIn;

View File

@ -0,0 +1,92 @@
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';
const SignUp = () => {
const { register, handleSubmit, formState: { errors, isValid, isSubmitting } } = useForm<IUser>({
defaultValues: {
email: '',
login: '',
phone: '',
name: '',
surname: '',
is_active: true,
password: '',
}
})
const onSubmit: SubmitHandler<IUser> = async (data) => {
try {
await UserService.createUser(data)
} catch (error) {
console.error('Ошибка регистрации:', error);
}
};
return (
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Flex direction='column' gap='sm'>
<Text size="xl" fw={500}>
Регистрация
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
<Flex direction='column' gap='sm'>
<TextInput
label='Email'
required
{...register('email', { required: 'Email обязателен' })}
error={errors.email?.message}
/>
<TextInput
label='Логин'
required
{...register('login', { required: 'Логин обязателен' })}
error={errors.login?.message}
/>
<TextInput
label='Телефон'
required
{...register('phone')}
error={errors.phone?.message}
/>
<TextInput
label='Имя'
required
{...register('name')}
error={errors.name?.message}
/>
<TextInput
label='Фамилия'
required
{...register('surname')}
error={errors.surname?.message}
/>
<TextInput
label='Пароль'
type="password"
required
{...register('password', { required: 'Пароль обязателен' })}
error={errors.password?.message}
/>
<Flex gap='sm'>
<Button disabled={!isValid} type="submit" flex={1} variant='filled'>
{isSubmitting ? <Loader size={16} /> : 'Зарегистрироваться'}
</Button>
</Flex>
</Flex>
</form>
</Flex>
</Paper>
);
};
export default SignUp;

View File

@ -0,0 +1,10 @@
$ka-background-color: #2c2c2c;
$ka-border-color: #4d4d4d;
$ka-cell-hover-background-color: adjust(#fff, 0.8);
$ka-color-base: #fefefe;
$ka-input-background-color: $ka-background-color;
$ka-input-border-color: $ka-border-color;
$ka-input-color: $ka-color-base;
$ka-row-hover-background-color: adjust(#fff, 0.9);
$ka-thead-background-color: #1b1b1b;
$ka-thead-color: #c5c5c5;

View File

@ -0,0 +1,32 @@
import { AxiosRequestConfig } from "axios";
import { BASE_URL } from "../constants";
import axiosInstance from "../http/axiosInstance";
const config: AxiosRequestConfig = {
baseURL: BASE_URL.auth,
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
// }
}
export default class AuthService {
static async login(data: URLSearchParams) {
return await axiosInstance.post(`/auth/login`, data, config)
}
static async refreshToken(token: string) {
return await axiosInstance.post(`/auth/refresh_token/${token}`, null, config)
}
static async getCurrentUser(token: string) {
return await axiosInstance.get(`/auth/get_current_user/${token}`, config)
}
static async resetPassword(email: string) {
return await axiosInstance.put(`/auth/user/reset_password?email=${email}`, null, config)
}
static async updatePassword(data: { id: number, password: string }) {
return await axiosInstance.put(`/auth/user/password_change`, data, config)
}
}

View File

@ -0,0 +1,247 @@
import { AxiosProgressEvent, AxiosRequestConfig } from "axios";
import axiosInstance from "../http/axiosInstance";
import { IBank, ICompany, IDepartment, IDocument, IDocumentFolder, IOrganization, IOrganizationBank } from "../interfaces/documents";
import { BASE_URL } from "../constants";
const config: AxiosRequestConfig = {
baseURL: BASE_URL.info,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
export default class DocumentService {
// Get Main
static async getMain() {
return await axiosInstance.get(`/info/`, config)
}
// Get Companies
static async getCompanies(limit?: number, offset?: number) {
return await axiosInstance.get(`/info/companies`, {
params: {
limit: limit || 10,
offset: offset || 0
},
...config
})
}
// Create Company
static async createCompany(data: ICompany) {
return await axiosInstance.post(`/info/companies/`, data, config)
}
// Delete Company
static async deleteCompany(company_id: number) {
return await axiosInstance.delete(`/info/companies/${company_id}`, config)
}
// Update Company
static async updateCompany(company_id: number) {
return await axiosInstance.patch(`/info/companies/${company_id}`, config)
}
// Get Departments
static async getDepartments(limit?: number, offset?: number) {
return await axiosInstance.get(`/info/departments/`, {
params: {
limit: limit || 10,
offset: offset || 0
},
...config
})
}
// Get Department
static async getDepartment(department_id: number) {
return await axiosInstance.get(`/info/departments/${department_id}`, config)
}
// Delete Department
static async deleteDepartment(department_id: number) {
return await axiosInstance.delete(`/info/departments/${department_id}`, config)
}
// Update Department
static async updateDepartment(department_id: number, data: IDepartment) {
return await axiosInstance.patch(`/info/departments/${department_id}`, data, config)
}
// Create Department
static async createDepartment(data: IDepartment) {
return await axiosInstance.post(`/info/department/`, data, config)
}
// Get Documents
static async getDocuments(limit?: number, offset?: number) {
return await axiosInstance.get(`/info/document_folder/`, {
params: {
limit: limit || 10,
offset: offset || 0
},
...config
})
}
// Create Documentfolder
static async createDocumentFolder(data: IDocumentFolder) {
return await axiosInstance.post(`/info/document_folder/`, data, config)
}
// Get Document
static async getDocument(folder_id: number) {
return await axiosInstance.get(`/info/document_folder/${folder_id}`, config)
}
// Delete Document
static async deleteDocument(folder_id: number) {
return await axiosInstance.delete(`/info/document_folder/${folder_id}`, config)
}
// Update Document
static async updateDocument(folder_id: number, data: IDocument) {
return await axiosInstance.patch(`/info/document_folder/${folder_id}`, data, config)
}
// Get Docs
static async getDocs(folder_id: number) {
return await axiosInstance.get(`/info/documents/${folder_id}`, config)
}
// Upload Files
static async uploadFiles(folder_id: number, files: FormData, setUploadProgress?: (value: number) => void) {
return await axiosInstance.post(`/info/documents/upload/${folder_id}`, files, {
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
const percentCompleted = progressEvent.progress
setUploadProgress?.(percentCompleted || 0)
},
...config
})
}
// Download Doc
static async downloadDoc(folder_id: number, doc_id: number) {
return await axiosInstance.get(`/info/document/${folder_id}&${doc_id}`, {
responseType: 'blob',
...config
})
}
// Delete Doc
static async deleteDoc(folder_id: number, doc_id: number) {
return await axiosInstance.delete(`/info/document/`, {
params: {
folder_id: folder_id,
doc_id: doc_id
},
...config
})
}
// Convert Phones
static async convertPhones(data: FormData) {
return await axiosInstance.post(`/info/other/phones/`, data, config)
}
// Get Budget
static async getBudget() {
return await axiosInstance.get(`/info/organization/budget/`, config)
}
// Add Bank
static async addBank(data: IBank) {
return await axiosInstance.post(`/info/organization/bank`, data, config)
}
// Update Bank
static async updateBank(bank_id: string, bank_1c_id: string, data: IBank) {
return await axiosInstance.patch(`/info/organization/bank`, data, {
params: {
bank_id: bank_id,
bank_1c_id: bank_1c_id
},
...config
})
}
// Get Banks
static async getBanks(bank_id?: string, search?: string, limit?: number, offset?: number) {
return await axiosInstance.get(`/info/organization/banks`, {
params: {
bank_id: bank_id,
search: search || null,
limit: limit || 10,
offset: offset || 0
},
...config
})
}
// Get Bank
static async getBank(id_1c: string) {
return await axiosInstance.get(`/info/organization/bank/${id_1c}`, config)
}
// Delete Bank
static async deleteBank(bank_id: string, bank_1c_id: string) {
return await axiosInstance.get(`/info/organization/bank/`, {
params: {
bank_id: bank_id,
bank_1c_id: bank_1c_id
},
...config
})
}
// Add Org
static async addOrganization(data: IOrganization) {
return await axiosInstance.post(`/info/organization/org/`, data, config)
}
// Update Org
static async updateOrganization(org_id: string, org_1c_id: string, data: IOrganization) {
return await axiosInstance.patch(`/info/organization/org`, data, {
params: {
org_id: org_id,
org_1c_id: org_1c_id
},
...config
})
}
// Delete Org
static async deleteOrganization(org_id: string, org_1c_id: string) {
return await axiosInstance.delete(`/info/organization/org`, {
params: {
org_id: org_id,
org_1c_id: org_1c_id
},
...config
})
}
// Get Orgs
static async getOrganizations(org_id?: string, search?: string, limit?: number, offset?: number) {
return await axiosInstance.get(`/info/organization/orgs`, {
params: {
org_id: org_id,
search: search || null,
limit: limit || 10,
offset: offset || 0
},
...config
})
}
// Get Org
static async getOrganization(id_1c: string) {
return await axiosInstance.get(`/info/organization/org/${id_1c}`, config)
}
// Add Orgbank
static async addOrganizationBank(data: IOrganizationBank) {
return await axiosInstance.post(`/info/organization/org_bank`, data, config)
}
}

View File

@ -0,0 +1,13 @@
import { AxiosRequestConfig } from "axios";
import axiosInstance from "../http/axiosInstance";
import { BASE_URL } from "../constants";
const config: AxiosRequestConfig = {
baseURL: BASE_URL.fuel
}
export default class FuelService {
static async getAddress(limit?: number, page?: number) {
return await axiosInstance.get(`/general/address?limit=${limit || 10}&page=${page || 1}`, config)
}
}

View File

@ -0,0 +1,27 @@
import { AxiosRequestConfig } from "axios";
import axiosInstance from "../http/axiosInstance";
import { IRoleCreate } from "../interfaces/role";
import { BASE_URL } from "../constants";
const config: AxiosRequestConfig = {
baseURL: BASE_URL.auth
}
export default class RoleService {
static async getRoles() {
return await axiosInstance.get(`/auth/roles`, config)
}
static async createRole(data: IRoleCreate) {
return await axiosInstance.post(`/auth/roles/`, data, config)
}
static async getRoleById(id: number) {
return await axiosInstance.get(`/auth/roles/${id}`, config)
}
// static async deleteRole(id: number) {
// return await axiosInstance.delete(`/auth/roles/${id}`)
// }
}

View File

@ -0,0 +1,42 @@
import { AxiosRequestConfig } from "axios";
import axiosInstance from "../http/axiosInstance";
import { IHardware, IServer, IServerIP, IStorage } from "../interfaces/servers";
import { BASE_URL } from "../constants";
const config: AxiosRequestConfig = {
baseURL: BASE_URL.servers
}
export default class ServerService {
static async removeServer(server_id: number) {
return await axiosInstance.delete(`/api/server/${server_id}`, config)
}
static async addServer(data: IServer) {
return await axiosInstance.post(`/api/server/`, data, config)
}
static async removeHardware(hardware_id: number) {
return await axiosInstance.delete(`/api/hardware/${hardware_id}`, config)
}
static async addHardware(data: IHardware) {
return await axiosInstance.post(`/api/hardware`, data, config)
}
static async removeStorage(storage_id: number) {
return await axiosInstance.delete(`/api/storage/${storage_id}`, config)
}
static async addStorage(data: IStorage) {
return await axiosInstance.post(`/api/storage`, data, config)
}
static async addServerIp(data: IServerIP) {
return await axiosInstance.post(`/api/server_ip`, data, config)
}
static async removeServerIp(ip_id: number) {
return await axiosInstance.delete(`/api/server_ip/${ip_id}`, config)
}
}

View File

@ -0,0 +1,39 @@
import { AxiosRequestConfig } from "axios";
import axiosInstance from "../http/axiosInstance";
import { UserCreds, UserData } from "../interfaces/auth";
import { BASE_URL } from "../constants";
import { IUser } from "../interfaces/user";
const config: AxiosRequestConfig = {
baseURL: BASE_URL.auth
}
export default class UserService {
static async createUser(data: IUser) {
return await axiosInstance.post(`/auth/user`, data, config)
}
static async getCurrentUser(token: string) {
return await axiosInstance.get(`/auth/get_current_user/${token}`, config)
}
static async getUsers() {
return await axiosInstance.get(`/auth/user`, config)
}
// static async deleteUser(id: number) {
// return await axiosInstance.delete(`/auth/user/${id}`)
// }
static async getUser(id: number) {
return await axiosInstance.get(`/auth/user/${id}`, config)
}
static async updatePassword(data: UserCreds) {
return await axiosInstance.put(`/auth/user/password_change`, data, config)
}
static async updateUser(data: UserData) {
return await axiosInstance.put(`/auth/user`, data, config)
}
}

Some files were not shown because too many files have changed in this diff Show More