25 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
108 changed files with 2428083 additions and 5263 deletions

View File

@ -6,9 +6,6 @@ POSTGRES_DB=ems
POSTGRES_USER=ems
POSTGRES_PASSWORD=
POSTGRES_PORT=5432
CLIENT_PORT=5173
EMS_PORT=5000
MONITOR_PORT=1234
CLICKHOUSE_DB=test_db
CLICKHOUSE_USER=test_user
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
CLICKHOUSE_PASSWORD=

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()]

1
client/.gitignore vendored
View File

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

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>Dashboard</title>
<title>ИС</title>
</head>
<body>
<div id="root"></div>

2850
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,32 +12,50 @@
},
"dependencies": {
"-": "^0.0.1",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@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",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@mui/x-charts": "^7.8.0",
"@mui/x-data-grid": "^7.7.1",
"@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",
"postcss": "^8.4.38",
"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": {
@ -50,6 +68,11 @@
"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",

View File

@ -1,6 +1,14 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
'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 one or more lines are too long

View File

@ -1,165 +1,11 @@
import { BrowserRouter as Router, Route, Routes, Navigate } from "react-router-dom"
import Main from "./pages/Main"
import Users from "./pages/Users"
import Roles from "./pages/Roles"
import NotFound from "./pages/NotFound"
import DashboardLayout from "./layouts/DashboardLayout"
import MainLayout from "./layouts/MainLayout"
import SignIn from "./pages/auth/SignIn"
import ApiTest from "./pages/ApiTest"
import SignUp from "./pages/auth/SignUp"
import { initAuth, useAuthStore } from "./store/auth"
import { useEffect, useState } from "react"
import { Box, CircularProgress } from "@mui/material"
import Documents from "./pages/Documents"
import Reports from "./pages/Reports"
import Boilers from "./pages/Boilers"
import Servers from "./pages/Servers"
import { Api, Assignment, Cloud, Factory, Home, Login, Map, MonitorHeart, Password, People, Settings as SettingsIcon, Shield, Storage, Warning } from "@mui/icons-material"
import Settings from "./pages/Settings"
import PasswordReset from "./pages/auth/PasswordReset"
import MapTest from "./pages/MapTest"
import MonitorPage from "./pages/MonitorPage"
import ChunkedUpload from "./components/map/ChunkedUpload"
// Определение страниц с путями и компонентом для рендера
export const pages = [
{
label: "",
path: "/auth/signin",
icon: <Login />,
component: <SignIn />,
drawer: false,
dashboard: false,
enabled: true,
},
{
label: "",
path: "/auth/signup",
icon: <Login />,
component: <SignUp />,
drawer: false,
dashboard: false,
enabled: false,
},
{
label: "",
path: "/auth/password-reset",
icon: <Password />,
component: <PasswordReset />,
drawer: false,
dashboard: false,
enabled: true,
},
{
label: "Настройки",
path: "/settings",
icon: <SettingsIcon />,
component: <Settings />,
drawer: false,
dashboard: true,
enabled: true,
},
{
label: "Главная",
path: "/",
icon: <Home />,
component: <Main />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Пользователи",
path: "/user",
icon: <People />,
component: <Users />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Роли",
path: "/role",
icon: <Shield />,
component: <Roles />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Документы",
path: "/documents",
icon: <Storage />,
component: <Documents />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Отчеты",
path: "/reports",
icon: <Assignment />,
component: <Reports />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Серверы",
path: "/servers",
icon: <Cloud />,
component: <Servers />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "Котельные",
path: "/boilers",
icon: <Factory />,
component: <Boilers />,
drawer: true,
dashboard: true,
enabled: true,
},
{
label: "API Test",
path: "/api-test",
icon: <Api />,
component: <ApiTest />,
drawer: true,
dashboard: true,
enabled: false,
},
{
label: "ИКС",
path: "/map-test",
icon: <Map />,
component: <MapTest />,
drawer: true,
dashboard: true,
enabled: false,
},
{
label: "Chunk test",
path: "/chunk-test",
icon: <Warning />,
component: <ChunkedUpload />,
drawer: true,
dashboard: true,
enabled: false,
},
{
label: "Монитор",
path: "/monitor",
icon: <MonitorHeart />,
component: <MonitorPage />,
drawer: true,
dashboard: true,
enabled: false,
},
]
import DashboardLayout from "./layouts/DashboardLayout"
import { Box, Loader } from "@mantine/core"
import { pages } from "./constants/app"
function App() {
const auth = useAuthStore()
@ -178,14 +24,11 @@ function App() {
if (isLoading) {
return (
<CircularProgress />
<Loader />
)
} else {
return (
<Box sx={{
width: "100%",
height: "100vh"
}}>
<Box w='100%' h='100vh'>
<Router>
<Routes>
<Route element={<MainLayout />}>
@ -194,7 +37,7 @@ function App() {
))}
</Route>
<Route element={auth.isAuthenticated ? <DashboardLayout /> : <Navigate to={"/auth/signin"} />}>
<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} />
))}

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

@ -1,156 +0,0 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Avatar from '@mui/material/Avatar';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import Settings from '@mui/icons-material/Settings';
import Logout from '@mui/icons-material/Logout';
import { useNavigate } from 'react-router-dom';
import { logout } from '../store/auth';
import { ListItemText, Switch, styled } from '@mui/material';
import { setDarkMode, usePrefStore } from '../store/preferences';
const Android12Switch = styled(Switch)(({ theme }) => ({
padding: 8,
'& .MuiSwitch-track': {
borderRadius: 22 / 2,
'&::before, &::after': {
content: '""',
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
width: 16,
height: 16,
},
'&::before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
theme.palette.getContrastText(theme.palette.primary.main),
)}" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>')`,
left: 12,
},
'&::after': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
theme.palette.getContrastText(theme.palette.primary.main),
)}" d="M19,13H5V11H19V13Z" /></svg>')`,
right: 12,
},
},
'& .MuiSwitch-thumb': {
boxShadow: 'none',
width: 16,
height: 16,
margin: 2,
},
}));
export default function AccountMenu() {
const navigate = useNavigate()
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const prefStore = usePrefStore()
return (
<React.Fragment>
<Box sx={{ display: 'flex', alignItems: 'center', textAlign: 'center' }}>
<Tooltip title="Account settings">
<IconButton
onClick={handleClick}
size="small"
sx={{ ml: 2 }}
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
<Avatar sx={{ width: 32, height: 32 }}></Avatar>
</IconButton>
</Tooltip>
</Box>
<Menu
anchorEl={anchorEl}
id="account-menu"
open={open}
onClose={handleClose}
slotProps={{
paper: {
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&::before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem onClick={() => {
}}>
<ListItemIcon>
<Android12Switch
checked={prefStore.darkMode}
onChange={(e) => {
setDarkMode(e.target.checked)
}} />
</ListItemIcon>
<ListItemText>
Тема: {prefStore.darkMode ? "темная" : "светлая"}
</ListItemText>
</MenuItem>
<MenuItem onClick={() => {
navigate('/settings')
}}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
Настройки
</MenuItem>
<MenuItem
onClick={() => {
logout()
navigate("/auth/signin")
}}
>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
Выход
</MenuItem>
</Menu>
</React.Fragment>
);
}

View File

@ -1,4 +1,4 @@
import { Divider, Paper, Typography } from '@mui/material'
import { Divider, Flex, Text } from '@mantine/core';
import { PropsWithChildren } from 'react'
interface CardInfoProps extends PropsWithChildren {
@ -10,14 +10,14 @@ export default function CardInfo({
label
}: CardInfoProps) {
return (
<Paper sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
<Typography fontWeight={600}>
<Flex direction='column' gap='sm' p='sm'>
<Text fw={600}>
{label}
</Typography>
</Text>
<Divider />
{children}
</Paper>
</Flex>
)
}

View File

@ -1,4 +1,4 @@
import { Chip } from '@mui/material'
import { Chip } from '@mantine/core';
import { ReactElement } from 'react'
interface CardInfoChipProps {
@ -17,9 +17,10 @@ export default function CardInfoChip({
return (
<Chip
icon={status ? iconOn : iconOff}
variant="outlined"
label={label}
color={status ? "success" : "error"}
/>
variant='outline'
>
{label}
</Chip>
)
}

View File

@ -1,4 +1,4 @@
import { Box, Typography } from '@mui/material'
import { Flex, Text } from '@mantine/core';
interface CardInfoLabelProps {
label: string;
value: string | number;
@ -9,14 +9,14 @@ export default function CardInfoLabel({
value
}: CardInfoLabelProps) {
return (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography>
<Flex justify='space-between' align='center'>
<Text>
{label}
</Typography>
</Text>
<Typography variant="h6" fontWeight={600}>
<Text fw={600}>
{value}
</Typography>
</Box>
</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

@ -1,21 +0,0 @@
import { useState, useEffect, useMemo } from 'react'
import axiosInstance from '../http/axiosInstance'
export function useDataFetching<T>(url: string, initData: T): T {
const [data, setData] = useState<T>(initData)
useEffect(() => {
const fetchData = async () => {
const response = await axiosInstance.get(url)
const result = await response.data
setData(result)
}
fetchData()
}, [url])
// Memoize the data value
const memoizedData = useMemo<T>(() => data, [data])
return memoizedData
}
export default useDataFetching;

View File

@ -1,25 +1,17 @@
import { useDocuments, useDownload, useFolders } from '../hooks/swrHooks'
import { IDocument, IDocumentFolder } from '../interfaces/documents'
import { Box, Breadcrumbs, Button, CircularProgress, Divider, IconButton, Link, List, ListItemButton, SxProps } from '@mui/material'
import { Cancel, Close, Download, Folder, InsertDriveFile, Upload, UploadFile } from '@mui/icons-material'
import React, { useEffect, useRef, useState } from 'react'
import React, { useEffect, useState } from 'react'
import DocumentService from '../services/DocumentService'
import { mutate } from 'swr'
import FileViewer from './modals/FileViewer'
interface FolderProps {
folder: IDocumentFolder;
index: number;
handleFolderClick: (folder: IDocumentFolder) => void;
}
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;
index: number;
handleDocumentClick: (index: number) => void;
}
const FileItemStyle: SxProps = {
const FileItemStyle: MantineStyleProp = {
cursor: 'pointer',
display: 'flex',
width: '100%',
@ -29,22 +21,6 @@ const FileItemStyle: SxProps = {
padding: '8px'
}
function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) {
return (
<ListItemButton
onClick={() => handleFolderClick(folder)}
>
<Box
sx={FileItemStyle}
{...props}
>
<Folder />
{folder.name}
</Box>
</ListItemButton>
)
}
const handleSave = async (file: Blob, filename: string) => {
const link = document.createElement('a')
link.href = window.URL.createObjectURL(file)
@ -54,7 +30,7 @@ const handleSave = async (file: Blob, filename: string) => {
window.URL.revokeObjectURL(link.href)
}
function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentProps) {
function ItemDocument({ doc }: DocumentProps) {
const [shouldFetch, setShouldFetch] = useState(false)
const { file, isLoading } = useDownload(shouldFetch ? doc?.document_folder_id : null, shouldFetch ? doc?.id : null)
@ -66,35 +42,25 @@ function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentPro
setShouldFetch(false)
}
}
}, [shouldFetch, file])
}, [shouldFetch, file, doc.name])
return (
<ListItemButton>
<Box
sx={FileItemStyle}
onClick={() => handleDocumentClick(index)}
{...props}
>
<InsertDriveFile />
{doc.name}
</Box>
<Box>
<IconButton
onClick={() => {
<Flex>
<ActionIcon
onClick={(e) => {
e.stopPropagation()
if (!isLoading) {
setShouldFetch(true)
}
}}
sx={{ ml: 'auto' }}
>
variant='subtle'>
{isLoading ?
<CircularProgress size={24} variant='indeterminate' />
<Loader size='sm' />
:
<Download />
<IconDownload />
}
</IconButton>
</Box>
</ListItemButton>
</ActionIcon>
</Flex>
)
}
@ -105,7 +71,6 @@ export default function FolderViewer() {
const { documents, isLoading: documentsLoading } = useDocuments(currentFolder?.id)
const [uploadProgress, setUploadProgress] = useState(0)
const [isUploading, setIsUploading] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const [fileViewerModal, setFileViewerModal] = useState(false)
const [currentFileNo, setCurrentFileNo] = useState<number>(-1)
@ -128,12 +93,6 @@ export default function FolderViewer() {
setCurrentFolder(newBreadcrumbs[newBreadcrumbs.length - 1])
}
const handleUploadClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click()
}
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(true)
@ -150,10 +109,11 @@ export default function FolderViewer() {
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
}
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || [])
const handleFileInput = (files: File[] | null) => {
if (files !== null) {
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
}
}
const uploadFiles = async () => {
setIsUploading(true)
@ -176,17 +136,13 @@ export default function FolderViewer() {
if (foldersLoading || documentsLoading) {
return (
<CircularProgress />
<Loader />
)
}
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
p: '16px'
}}>
<ScrollAreaAutosize w={'100%'} h={'100%'} p={'sm'}>
{fileViewerModal &&
<FileViewer
open={fileViewerModal}
setOpen={setFileViewerModal}
@ -194,82 +150,53 @@ export default function FolderViewer() {
setCurrentFileNo={setCurrentFileNo}
docs={documents}
/>
}
<Stack>
<Breadcrumbs>
<Link
underline='hover'
color='inherit'
<Anchor
onClick={() => {
setCurrentFolder(null)
setBreadcrumbs([])
}}
sx={{ cursor: 'pointer' }}
>
Главная
</Link>
</Anchor>
{breadcrumbs.map((breadcrumb, index) => (
<Link
<Anchor
key={breadcrumb.id}
underline="hover"
color="inherit"
onClick={() => handleBreadcrumbClick(index)}
sx={{ cursor: 'pointer' }}
>
{breadcrumb.name}
</Link>
</Anchor>
))}
</Breadcrumbs>
{currentFolder &&
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
<Flex direction='column' gap='sm'>
<Flex direction='column' gap='sm' p='sm' style={{
border: filesToUpload.length > 0 ? '1px dashed gray' : 'none',
borderRadius: '8px',
p: '16px'
}}>
<Box sx={{ display: 'flex', gap: '16px' }}>
<Button
LinkComponent="label"
role={undefined}
variant="outlined"
tabIndex={-1}
startIcon={
isUploading ? <CircularProgress sx={{ maxHeight: "20px", maxWidth: "20px" }} variant="determinate" value={uploadProgress} /> : <UploadFile />
}
onClick={handleUploadClick}
>
<input
type='file'
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleFileInput}
onClick={(e) => {
if (e.currentTarget) {
e.currentTarget.value = ''
}
}}
/>
Добавить
</Button>
<Flex gap='sm'>
<FileButton multiple onChange={handleFileInput}>
{(props) => <Button variant='filled' leftSection={isUploading ? <Loader /> : <IconFilePlus />} {...props}>Добавить</Button>}
</FileButton>
{filesToUpload.length > 0 &&
<>
<Button
variant="contained"
color="primary"
startIcon={<Upload />}
variant='filled'
leftSection={isUploading ? <RingProgress sections={[{ value: uploadProgress, color: 'blue' }]} /> : <IconFileUpload />}
onClick={uploadFiles}
>
Загрузить все
</Button>
<Button
variant='outlined'
startIcon={<Cancel />}
variant='outline'
leftSection={<IconCancel />}
onClick={() => {
setFilesToUpload([])
}}
@ -278,67 +205,88 @@ export default function FolderViewer() {
</Button>
</>
}
</Box>
</Flex>
<Divider />
{filesToUpload.length > 0 &&
<Box>
<Flex direction='column'>
{filesToUpload.map((file, index) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
<Box>
<InsertDriveFile />
<span>{file.name}</span>
</Box>
<Flex key={index} p='8px'>
<Flex gap='sm' direction='row' align='center'>
<IconFile />
<Text>{file.name}</Text>
</Flex>
<IconButton sx={{ ml: 'auto' }} onClick={() => {
<ActionIcon onClick={() => {
setFilesToUpload(prev => {
return prev.filter((_, i) => i != index)
})
}}>
<Close />
</IconButton>
</Box>
}} ml='auto' variant='subtle'>
<IconX />
</ActionIcon>
</Flex>
))}
</Box>
</Flex>
}
</Box>
</Box>
</Flex>
</Flex>
}
<List
dense
<Table
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
sx={{
backgroundColor: dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit'
}}
>
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) => (
<div key={`${doc.id}-${doc.name}`}>
<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}
index={index}
handleDocumentClick={handleDocumentClick}
/>
{index < documents.length - 1 && <Divider />}
</div>
</Table.Td>
</Table.Tr>
))
) : (
folders?.map((folder: IDocumentFolder, index: number) => (
<div key={`${folder.id}-${folder.name}`}>
<ItemFolder
folder={folder}
index={index}
handleFolderClick={handleFolderClick}
/>
{index < folders.length - 1 && <Divider />}
</div>
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>
))
)}
</List>
</Box>
</Table.Tbody>
</Table>
</Stack>
</ScrollAreaAutosize>
)
}

View File

@ -1,7 +1,7 @@
import { SubmitHandler, useForm } from 'react-hook-form'
import { CreateField } from '../interfaces/create'
import { Box, Button, CircularProgress, Stack, SxProps, TextField, Typography } from '@mui/material';
import { AxiosResponse } from 'axios';
import { Button, Loader, Stack, Text, TextInput } from '@mantine/core';
interface Props {
title?: string;
@ -11,7 +11,6 @@ interface Props {
mutateHandler?: any;
defaultValues?: {};
watchValues?: string[];
sx?: SxProps | null;
}
function FormFields({
@ -20,8 +19,7 @@ function FormFields({
fields,
submitButtonText = 'Сохранить',
mutateHandler,
defaultValues,
sx
defaultValues
}: Props) {
const getDefaultValues = (fields: CreateField[]) => {
let result: { [key: string]: string | boolean } = {}
@ -53,20 +51,20 @@ function FormFields({
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Stack sx={sx} spacing={2} width='100%'>
<Typography variant="h6" component="h6" gutterBottom>
<Stack gap='sm' w='100%'>
{title.length > 0 &&
<Text size="xl" fw={500}>
{title}
</Typography>
</Text>
}
{fields.map((field: CreateField) => {
return (
<TextField
fullWidth
margin='normal'
<TextInput
key={field.key}
type={field.inputType ? field.inputType : 'text'}
label={field.headerName || field.key.charAt(0).toUpperCase() + field.key.slice(1)}
required={field.required || false}
//placeholder="Your name"
type={field.inputType ? field.inputType : 'text'}
{...register(field.key, {
required: field.required ? `${field.headerName} обязателен` : false,
validate: (val: string | boolean) => {
@ -77,21 +75,17 @@ function FormFields({
}
},
})}
error={!!errors[field.key]}
helperText={errors[field.key]?.message}
radius="md"
required={field.required || false}
error={errors[field.key]?.message}
errorProps={errors[field.key]}
/>
)
})}
<Box sx={{
display: "flex",
justifyContent: "space-between",
gap: "8px"
}}>
<Button disabled={isSubmitting || Object.keys(dirtyFields).length === 0 || !isValid} type="submit" variant="contained" color="primary">
{isSubmitting ? <CircularProgress size={16} /> : submitButtonText}
<Button disabled={isSubmitting || Object.keys(dirtyFields).length === 0 || !isValid} type='submit'>
{isSubmitting ? <Loader size={16} /> : submitButtonText}
</Button>
</Box>
</Stack>
</form>
)

View File

@ -1,13 +1,11 @@
import { Box } from '@mui/material'
import { IServer } from '../interfaces/servers'
import { useServerIps } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import { GridColDef } from '@mui/x-data-grid'
import { Flex, Table } from '@mantine/core'
function ServerData({ id }: IServer) {
const { serverIps } = useServerIps(id, 0, 10)
const serverIpsColumns: GridColDef[] = [
const serverIpsColumns = [
{ field: 'id', headerName: 'ID', type: 'number' },
{ field: 'server_id', headerName: 'Server ID', type: 'number' },
{ field: 'name', headerName: 'Название', type: 'string' },
@ -17,22 +15,26 @@ function ServerData({ id }: IServer) {
]
return (
<Box sx={{ display: 'flex', flexDirection: 'column', p: '16px' }}>
<Flex direction='column' p='sm'>
{serverIps &&
<FullFeaturedCrudGrid
initialRows={serverIps}
columns={serverIpsColumns}
actions
onRowClick={() => {
//setCurrentServerData(params.row)
//setServerDataOpen(true)
}}
onSave={undefined}
onDelete={undefined}
loading={false}
/>
<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>
}
</Box>
</Flex>
)
}

View File

@ -1,32 +1,15 @@
import { AppBar, Autocomplete, CircularProgress, Dialog, IconButton, TextField, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react'
import { IRegion } from '../interfaces/fuel'
import { useState } from 'react'
import { useHardwares, useServers } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService'
import { GridColDef } from '@mui/x-data-grid'
import { Close } from '@mui/icons-material'
import ServerData from './ServerData'
import { Autocomplete, CloseButton, Loader, Table } from '@mantine/core'
import { IServer } from '../interfaces/servers'
export default function ServerHardware() {
const [open, setOpen] = useState(false)
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
const { servers, isLoading } = useServers()
const [selectedOption, setSelectedOption] = useState<number | undefined>(undefined)
const { servers } = useServers()
const [serverDataOpen, setServerDataOpen] = useState(false)
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
const { hardwares, isLoading: serversLoading } = useHardwares(selectedOption, 0, 10)
const handleInputChange = (value: string) => {
return value
}
const handleOptionChange = (value: IRegion | null) => {
setSelectedOption(value)
}
const { hardwares, isLoading: serversLoading } = useHardwares(selectedOption?.id, 0, 10)
const hardwareColumns: GridColDef[] = [
const hardwareColumns = [
{ field: 'id', headerName: 'ID', type: 'number' },
{ field: 'name', headerName: 'Название', type: 'string' },
{ field: 'server_id', headerName: 'Server ID', type: 'number' },
@ -39,86 +22,51 @@ export default function ServerHardware() {
return (
<>
<Dialog
fullScreen
open={serverDataOpen}
onClose={() => {
setServerDataOpen(false)
}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description">
<AppBar sx={{ position: 'sticky' }}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
<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={() => {
setServerDataOpen(false)
//setSearch('')
setSelectedOption(undefined)
}}
aria-label="close"
>
<Close />
</IconButton>
</Toolbar>
</AppBar>
{currentServerData &&
<ServerData
id={currentServerData?.id}
region_id={currentServerData?.region_id}
name={currentServerData?.name}
aria-label="Clear value"
/>
)
}
</Dialog>
//value={search}
/>
</form>
{serversLoading ?
<CircularProgress />
<Loader />
:
<FullFeaturedCrudGrid
autoComplete={
<Autocomplete
open={open}
onOpen={() => {
setOpen(true)
}}
onClose={() => {
setOpen(false)
}}
onInputChange={(_, value) => handleInputChange(value)}
onChange={(_, value) => handleOptionChange(value)}
filterOptions={(x) => x}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={servers || []}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
label="Сервер"
size='small'
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}} />
)} />}
onSave={() => {
}}
onDelete={ServerService.removeServer}
initialRows={hardwares || []}
columns={hardwareColumns}
actions
onRowClick={(params) => {
setCurrentServerData(params.row)
setServerDataOpen(true)
}}
loading={false}
/>
<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

@ -1,32 +1,15 @@
import { AppBar, Autocomplete, CircularProgress, Dialog, IconButton, TextField, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react'
import { IRegion } from '../interfaces/fuel'
import { useEffect, useState } from 'react'
import { useServerIps, useServers } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService'
import { GridColDef } from '@mui/x-data-grid'
import { Close } from '@mui/icons-material'
import ServerData from './ServerData'
import { Autocomplete, CloseButton, Loader, Table } from '@mantine/core'
import { IServer } from '../interfaces/servers'
export default function ServerIpsView() {
const [open, setOpen] = useState(false)
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
const { servers, isLoading } = useServers()
const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { servers } = useServers()
const [serverDataOpen, setServerDataOpen] = useState(false)
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
const { serverIps, isLoading: serversLoading } = useServerIps(selectedOption, 0, 10)
const handleInputChange = (value: string) => {
return value
}
const handleOptionChange = (value: IRegion | null) => {
setSelectedOption(value)
}
const { serverIps, isLoading: serversLoading } = useServerIps(selectedOption?.id, 0, 10)
const serverIpsColumns: GridColDef[] = [
const serverIpsColumns = [
{ field: 'id', headerName: 'ID', type: 'number' },
{ field: 'server_id', headerName: 'Server ID', type: 'number' },
{ field: 'name', headerName: 'Название', type: 'string' },
@ -35,86 +18,59 @@ export default function ServerIpsView() {
{ field: 'servername', headerName: 'Название сервера', type: 'string' },
]
useEffect(() => {
console.log(serverIps)
}, [serverIps])
return (
<>
<Dialog
fullScreen
open={serverDataOpen}
onClose={() => {
setServerDataOpen(false)
}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description">
<AppBar sx={{ position: 'sticky' }}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
<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={() => {
setServerDataOpen(false)
//setSearch('')
setSelectedOption(null)
}}
aria-label="close"
>
<Close />
</IconButton>
</Toolbar>
</AppBar>
{currentServerData &&
<ServerData
id={currentServerData?.id}
region_id={currentServerData?.region_id}
name={currentServerData?.name}
aria-label="Clear value"
/>
)
}
</Dialog>
//value={search}
/>
</form>
{serversLoading ?
<CircularProgress />
<Loader />
:
<FullFeaturedCrudGrid
autoComplete={
<Autocomplete
open={open}
onOpen={() => {
setOpen(true)
}}
onClose={() => {
setOpen(false)
}}
onInputChange={(_, value) => handleInputChange(value)}
onChange={(_, value) => handleOptionChange(value)}
filterOptions={(x) => x}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={servers || []}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
size='small'
label="Сервер"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}} />
)} />}
onSave={() => {
}}
onDelete={ServerService.removeServer}
initialRows={serverIps || []}
columns={serverIpsColumns}
actions
onRowClick={(params) => {
setCurrentServerData(params.row)
setServerDataOpen(true)
}} loading={false} />
<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

@ -1,32 +1,14 @@
import { AppBar, Autocomplete, CircularProgress, Dialog, IconButton, TextField, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react'
import { useState } from 'react'
import { IRegion } from '../interfaces/fuel'
import { useHardwares, useStorages } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService'
import { GridColDef } from '@mui/x-data-grid'
import { Close } from '@mui/icons-material'
import ServerData from './ServerData'
import { useStorages } from '../hooks/swrHooks'
import { Loader, Table } from '@mantine/core'
export default function ServerStorage() {
const [open, setOpen] = useState(false)
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
const { hardwares, isLoading } = useHardwares()
const [serverDataOpen, setServerDataOpen] = useState(false)
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
const handleInputChange = (value: string) => {
return value
}
const handleOptionChange = (value: IRegion | null) => {
setSelectedOption(value)
}
const [selectedOption] = useState<IRegion | null>(null)
const { storages, isLoading: serversLoading } = useStorages(selectedOption?.id, 0, 10)
const storageColumns: GridColDef[] = [
const storageColumns = [
{ field: 'id', headerName: 'ID', type: 'number' },
{ field: 'hardware_id', headerName: 'Hardware ID', type: 'number' },
{ field: 'name', headerName: 'Название', type: 'string' },
@ -36,86 +18,25 @@ export default function ServerStorage() {
return (
<>
<Dialog
fullScreen
open={serverDataOpen}
onClose={() => {
setServerDataOpen(false)
}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description">
<AppBar sx={{ position: 'sticky' }}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={() => {
setServerDataOpen(false)
}}
aria-label="close"
>
<Close />
</IconButton>
</Toolbar>
</AppBar>
{currentServerData &&
<ServerData
id={currentServerData?.id}
region_id={currentServerData?.region_id}
name={currentServerData?.name}
/>
}
</Dialog>
{serversLoading ?
<CircularProgress />
<Loader />
:
<FullFeaturedCrudGrid
autoComplete={
<Autocomplete
open={open}
onOpen={() => {
setOpen(true)
}}
onClose={() => {
setOpen(false)
}}
onInputChange={(_, value) => handleInputChange(value)}
onChange={(_, value) => handleOptionChange(value)}
filterOptions={(x) => x}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={hardwares || []}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
size='small'
label="Hardware"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}} />
)} />}
onSave={() => {
}}
onDelete={ServerService.removeServer}
initialRows={storages || []}
columns={storageColumns}
actions
onRowClick={(params) => {
setCurrentServerData(params.row)
setServerDataOpen(true)
}}
loading={false}
/>
<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

@ -1,35 +1,21 @@
import { AppBar, Autocomplete, Box, CircularProgress, Dialog, Grid, IconButton, TextField, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react'
import { useState } from 'react'
import { IRegion } from '../interfaces/fuel'
import { useRegions, useServers, useServersInfo } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService'
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'
import { Close, Cloud, CloudOff } from '@mui/icons-material'
import ServerData from './ServerData'
import { IServersInfo } from '../interfaces/servers'
import CardInfo from './CardInfo/CardInfo'
import CardInfoLabel from './CardInfo/CardInfoLabel'
import CardInfoChip from './CardInfo/CardInfoChip'
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 | null>("")
const [search, setSearch] = useState<string | undefined>("")
const debouncedSearch = useDebounce(search, 500)
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { regions, isLoading } = useRegions(10, 1, debouncedSearch)
const { regions } = useRegions(10, 1, debouncedSearch)
const { serversInfo } = useServersInfo(selectedOption?.id)
const { servers } = useServers(selectedOption, 0, 10)
const [serverDataOpen, setServerDataOpen] = useState(false)
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
const { servers, isLoading: serversLoading } = useServers(selectedOption?.id, 0, 10)
const serversColumns: GridColDef[] = [
const serversColumns = [
//{ field: 'id', headerName: 'ID', type: "number" },
{
field: 'name', headerName: 'Название', type: "string", editable: true,
@ -37,141 +23,55 @@ export default function ServersView() {
{
field: 'region_id',
editable: true,
renderCell: (params) => (
<div>
{params.value}
</div>
),
renderEditCell: (params: GridRenderCellParams) => (
<Autocomplete
sx={{ display: 'flex', flexGrow: '1' }}
onInputChange={(_, value) => setSearch(value)}
onChange={(_, value) => {
params.value = value
}}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={regions || []}
loading={isLoading}
value={params.value}
renderInput={(params) => (
<TextField
{...params}
size='small'
variant='standard'
label="Район"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
/>
),
headerName: 'region_id',
flex: 1
}
]
return (
<>
<Dialog
fullScreen
open={serverDataOpen}
onClose={() => {
setServerDataOpen(false)
}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description">
<AppBar sx={{ position: 'sticky' }}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={() => {
setServerDataOpen(false)
}}
aria-label="close"
>
<Close />
</IconButton>
</Toolbar>
</AppBar>
{currentServerData &&
<ServerData
id={currentServerData?.id}
region_id={currentServerData?.region_id}
name={currentServerData?.name}
/>
}
</Dialog>
{serversInfo &&
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
<Grid container spacing={{ xs: 2, md: 3 }} columns={{ xs: 1, sm: 1, md: 2, lg: 3, xl: 4 }}>
{serversInfo.map((serverInfo: IServersInfo) => (
<Grid key={`si-${serverInfo.id}`} item xs={1} sm={1} md={1}>
<CardInfo label={serverInfo.name}>
<CardInfoLabel label='Количество IP' value={serverInfo.IPs_count} />
<CardInfoLabel label='Количество серверов' value={serverInfo.servers_count} />
<CardInfoChip
status={serverInfo.status === "Online"}
label={serverInfo.status}
iconOn={<Cloud />}
iconOff={<CloudOff />}
/>
</CardInfo>
</Grid>
))}
</Grid>
</Box>
}
<FullFeaturedCrudGrid
loading={serversLoading}
autoComplete={
<form>
<Autocomplete
onInputChange={(_, value) => setSearch(value)}
onChange={(_, value) => setSelectedOption(value)}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.id === value.id}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={regions || []}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
size='small'
label="Район"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
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"
/>
)
}}
/>
)}
/>
}
onSave={() => {
}}
onDelete={ServerService.removeServer}
initialRows={servers}
columns={serversColumns}
actions
onRowClick={(params) => {
setCurrentServerData(params.row)
setServerDataOpen(true)
}}
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

@ -1,233 +0,0 @@
import { useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Close';
import {
GridRowsProp,
GridRowModesModel,
GridRowModes,
DataGrid,
GridColDef,
GridToolbarContainer,
GridActionsCellItem,
GridEventListener,
GridRowId,
GridRowModel,
GridRowEditStopReasons,
GridSlots,
} from '@mui/x-data-grid';
interface EditToolbarProps {
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void
setRowModesModel: (
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void
columns: GridColDef[]
autoComplete?: React.ReactElement | null
}
function EditToolbar(props: EditToolbarProps) {
const { setRows, setRowModesModel, columns, autoComplete } = props
const handleClick = () => {
const id = Date.now().toString(36)
const newValues: any = {}
columns.forEach(column => {
if (column.type === 'number') {
newValues[column.field] = 0
} else if (column.type === 'string') {
newValues[column.field] = ''
} else if (column.type === 'boolean') {
newValues[column.field] = false
} else {
newValues[column.field] = undefined
}
if (column.field === 'region_id') {
// column.valueGetter = (value: any) => {
// console.log(value)
// }
}
})
setRows((oldRows) => [...oldRows, { id, ...newValues, isNew: true }]);
setRowModesModel((oldModel) => ({
...oldModel,
[id]: { mode: GridRowModes.Edit, fieldToFocus: columns[0].field },
}))
};
return (
<GridToolbarContainer sx={{ px: '16px', py: '16px' }}>
{autoComplete &&
<Box sx={{ flexGrow: '1' }}>
{autoComplete}
</Box>
}
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
Добавить
</Button>
</GridToolbarContainer>
);
}
interface DataGridProps {
initialRows: GridRowsProp;
columns: GridColDef[];
actions: boolean;
onRowClick: GridEventListener<"rowClick">;
onSave: any;
onDelete: any;
autoComplete?: React.ReactElement | null;
loading: boolean;
}
export default function FullFeaturedCrudGrid({
initialRows,
columns,
actions = false,
//onRowClick,
onSave,
onDelete,
autoComplete,
loading
}: DataGridProps) {
const [rows, setRows] = useState(initialRows);
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => {
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
event.defaultMuiPrevented = true;
}
};
const handleEditClick = (id: GridRowId) => () => {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
};
const handleSaveClick = (id: GridRowId) => () => {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
onSave?.(id)
};
const handleDeleteClick = (id: GridRowId) => () => {
setRows(rows.filter((row) => row.id !== id));
onDelete?.(id)
};
const handleCancelClick = (id: GridRowId) => () => {
setRowModesModel({
...rowModesModel,
[id]: { mode: GridRowModes.View, ignoreModifications: true },
});
const editedRow = rows.find((row) => row.id === id);
if (editedRow!.isNew) {
setRows(rows.filter((row) => row.id !== id));
}
};
const processRowUpdate = (newRow: GridRowModel) => {
const updatedRow = { ...newRow, isNew: false };
setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
return updatedRow;
};
const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
setRowModesModel(newRowModesModel);
};
useEffect(() => {
if (initialRows) {
setRows(initialRows)
}
}, [initialRows])
const actionColumns: GridColDef[] = [
{
field: 'actions',
type: 'actions',
headerName: 'Действия',
width: 100,
cellClassName: 'actions',
getActions: ({ id }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<SaveIcon />}
label="Save"
sx={{
color: 'primary.main',
}}
onClick={handleSaveClick(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
className="textPrimary"
onClick={handleCancelClick(id)}
color="inherit"
/>,
];
}
return [
<GridActionsCellItem
icon={<EditIcon />}
label="Edit"
className="textPrimary"
onClick={handleEditClick(id)}
color="inherit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
onClick={handleDeleteClick(id)}
color="inherit"
/>,
];
},
}
]
return (
<Box
sx={{
height: 500,
width: '100%',
'& .actions': {
color: 'text.secondary',
},
'& .textPrimary': {
color: 'text.primary',
},
}}
>
<DataGrid
loading={loading}
rows={rows || []}
columns={actions ? [...columns, ...actionColumns] : columns}
editMode="row"
rowModesModel={rowModesModel}
//onRowClick={onRowClick}
onRowModesModelChange={handleRowModesModelChange}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
slots={{
toolbar: EditToolbar as GridSlots['toolbar'],
}}
slotProps={{
toolbar: { setRows, setRowModesModel, columns, autoComplete },
}}
/>
</Box>
);
}

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

View File

@ -1,18 +0,0 @@
import { useEffect, useMemo, useState } from "react";
import UserService from "../services/UserService";
export default function useUserData<T>(token: string, initData: T): T {
const [userData, setUserData] = useState<T>(initData)
useEffect(()=> {
const fetchUserData = async (token: string) => {
const response = await UserService.getCurrentUser(token)
setUserData(response.data)
}
fetchUserData(token)
}, [token])
const memoizedData = useMemo<T>(() => userData, [userData])
return memoizedData
}

View File

@ -1,58 +0,0 @@
import React, { useState } from 'react';
import axios from 'axios';
const ChunkedUpload = () => {
const [file, setFile] = useState<File | null>(null);
const [uploadProgress, setUploadProgress] = useState<number>(0);
// Handle file selection
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
setFile(event.target.files[0]);
}
};
// Upload the file in chunks
const uploadFile = async () => {
if (!file) return;
const chunkSize = 1024 * 1024; // 1MB per chunk
const totalChunks = Math.ceil(file.size / chunkSize);
const fileId = `${file.name}-${Date.now()}`; // Unique file identifier
let uploadedChunks = 0;
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize);
const chunkNumber = Math.ceil(start / chunkSize) + 1;
try {
await axios.post(`${import.meta.env.VITE_API_EMS_URL}/upload`, chunk, {
headers: {
'Content-Type': 'application/octet-stream',
'X-Chunk-Number': chunkNumber.toString(),
'X-Total-Chunks': totalChunks.toString(),
'X-File-Id': fileId,
},
});
uploadedChunks++;
setUploadProgress((uploadedChunks / totalChunks) * 100);
} catch (error) {
console.error('Chunk upload failed', error);
// Implement retry logic if needed
break;
}
}
};
return (
<div>
<input type="file" onChange={handleFileChange} />
<button onClick={uploadFile} disabled={!file}>
Upload File
</button>
<div>Upload Progress: {uploadProgress.toFixed(2)}%</div>
</div>
);
};
export default ChunkedUpload;

File diff suppressed because it is too large Load Diff

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

@ -10,16 +10,21 @@ 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}/tile/google/{z}/{x}/{y}`,
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}/tile/yandex/{z}/{x}/{y}`,
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(),
@ -28,5 +33,6 @@ const regionsLayerSource = new VectorSource({
export {
googleMapsSatelliteSource,
yandexMapsSatelliteSource,
regionsLayerSource
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

@ -1,7 +1,131 @@
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)',
@ -35,5 +159,7 @@ const regionsLayerStyle = new Style({
export {
drawingLayerStyle,
selectStyle,
regionsLayerStyle
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

@ -1,11 +1,396 @@
import { Coordinate, distance, rotate } from "ol/coordinate";
import { Extent, getCenter } from "ol/extent";
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) {
var coord = rotate(
const coord = rotate(
[coordinate[0] - anchor[0], coordinate[1] - anchor[1]],
angle
);
@ -20,10 +405,10 @@ function rotateProjection(projection: ProjectionLike, angle: number, extent: Ext
return rotateCoordinate(coordinate, -angle, getCenter(extent));
}
var normalProjection = get(projection);
const normalProjection = get(projection);
if (normalProjection) {
var rotatedProjection = new Projection({
const rotatedProjection = new Projection({
code: normalProjection.getCode() + ":" + angle.toString() + ":" + extent.toString(),
units: normalProjection.getUnits(),
extent: extent
@ -54,9 +439,9 @@ function rotateProjection(projection: ProjectionLike, angle: number, extent: Ext
// also set up transforms with any projections defined using proj4
if (typeof proj4 !== "undefined") {
var projCodes = Object.keys(proj4.defs);
const projCodes = Object.keys(proj4.defs);
projCodes.forEach(function (code) {
var proj4Projection = get(code) as Projection;
const proj4Projection = get(code) as Projection;
if (proj4Projection) {
if (!getTransform(proj4Projection, rotatedProjection)) {
addCoordinateTransforms(
@ -119,9 +504,87 @@ function calculateExtent(bottomLeft: Coordinate, topLeft: Coordinate, topRight:
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
calculateCentroid,
getTilesPerSide,
normalize,
getTileIndex,
getGridCellPosition,
calculateCenter
}

View File

@ -1,6 +1,4 @@
import { useEffect, useRef } from 'react'
import { AppBar, Box, Button, CircularProgress, Dialog, IconButton, Toolbar, Typography } from '@mui/material';
import { ChevronLeft, ChevronRight, Close, Warning } from '@mui/icons-material';
import { useDownload, useFileType } from '../../hooks/swrHooks';
import jsPreviewExcel from "@js-preview/excel"
@ -11,6 +9,8 @@ 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;
@ -45,9 +45,9 @@ function PdfViewer({
}, [previewContainerRef])
return (
<Box ref={previewContainerRef} sx={{
<div ref={previewContainerRef} style={{
width: '100%',
height: '100%'
height: '100%',
}} />
)
}
@ -75,9 +75,9 @@ function DocxViewer({
}, [])
return (
<Box ref={previewContainerRef} sx={{
<div ref={previewContainerRef} style={{
width: '100%',
height: '100%'
height: '100%',
}} />
)
}
@ -101,9 +101,9 @@ function ExcelViewer({
}, [])
return (
<Box ref={previewContainerRef} sx={{
<div ref={previewContainerRef} style={{
width: '100%',
height: '100%'
height: '100%',
}} />
)
}
@ -112,7 +112,7 @@ function ImageViewer({
url
}: ViewerProps) {
return (
<Box sx={{
<Flex style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@ -125,7 +125,7 @@ function ImageViewer({
maxWidth: '100%',
maxHeight: '100%'
}} />
</Box>
</Flex>
)
}
@ -151,37 +151,23 @@ export default function FileViewer({
}
return (
<Dialog
fullScreen
open={open}
onClose={() => {
setOpen(false)
setCurrentFileNo(-1)
}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<AppBar sx={{ position: 'sticky' }}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={() => {
setOpen(false)
setCurrentFileNo(-1)
}}
aria-label="close"
>
<Close />
</IconButton>
<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>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
{currentFileNo != -1 && docs[currentFileNo].name}
</Typography>
<div>
<IconButton
color='inherit'
<Grid>
<Grid.Col span='auto'>
<Button
variant='transparent'
onClick={() => {
if (currentFileNo >= 0 && currentFileNo > 0) {
setCurrentFileNo(currentFileNo - 1)
@ -189,11 +175,13 @@ export default function FileViewer({
}}
disabled={currentFileNo >= 0 && currentFileNo === 0}
>
<ChevronLeft />
</IconButton>
<IconChevronLeft />
</Button>
</Grid.Col>
<IconButton
color='inherit'
<Grid.Col span='auto'>
<Button
variant='transparent'
onClick={() => {
if (currentFileNo >= 0 && currentFileNo < docs.length) {
setCurrentFileNo(currentFileNo + 1)
@ -201,34 +189,33 @@ export default function FileViewer({
}}
disabled={currentFileNo >= 0 && currentFileNo >= docs.length - 1}
>
<ChevronRight />
</IconButton>
</div>
<IconChevronRight />
</Button>
</Grid.Col>
</Grid>
<Button
autoFocus
color="inherit"
variant='subtle'
onClick={handleSave}
>
Сохранить
</Button>
</Toolbar>
</AppBar>
<Box sx={{
flexGrow: '1',
overflowY: 'hidden'
}}>
</Flex>
</Modal.Title>
<Modal.CloseButton ml='xl' />
</Modal.Header>
<Modal.Body style={{ display: 'flex', flexGrow: 1, height: '100%', width: '100vw' }}>
{fileIsLoading || fileTypeIsLoading ?
<Box sx={{
<Flex style={{
display: 'flex',
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center'
}}>
<CircularProgress />
</Box>
<Loader />
</Flex>
:
fileType === 'application/pdf' ?
<PdfViewer url={window.URL.createObjectURL(file)} />
@ -243,26 +230,27 @@ export default function FileViewer({
<ImageViewer url={window.URL.createObjectURL(file)} />
:
fileType && file ?
<Box sx={{ display: 'flex', gap: '16px', flexDirection: 'column', p: '16px' }}>
<Box sx={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<Warning />
<Typography>
<Flex style={{ display: 'flex', gap: '16px', flexDirection: 'column', p: '16px' }}>
<Flex style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<IconAlertTriangle />
<Text>
Предпросмотр данного файла невозможен.
</Typography>
</Box>
</Text>
</Flex>
<Box>
<Flex>
<Button variant='contained' onClick={() => {
handleSave()
}}>
Сохранить
</Button>
</Box>
</Box>
</Flex>
</Flex>
:
null
}
</Box>
</Dialog>
</Modal.Body>
</Modal.Content>
</Modal.Root>
)
}

View File

@ -1,171 +0,0 @@
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import InboxIcon from '@mui/icons-material/MoveToInbox';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MailIcon from '@mui/icons-material/Mail';
import MenuIcon from '@mui/icons-material/Menu';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
const drawerWidth = 240;
export default function ResponsiveDrawer() {
//const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false);
const handleDrawerClose = () => {
setIsClosing(true);
setMobileOpen(false);
};
const handleDrawerTransitionEnd = () => {
setIsClosing(false);
};
const handleDrawerToggle = () => {
if (!isClosing) {
setMobileOpen(!mobileOpen);
}
};
const drawer = (
<div>
<Toolbar />
<Divider />
<List>
{['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List>
{['All mail', 'Trash', 'Spam'].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
</div>
);
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
Dashboard
</Typography>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
aria-label="mailbox folders"
>
{/* The implementation can be swapped with js to avoid SEO duplication of links. */}
<Drawer
variant="temporary"
open={mobileOpen}
onTransitionEnd={handleDrawerTransitionEnd}
onClose={handleDrawerClose}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
display: { xs: 'block', sm: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawer}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', sm: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawer}
</Drawer>
</Box>
<Box
component="main"
sx={{ flexGrow: 1, p: 3, width: { sm: `calc(100% - ${drawerWidth}px)` } }}
>
<Toolbar />
<Typography paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non
enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus
imperdiet. Semper risus in hendrerit gravida rutrum quisque non tellus.
Convallis convallis tellus id interdum velit laoreet id donec ultrices.
Odio morbi quis commodo odio aenean sed adipiscing. Amet nisl suscipit
adipiscing bibendum est ultricies integer quis. Cursus euismod quis viverra
nibh cras. Metus vulputate eu scelerisque felis imperdiet proin fermentum
leo. Mauris commodo quis imperdiet massa tincidunt. Cras tincidunt lobortis
feugiat vivamus at augue. At augue eget arcu dictum varius duis at
consectetur lorem. Velit sed ullamcorper morbi tincidunt. Lorem donec massa
sapien faucibus et molestie ac.
</Typography>
<Typography paragraph>
Consequat mauris nunc congue nisi vitae suscipit. Fringilla est ullamcorper
eget nulla facilisi etiam dignissim diam. Pulvinar elementum integer enim
neque volutpat ac tincidunt. Ornare suspendisse sed nisi lacus sed viverra
tellus. Purus sit amet volutpat consequat mauris. Elementum eu facilisis
sed odio morbi. Euismod lacinia at quis risus sed vulputate odio. Morbi
tincidunt ornare massa eget egestas purus viverra accumsan in. In hendrerit
gravida rutrum quisque non tellus orci ac. Pellentesque nec nam aliquam sem
et tortor. Habitant morbi tristique senectus et. Adipiscing elit duis
tristique sollicitudin nibh sit. Ornare aenean euismod elementum nisi quis
eleifend. Commodo viverra maecenas accumsan lacus vel facilisis. Nulla
posuere sollicitudin aliquam ultrices sagittis orci a.
</Typography>
</Box>
</Box>
);
}

View File

@ -1,30 +0,0 @@
import { Tab, Tabs } from "@mui/material"
import { Link, matchPath, useLocation } from "react-router-dom"
function useRouteMatch(patterns: readonly string[]) {
const { pathname } = useLocation()
for (let i = 0; i < patterns.length; i += 1) {
const pattern = patterns[i]
const possibleMatch = matchPath(pattern, pathname)
if (possibleMatch !== null) {
return possibleMatch
}
}
return null
}
export default function NavTabs() {
const routeMatch = useRouteMatch(['/', '/user', '/role']);
const currentTab = routeMatch?.pattern?.path;
return (
<Tabs value={currentTab}>
<Tab label="Главная" value="/" to="/" component={Link} />
<Tab label="Пользователи" value="/user" to="/user" component={Link} />
<Tab label="Роли" value="/role" to="/role" component={Link} />
</Tabs>
);
}

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

@ -198,7 +198,7 @@ export function useServers(region_id?: number | null, offset?: number, limit?: n
}
}
export function useServersInfo(region_id?: number, offset?: number, limit?: number) {
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),

View File

@ -1,22 +1,7 @@
import { Validate } from "react-hook-form";
export interface CreateFieldTypes {
string: 'string';
number: 'number';
date: 'date';
dateTime: 'dateTime';
boolean: 'boolean';
singleSelect: 'singleSelect';
actions: 'actions';
custom: 'custom';
}
export interface InputTypes {
password: 'password';
}
export type CreateFieldType = CreateFieldTypes[keyof CreateFieldTypes]
export type InputType = InputTypes[keyof InputTypes]
export type CreateFieldType = 'string' | 'number' | 'date' | 'dateTime' | 'boolean' | 'singleSelect' | 'actions' | 'custom'
export type InputType = 'password'
export interface CreateField {
key: string;

View File

@ -9,7 +9,6 @@ export interface ICity {
}
export interface IBoiler {
id: string;
id_object: string;
boiler_name: string;
boiler_code: string;

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

@ -1,6 +1,12 @@
export interface SatelliteMapsProviders {
google: 'google';
yandex: 'yandex';
custom: 'custom';
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
}
export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders]

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

@ -7,4 +7,5 @@ export interface IUser {
name: string;
surname: string;
is_active: boolean;
role_id: number;
}

View File

@ -1,96 +1,28 @@
import * as React from 'react';
import { styled, createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import MuiDrawer from '@mui/material/Drawer';
import Box from '@mui/material/Box';
import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import List from '@mui/material/List';
import Typography from '@mui/material/Typography';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import { colors, ListItem, ListItemButton, ListItemIcon, ListItemText, } from '@mui/material';
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 { getUserData, useAuthStore } from '../store/auth';
import { useTheme } from '@emotion/react';
import AccountMenu from '../components/AccountMenu';
import { pages } from '../App';
const drawerWidth: number = 240;
interface AppBarProps extends MuiAppBarProps {
open?: boolean;
}
const AppBar = styled(MuiAppBar, {
shouldForwardProp: (prop) => prop !== 'open',
})<AppBarProps>(({ theme, open }) => ({
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
...(open && {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}));
const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(
({ theme, open }) => ({
'& .MuiDrawer-paper': {
position: 'relative',
whiteSpace: 'nowrap',
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
boxSizing: 'border-box',
...(!open && {
overflowX: 'hidden',
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up('sm')]: {
//width: theme.spacing(9),
},
}),
},
}),
);
export default function DashboardLayout() {
const theme = useTheme()
const innerTheme = createTheme(theme)
const [open, setOpen] = React.useState(true);
const toggleDrawer = () => {
setOpen(!open);
};
const authStore = useAuthStore();
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 : "Dashboard";
};
const currentPath = location.pathname
const allPages = [...pages]
const currentPage = allPages.find(page => page.path === currentPath)
return currentPage ? currentPage.label : "Панель управления"
}
const [userData, setUserData] = React.useState<UserData>();
const authStore = useAuthStore()
const [userData, setUserData] = useState<UserData>()
React.useEffect(() => {
useEffect(() => {
if (authStore) {
const stored = getUserData()
if (stored) {
@ -99,113 +31,111 @@ export default function DashboardLayout() {
}
}, [authStore])
const { colorScheme, setColorScheme } = useMantineColorScheme();
return (
<ThemeProvider theme={innerTheme}>
<Box sx={{
display: 'flex',
height: "100%"
}}>
<CssBaseline />
<AppBar position="absolute" open={open}>
<Toolbar
sx={{
pr: '24px', // keep right padding when drawer closed
<AppShell
header={{ height: 60 }}
navbar={{
width: desktopOpened ? 200 : 50,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened },
}}
>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
onClick={toggleDrawer}
sx={{
marginRight: '36px',
//...(open && { display: 'none' }),
}}
>
<MenuIcon />
</IconButton>
<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>
<Typography
component="h1"
variant="h6"
color="inherit"
noWrap
sx={{ flexGrow: 1 }}
>
<Group w='auto'>
{getPageTitle()}
</Typography>
</Group>
<Box sx={{ display: "flex", gap: "8px" }}>
<Box>
<Typography>{userData?.name} {userData?.surname}</Typography>
<Divider />
<Typography variant="caption">{userData?.login}</Typography>
</Box>
<Group id='header-portal' w='auto' ml='auto'>
<AccountMenu />
</Box>
</Group>
</Toolbar>
</AppBar>
<Drawer variant="permanent" open={open}>
<Toolbar
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
px: [1],
}}
<Group style={{ flexShrink: 0 }}>
<Menu
width={260}
position="bottom-end"
transitionProps={{ transition: 'pop-top-right' }}
withinPortal
>
<Box display="flex" justifyContent={'space-between'} width={"100%"}>
</Box>
</Toolbar>
<Divider />
<List component="nav">
{pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item, index) => (
<ListItem
key={index}
disablePadding
<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')}
>
<ListItemButton
Тема: {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={() => {
navigate(item.path)
logout()
navigate("/auth/signin")
}}
style={{ background: location.pathname === item.path ? innerTheme.palette.action.selected : "transparent" }}
selected={location.pathname === item.path}
leftSection={<IconLogout style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.label}
sx={{ color: location.pathname === item.path ? colors.blue[700] : innerTheme.palette.text.primary }}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Drawer>
Выход
</Menu.Item>
<Box
component="main"
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? theme.palette.grey[100]
: theme.palette.grey[900],
flexGrow: 1,
maxHeight: "100vh",
overflow: 'auto',
}}
>
<Toolbar />
<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 />
</Box>
</Box>
</ThemeProvider>
);
</Flex>
</AppShell.Main>
</AppShell>
)
}
export default DashboardLayout

View File

@ -1,252 +0,0 @@
// Layout for dashboard with responsive drawer
import { Outlet, useLocation, useNavigate } from "react-router-dom"
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MenuIcon from '@mui/icons-material/Menu';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import { Api, ExitToApp, Home, People, Settings, Shield } from "@mui/icons-material";
import { getUserData, useAuthStore } from "../store/auth";
import { UserData } from "../interfaces/auth";
const drawerWidth = 240;
export default function DashboardLayoutResponsive() {
const [open, setOpen] = React.useState(true);
const authStore = useAuthStore();
const [userData, setUserData] = React.useState<UserData>();
const location = useLocation()
const navigate = useNavigate()
//const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false);
const handleDrawerClose = () => {
setIsClosing(true);
setMobileOpen(false);
};
const handleDrawerTransitionEnd = () => {
setIsClosing(false);
};
const handleDrawerToggle = () => {
if (!isClosing) {
setMobileOpen(!mobileOpen);
}
};
const pages = [
{
label: "Главная",
path: "/",
icon: <Home />
},
{
label: "Пользователи",
path: "/user",
icon: <People />
},
{
label: "Роли",
path: "/role",
icon: <Shield />
},
{
label: "API Test",
path: "/api-test",
icon: <Api />
},
]
const misc = [
{
label: "Настройки",
path: "/settings",
icon: <Settings />
},
{
label: "Выход",
path: "/signOut",
icon: <ExitToApp />
}
]
const getPageTitle = () => {
const currentPath = location.pathname;
const allPages = [...pages, ...misc];
const currentPage = allPages.find(page => page.path === currentPath);
return currentPage ? currentPage.label : "Dashboard";
};
const toggleDrawer = () => {
setOpen(!open);
};
const drawer = (
<div>
<Toolbar>
<Box display="flex" justifyContent={'space-between'} width={"100%"}>
<Box>
<Typography>{userData?.name} {userData?.surname}</Typography>
<Divider />
<Typography variant="caption">{userData?.login}</Typography>
</Box>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
onClick={toggleDrawer}
sx={{
...(open && { display: 'none' }),
}}
>
<MenuIcon />
</IconButton>
</Box>
</Toolbar>
<Divider />
<List>
{pages.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List>
{misc.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
</div >
);
React.useEffect(() => {
if (authStore) {
const stored = getUserData()
if (stored) {
setUserData(stored)
}
}
}, [authStore])
return (
<Box sx={{
display: 'flex',
flexGrow: 1,
height: "100vh"
}}>
<CssBaseline />
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
{getPageTitle()}
</Typography>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
aria-label="mailbox folders"
>
{/* The implementation can be swapped with js to avoid SEO duplication of links. */}
<Drawer
variant="temporary"
open={mobileOpen}
onTransitionEnd={handleDrawerTransitionEnd}
onClose={handleDrawerClose}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
display: { xs: 'block', sm: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawer}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', sm: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawer}
</Drawer>
</Box>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${drawerWidth}px)` }
}}
>
<Toolbar />
<Outlet />
</Box>
</Box>
);
}

View File

@ -1,31 +1,12 @@
// Layout for fullscreen pages
import { Box, createTheme, ThemeProvider, useTheme } from "@mui/material";
import { Flex } from "@mantine/core";
import { Outlet } from "react-router-dom";
export default function MainLayout() {
const theme = useTheme()
const innerTheme = createTheme(theme)
return (
<ThemeProvider theme={innerTheme}>
<Box
sx={{
color: (theme) => theme.palette.mode === 'light'
? theme.palette.grey[900]
: theme.palette.grey[100],
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? theme.palette.grey[100]
: theme.palette.grey[900],
flexGrow: 1,
maxHeight: "100vh",
height: '100%',
overflow: 'auto',
}}
>
<Flex align='center' justify='center' h='100%' w='100%'>
<Outlet />
</Box>
</ThemeProvider>
</Flex>
)
}

View File

@ -1,98 +1,25 @@
import "@fontsource/inter";
import React, { useEffect } from 'react'
import '@mantine/core/styles.css';
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { ThemeProvider } from '@emotion/react'
import { createTheme } from '@mui/material'
import { ruRU } from '@mui/material/locale'
import { getDarkMode, usePrefStore } from "./store/preferences.ts";
import { createTheme, DEFAULT_THEME, MantineProvider, mergeMantineTheme } from '@mantine/core';
const mainTheme = createTheme(
{
typography: {
fontFamily: [
'Inter'
].join(',')
const overrides = createTheme({
// Set this color to `--mantine-color-body` CSS variable
white: '#F0F0F0',
colors: {
// ...
},
components: {
MuiAppBar: {
// styleOverrides: {
// colorPrimary: {
// backgroundColor: 'gray'
// }
// }
},
MuiListItemButton: {
defaultProps: {
//disableRipple: true
}
},
MuiButton: {
defaultProps: {
//disableRipple: true
}
},
MuiButtonBase: {
defaultProps: {
//disableRipple: true,
}
},
MuiButtonGroup: {
defaultProps: {
//disableRipple: true,
}
},
MuiIconButton: {
defaultProps: {
})
}
},
MuiIcon: {
defaultProps: {
}
}
},
},
ruRU
)
const darkTheme = createTheme(
{
...mainTheme,
palette: {
mode: "dark",
primary: { main: '#1976d2' },
},
},
);
const lightTheme = createTheme(
{
...mainTheme,
palette: {
mode: "light",
primary: { main: '#1976d2' },
},
},
);
function ThemedApp() {
const prefStore = usePrefStore()
useEffect(() => {
getDarkMode()
}, [])
return (
<ThemeProvider theme={prefStore.darkMode ? darkTheme : lightTheme}>
<App />
</ThemeProvider>
)
}
const theme = mergeMantineTheme(DEFAULT_THEME, overrides);
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<ThemedApp />
<MantineProvider theme={theme}>
<App />
</MantineProvider>
</React.StrictMode>,
)

View File

@ -1,51 +0,0 @@
import { Box } from "@mui/material"
import { useCities } from "../hooks/swrHooks"
import { useEffect, useState } from "react"
import { DataGrid, GridColDef } from "@mui/x-data-grid"
import axiosInstance from "../http/axiosInstance"
import { BASE_URL } from "../constants"
export default function ApiTest() {
const limit = 10
const [paginationModel, setPaginationModel] = useState({
page: 1,
pageSize: limit
})
const [rowCount, setRowCount] = useState(0)
const fetchCount = async () => {
await axiosInstance.get(`/general/cities_count`, {
baseURL: BASE_URL.fuel
}).then(response => {
setRowCount(response.data)
})
}
const { cities, isLoading } = useCities(paginationModel.pageSize, paginationModel.page)
useEffect(() => {
fetchCount()
}, [])
const citiesColumns: GridColDef[] = [
{ field: 'id' },
{ field: 'name' },
]
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
<DataGrid
rows={cities || []}
columns={citiesColumns}
paginationMode='server'
rowCount={rowCount}
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
/>
</Box>
)
}

View File

@ -1,8 +1,7 @@
import { Box, Typography } from '@mui/material'
import { DataGrid, GridColDef } from '@mui/x-data-grid'
import { useEffect, useState } from 'react'
import { IBoiler } from '../interfaces/fuel'
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)
@ -25,8 +24,8 @@ function Boilers() {
setBoilerSearch("")
}, [])
const boilersColumns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number" },
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 },
@ -34,23 +33,52 @@ function Boilers() {
]
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}>
<Typography variant='h6' fontWeight='600'>
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Text size="xl" fw={600}>
Котельные
</Typography>
</Text>
{boilers &&
<DataGrid
rows={boilers.map((boiler: IBoiler) => {
return { ...boiler, id: boiler.id_object }
})}
columns={boilersColumns}
/>
}
<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>
</Box>
</Box>
) : (
<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>
)
}

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

@ -1,15 +1,54 @@
import { Box, Card, Typography } from "@mui/material";
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 (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
<Typography variant='h6' fontWeight='700'>
Последние файлы
</Typography>
<Card>
<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>
</Box>
)
}
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

@ -1,22 +1,22 @@
import { useEffect, useState } from 'react'
import { Card, Stack } from '@mui/material';
import { Card, Flex } from '@mantine/core';
function CardComponent({
url,
is_alive
}: { url: any, is_alive: any }) {
}: { url: string, is_alive: boolean }) {
return (
<Card>
<Stack p='24px' direction='column'>
<Flex p='sm' direction='column'>
<p>{url}</p>
<p>{JSON.stringify(is_alive)}</p>
</Stack>
</Flex>
</Card>
)
}
export default function MonitorPage() {
const [servers, setServers] = useState<any>([])
const [servers, setServers] = useState([])
useEffect(() => {
const eventSource = new EventSource(`${import.meta.env.VITE_API_MONITOR_URL}/watch`);
@ -38,11 +38,11 @@ export default function MonitorPage() {
return (
<div>
<Stack direction='column' spacing={1}>
{servers.length > 0 && servers.map((server: any) => (
<Flex direction='column' gap='sm'>
{servers.length > 0 && servers.map((server: { name: string, status: boolean }) => (
<CardComponent url={server.name} is_alive={server.status} />
))}
</Stack>
</Flex>
</div>
)
}

View File

@ -1,13 +1,15 @@
import { Error } from "@mui/icons-material";
import { Box, Typography } from "@mui/material";
import { Flex, Text } from "@mantine/core";
import { IconError404 } from "@tabler/icons-react";
export default function NotFound() {
return (
<>
<Box sx={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<Error />
<Typography>Запрашиваемая страница не найдена.</Typography>
</Box>
</>
<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

@ -1,26 +1,25 @@
import { Fragment, useEffect, useState } from "react"
import { Autocomplete, Box, Button, CircularProgress, IconButton, TextField } from "@mui/material"
import { DataGrid } from "@mui/x-data-grid"
import { useEffect, useState } from "react"
import { useCities, useReport, useReportExport } from "../hooks/swrHooks"
import { useDebounce } from "@uidotdev/usehooks"
import { ICity } from "../interfaces/fuel"
import { Update } from "@mui/icons-material"
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 | null>("")
const [search, setSearch] = useState<string | undefined>("")
const debouncedSearch = useDebounce(search, 500)
const [selectedOption, setSelectedOption] = useState<ICity | null>(null)
const { cities, isLoading } = useCities(10, 1, debouncedSearch)
const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { cities } = useCities(10, 1, debouncedSearch)
const { report, isLoading: reportLoading } = useReport(selectedOption?.id)
const { report } = useReport(selectedOption)
const { reportExported } = useReportExport(selectedOption?.id, download)
const { reportExported } = useReportExport(selectedOption, download)
const refreshReport = async () => {
mutate(`/info/reports/${selectedOption?.id}?to_export=false`)
mutate(`/info/reports/${selectedOption}?to_export=false`)
}
useEffect(() => {
@ -41,84 +40,99 @@ export default function Reports() {
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
<Box sx={{ display: 'flex', gap: '16px' }}>
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Flex component="form" gap={'sm'}>
{/* <SearchableSelect /> */}
<Autocomplete
fullWidth
onInputChange={(_, value) => setSearch(value)}
onChange={(_, value) => setSelectedOption(value)}
isOptionEqualToValue={(option: ICity, value: ICity) => option.id === value.id}
getOptionLabel={(option: ICity) => option.name ? option.name : ""}
options={cities || []}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
size='small'
label="Населенный пункт"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
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}
/>
<IconButton onClick={() => refreshReport()}>
<Update />
</IconButton>
<ActionIcon size='auto' variant='transparent' onClick={() => refreshReport()}>
<IconRefresh />
</ActionIcon>
<Button onClick={() => exportReport()}>
<Button disabled={!selectedOption} onClick={() => exportReport()}>
Экспорт
</Button>
</Box>
</Flex>
<DataGrid
autoHeight
style={{ width: "100%" }}
loading={reportLoading}
rows={
report ?
[...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 row;
})
:
[]
}
columns={[
{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
}))
]}
initialState={{
pagination: {
paginationModel: { page: 0, pageSize: 10 },
},
}}
pageSizeOptions={[10, 20, 50, 100]}
checkboxSelection={false}
disableRowSelectionOnClick
].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>
processRowUpdate={(updatedRow) => {
return updatedRow
}}
onProcessRowUpdateError={() => {
}}
/>
</Box>
) : (
<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

@ -1,84 +1,63 @@
import { useState } from 'react'
import { Box, Button, CircularProgress, Modal } from '@mui/material'
import { DataGrid, GridColDef } from '@mui/x-data-grid'
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 [open, setOpen] = useState(false)
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: GridColDef[] = [
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 <CircularProgress />
if (isLoading) return <Loader />
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '16px',
flexGrow: 1,
p: '16px'
}}>
<Button onClick={() => setOpen(true)}>
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Button onClick={open}>
Добавить роль
</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
>
<Modal opened={opened} onClose={close} title="Создание роли" centered>
<FormFields
sx={{
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
}}
fields={createFields}
submitHandler={RoleService.createRole}
title="Создание роли"
/>
</Modal>
<DataGrid
autoHeight
style={{ width: "100%" }}
rows={roles}
columns={columns}
initialState={{
pagination: {
paginationModel: { page: 0, pageSize: 10 },
},
}}
pageSizeOptions={[10, 20, 50, 100]}
disableRowSelectionOnClick
processRowUpdate={(updatedRow) => {
return updatedRow
}}
onProcessRowUpdateError={() => {
}}
/>
</Box>
<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

@ -1,67 +1,41 @@
import { Box, Tab, Tabs } from "@mui/material"
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(0)
const handleTabChange = (newValue: number) => {
setCurrentTab(newValue);
}
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function CustomTabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
const [currentTab, setCurrentTab] = useState<string | null>('0')
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>{children}</Box>}
</div>
);
}
<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>
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={currentTab} onChange={(_, value) =>
handleTabChange(value)
} aria-label="basic tabs example">
<Tab label="Серверы" />
<Tab label="IP-адреса" />
<Tab label="Hardware" />
<Tab label="Storages" />
</Tabs>
</Box>
<CustomTabPanel value={currentTab} index={0}>
<Tabs.Panel value="0" pt='sm'>
<ServersView />
</CustomTabPanel>
</Tabs.Panel>
<CustomTabPanel value={currentTab} index={1}>
<Tabs.Panel value="1" pt='sm'>
<ServerIpsView />
</CustomTabPanel>
</Tabs.Panel>
<CustomTabPanel value={currentTab} index={2}>
<Tabs.Panel value="2" pt='sm'>
<ServerHardware />
</CustomTabPanel>
</Tabs.Panel>
<CustomTabPanel value={currentTab} index={3}>
<Tabs.Panel value="3" pt='sm'>
<ServerStorage />
</CustomTabPanel>
</Tabs.Panel>
</Tabs>
</Flex>
{/* <BarChart
xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]}
@ -69,6 +43,6 @@ export default function Servers() {
width={500}
height={300}
/> */}
</Box>
</ScrollAreaAutosize>
)
}

View File

@ -1,4 +1,3 @@
import { Box, Stack } from "@mui/material"
import UserService from "../services/UserService"
import { setUserData, useAuthStore } from "../store/auth"
import { useEffect, useState } from "react"
@ -6,6 +5,7 @@ import { CreateField } from "../interfaces/create"
import { IUser } from "../interfaces/user"
import FormFields from "../components/FormFields"
import AuthService from "../services/AuthService"
import { Flex, ScrollAreaAutosize } from "@mantine/core"
export default function Settings() {
const { token } = useAuthStore()
@ -39,17 +39,13 @@ export default function Settings() {
]
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "16px",
}}
<ScrollAreaAutosize
w={'100%'}
h={'100%'}
p='sm'
>
{currentUser &&
<Stack spacing={2} width='100%'>
<Stack width='100%'>
<Flex direction='column' gap='sm' w='100%'>
<FormFields
fields={profileFields}
defaultValues={currentUser}
@ -59,17 +55,14 @@ export default function Settings() {
submitHandler={(data) => UserService.updateUser({ id: currentUser.id, ...data })}
title="Пользователь"
/>
</Stack>
<Stack width='100%'>
<FormFields
fields={passwordFields}
submitHandler={(data) => AuthService.updatePassword({ id: currentUser.id, ...data })}
title="Смена пароля"
/>
</Stack>
</Stack>
</Flex>
}
</Box>
</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

View File

@ -1,18 +1,27 @@
import { Box, Button, CircularProgress, Modal } from "@mui/material"
import { DataGrid, GridColDef } from "@mui/x-data-grid"
import { useRoles, useUsers } from "../hooks/swrHooks"
import { IRole } from "../interfaces/role"
import { useState } from "react"
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 [open, setOpen] = useState(false)
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: '' },
@ -23,14 +32,14 @@ export default function Users() {
{ key: 'password', headerName: 'Пароль', type: 'string', required: true, defaultValue: '' },
]
const columns: GridColDef[] = [
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: 'is_active', headerName: 'Статус', type: "boolean", flex: 1, editable: true },
{
field: 'role_id',
headerName: 'Роль',
@ -41,44 +50,91 @@ export default function Users() {
},
];
if (isError) return <div>Произошла ошибка при получении данных.</div>
if (isLoading) return <CircularProgress />
if (isError) return (
<div>
Произошла ошибка при получении данных.
</div>
)
if (isLoading) {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<Loader />
</Flex>
)
}
return (
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "16px",
p: '16px'
}}
>
<Button onClick={() => setOpen(true)}>
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Button onClick={open}>
Добавить пользователя
</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
>
<Modal opened={opened} onClose={close} title="Регистрация пользователя" centered>
<FormFields
fields={createFields}
submitHandler={UserService.createUser}
title="Создание пользователя"
sx={{
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
}}
/>
</Modal>
<DataGrid
{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%" }}
@ -99,7 +155,7 @@ export default function Users() {
onProcessRowUpdateError={() => {
}}
/>
</Box>
/> */}
</ScrollAreaAutosize>
)
}

View File

@ -1,8 +1,8 @@
import { Box, Button, CircularProgress, Container, Fade, Grow, Stack, TextField, Typography } from '@mui/material'
import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form';
import AuthService from '../../services/AuthService';
import { CheckCircle } from '@mui/icons-material';
import { Button, Flex, Loader, Paper, Text, TextInput, Transition } from '@mantine/core';
import { IconCheck } from '@tabler/icons-react';
interface PasswordResetProps {
email: string;
@ -31,61 +31,65 @@ function PasswordReset() {
}
return (
<Container maxWidth="sm">
<Box my={4}>
<Typography variant="h4" component="h1" gutterBottom>
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Flex direction='column' gap='sm'>
<Text size="xl" fw={500}>
Восстановление пароля
</Typography>
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
{!success && <Fade in={!success}>
<Stack spacing={2}>
<Typography>
{!success &&
<Transition mounted={!success} transition='fade'>
{(styles) =>
<Flex style={styles} direction='column' gap={'md'}>
<Text>
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
</Typography>
</Text>
<TextField
fullWidth
margin="normal"
label="E-mail"
<TextInput
label='E-mail'
required
{...register('email', { required: 'Введите E-mail' })}
error={!!errors.email}
helperText={errors.email?.message}
error={errors.email?.message}
/>
<Box sx={{ display: 'flex', gap: '16px' }}>
<Button fullWidth type="submit" disabled={isSubmitting || watch('email').length == 0} variant="contained" color="primary">
{isSubmitting ? <CircularProgress size={16} /> : 'Восстановить пароль'}
<Flex gap='sm'>
<Button flex={1} type="submit" disabled={isSubmitting || watch('email').length == 0} variant='filled'>
{isSubmitting ? <Loader size={16} /> : 'Восстановить пароль'}
</Button>
<Button fullWidth href="/auth/signin" type="button" variant="text" color="primary">
<Button flex={1} component='a' href="/auth/signin" type="button" variant='light'>
Назад
</Button>
</Box>
</Flex>
</Stack>
</Fade>}
</Flex>
}
</Transition>
}
{success &&
<Grow in={success}>
<Stack spacing={2}>
<Stack direction='row' alignItems='center' spacing={2}>
<CheckCircle color='success' />
<Typography>
<Transition mounted={!success} transition='scale'>
{(styles) =>
<Flex style={styles} direction='column' gap='sm'>
<Flex align='center' gap='sm'>
<IconCheck />
<Text>
На указанный адрес было отправлено письмо с новыми данными для авторизации.
</Typography>
</Stack>
<Box sx={{ display: 'flex', gap: '16px' }}>
<Button fullWidth href="/auth/signin" type="button" variant="contained" color="primary">
</Text>
</Flex>
<Flex gap='sm'>
<Button component='a' href="/auth/signin" type="button">
Войти
</Button>
</Box>
</Stack>
</Grow>
</Flex>
</Flex>
}
</Transition>
}
</form>
</Box>
</Container>
</Flex>
</Paper>
)
}

View File

@ -1,14 +1,14 @@
import { useForm, SubmitHandler } from 'react-hook-form';
import { TextField, Button, Container, Typography, Box, Stack, Link, CircularProgress } from '@mui/material';
import { AxiosResponse } from 'axios';
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 } } = useForm<LoginFormData>({
const { register, handleSubmit, setError, formState: { errors, isSubmitting, isValid } } = useForm<LoginFormData>({
defaultValues: {
username: '',
password: '',
@ -39,62 +39,60 @@ const SignIn = () => {
login(token)
navigate('/');
} catch (error: any) {
} catch (error: unknown) {
if ((error as AxiosError).response?.data) {
const err = (error as AxiosError).response?.data
setError('password', {
message: error?.response?.data?.detail
message: (err as { detail: string })?.detail
})
}
}
};
return (
<Container maxWidth="sm">
<Box my={4}>
<Typography variant="h4" component="h1" gutterBottom>
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Flex direction='column' gap='sm'>
<Text size="xl" fw={500}>
Вход
</Typography>
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}>
<TextField
fullWidth
margin="normal"
label="Логин"
<Flex direction='column' gap='sm'>
<TextInput
label='Логин'
required
{...register('username', { required: 'Введите логин' })}
error={!!errors.username}
helperText={errors.username?.message}
error={errors.username?.message}
/>
<TextField
fullWidth
margin="normal"
type="password"
label="Пароль"
<TextInput
label='Пароль'
type='password'
required
{...register('password', { required: 'Введите пароль' })}
error={!!errors.password}
helperText={errors.password?.message}
error={errors.password?.message}
/>
<Box sx={{ display: 'flex', gap: '16px', justifyContent: 'flex-end' }}>
<Link href="/auth/password-reset" color="primary">
<Flex justify='flex-end' gap='sm'>
<Button component='a' href='/auth/password-reset' variant='transparent'>
Восстановить пароль
</Link>
</Box>
</Button>
</Flex>
<Box sx={{ display: 'flex', gap: '16px' }}>
<Button fullWidth type="submit" variant="contained" color="primary">
{isSubmitting ? <CircularProgress size={16} /> : 'Вход'}
<Flex gap='sm'>
<Button disabled={!isValid} type="submit" flex={1} variant='filled'>
{isSubmitting ? <Loader size={16} /> : 'Вход'}
</Button>
{/* <Button fullWidth href="/auth/signup" type="button" variant="text" color="primary">
{/* <Button component='a' flex={1} href='/auth/signup' type="button" variant='light'>
Регистрация
</Button> */}
</Box>
</Stack>
</Flex>
</Flex>
</form>
</Box>
</Container>
</Flex>
</Paper>
);
};

View File

@ -1,10 +1,10 @@
import { useForm, SubmitHandler } from 'react-hook-form';
import { TextField, Button, Container, Typography, Box } from '@mui/material';
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 } } = useForm<IUser>({
const { register, handleSubmit, formState: { errors, isValid, isSubmitting } } = useForm<IUser>({
defaultValues: {
email: '',
login: '',
@ -26,77 +26,66 @@ const SignUp = () => {
};
return (
<Container maxWidth="sm">
<Box my={4}>
<Typography variant="h4" component="h1" gutterBottom>
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Flex direction='column' gap='sm'>
<Text size="xl" fw={500}>
Регистрация
</Typography>
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
<TextField
fullWidth
margin="normal"
label="Email"
<Flex direction='column' gap='sm'>
<TextInput
label='Email'
required
{...register('email', { required: 'Email обязателен' })}
error={!!errors.email}
helperText={errors.email?.message}
error={errors.email?.message}
/>
<TextField
fullWidth
margin="normal"
label="Логин"
<TextInput
label='Логин'
required
{...register('login', { required: 'Логин обязателен' })}
error={!!errors.login}
helperText={errors.login?.message}
error={errors.login?.message}
/>
<TextField
fullWidth
margin="normal"
label="Телефон"
<TextInput
label='Телефон'
required
{...register('phone')}
error={!!errors.phone}
helperText={errors.phone?.message}
error={errors.phone?.message}
/>
<TextField
fullWidth
margin="normal"
label="Имя"
<TextInput
label='Имя'
required
{...register('name')}
error={!!errors.name}
helperText={errors.name?.message}
error={errors.name?.message}
/>
<TextField
fullWidth
margin="normal"
label="Фамилия"
<TextInput
label='Фамилия'
required
{...register('surname')}
error={!!errors.surname}
helperText={errors.surname?.message}
error={errors.surname?.message}
/>
<TextField
fullWidth
margin="normal"
<TextInput
label='Пароль'
type="password"
label="Пароль"
required
{...register('password', { required: 'Пароль обязателен' })}
error={!!errors.password}
helperText={errors.password?.message}
error={errors.password?.message}
/>
<Button type="submit" variant="contained" color="primary">
Зарегистрироваться
<Flex gap='sm'>
<Button disabled={!isValid} type="submit" flex={1} variant='filled'>
{isSubmitting ? <Loader size={16} /> : 'Зарегистрироваться'}
</Button>
</Flex>
</Flex>
</form>
</Box>
</Container>
</Flex>
</Paper>
);
};

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;

108
client/src/store/map.ts Normal file
View File

@ -0,0 +1,108 @@
import { create } from 'zustand';
import { ToolType } from '../types/tools';
import { Point } from 'ol/geom';
import Map from 'ol/Map';
import { Coordinate } from 'ol/coordinate';
import { SatelliteMapsProvider } from '../interfaces/map';
interface MapState {
currentTool: ToolType;
measureType: "LineString" | "Polygon";
measureShowSegments: boolean;
measureClearPrevious: boolean;
tipPoint: Point | null;
map: Map | null;
currentZ: number | undefined;
currentX: number | undefined;
currentY: number | undefined;
currentCoordinate: Coordinate | null;
statusText: string;
satMapsProvider: SatelliteMapsProvider;
selectedObjectType: number | null;
alignMode: boolean;
}
export const useMapStore = create<MapState>(() => ({
currentTool: null,
measureType: "LineString",
measureShowSegments: true,
measureClearPrevious: true,
tipPoint: null,
map: null,
currentZ: undefined,
currentX: undefined,
currentY: undefined,
currentCoordinate: null,
statusText: '',
satMapsProvider: 'google',
selectedObjectType: null,
alignMode: false
}));
const setCurrentZ = (z: number | undefined) => useMapStore.setState(() => ({ currentZ: z }))
const setCurrentX = (x: number | undefined) => useMapStore.setState(() => ({ currentX: x }))
const setCurrentY = (y: number | undefined) => useMapStore.setState(() => ({ currentY: y }))
const setCurrentCoordinate = (c: Coordinate | null) => useMapStore.setState(() => ({ currentCoordinate: c }))
const setStatusText = (t: string) => useMapStore.setState(() => ({ statusText: t }))
const setSatMapsProvider = (p: SatelliteMapsProvider) => useMapStore.setState(() => ({ satMapsProvider: p }))
const setSelectedObjectType = (t: number | null) => useMapStore.setState(() => ({ selectedObjectType: t }))
const setMap = (m: Map | null) => useMapStore.setState(() => ({ map: m }))
const setAlignMode = (m: boolean) => useMapStore.setState(() => ({ alignMode: m }))
const setTipPoint = (tipPoint: Point | null) => {
useMapStore.setState(() => ({ tipPoint: tipPoint }))
}
const getTipPoint = () => useMapStore.getState().tipPoint
const getAlignMode = () => useMapStore.getState().alignMode
const getMap = () => useMapStore.getState().map
const setMeasureType = (tool: "LineString" | "Polygon") => useMapStore.setState(() => ({ measureType: tool }))
const getMeasureType = () => useMapStore.getState().measureType
const setCurrentTool = (tool: ToolType) => {
tool === useMapStore.getState().currentTool
? useMapStore.setState(() => ({ currentTool: null }))
: useMapStore.setState(() => ({ currentTool: tool }))
}
const getCurrentTool = () => useMapStore.getState().currentTool
const getMeasureShowSegments = () => useMapStore.getState().measureShowSegments
const getMeasureClearPrevious = () => useMapStore.getState().measureClearPrevious
const setMeasureShowSegments = (bool: boolean) => {
useMapStore.setState(() => ({ measureShowSegments: bool }))
}
const setMeasureClearPrevious = (bool: boolean) => {
useMapStore.setState(() => ({ measureClearPrevious: bool }))
}
export {
setCurrentTool,
getCurrentTool,
setMeasureShowSegments,
setMeasureClearPrevious,
getMeasureShowSegments,
getMeasureClearPrevious,
setMeasureType,
getMeasureType,
getTipPoint,
setTipPoint,
setCurrentZ,
setCurrentX,
setCurrentY,
setCurrentCoordinate,
setStatusText,
setSatMapsProvider,
setSelectedObjectType,
setMap,
getMap,
setAlignMode,
getAlignMode
}

View File

@ -0,0 +1,60 @@
import { create } from 'zustand';
interface ObjectsState {
selectedRegion: number | null;
selectedDistrict: number | null;
selectedCity: number | null;
selectedYear: number | null;
currentObjectId: string | null;
}
export const useObjectsStore = create<ObjectsState>(() => ({
selectedRegion: null,
selectedDistrict: null,
selectedCity: null,
selectedYear: null,
currentObjectId: null
}));
const getSelectedCity = () => {
return useObjectsStore.getState().selectedCity
}
const setSelectedRegion = (region: number | null) => {
useObjectsStore.setState(() => ({ selectedRegion: region }))
}
const setSelectedDistrict = (district: number | null) => {
useObjectsStore.setState(() => ({ selectedDistrict: district }))
}
const setSelectedCity = (city: number | null) => {
useObjectsStore.setState(() => ({ selectedCity: city }))
}
const getSelectedYear = () => {
return useObjectsStore.getState().selectedYear
}
const setSelectedYear = (year: number | null) => {
useObjectsStore.setState(() => ({ selectedYear: year }))
}
const getCurrentObjectId = () => {
return useObjectsStore.getState().currentObjectId
}
const setCurrentObjectId = (objectId: string | null) => {
useObjectsStore.setState(() => ({ currentObjectId: objectId }))
}
export {
getSelectedCity,
setSelectedCity,
getSelectedYear,
setSelectedYear,
getCurrentObjectId,
setCurrentObjectId,
setSelectedRegion,
setSelectedDistrict
}

14
client/src/types/tools.ts Normal file
View File

@ -0,0 +1,14 @@
export type ToolType =
"Point" |
"LineString" |
"LinearRing" |
"Polygon" |
"MultiPoint" |
"MultiLineString" |
"MultiPolygon" |
"GeometryCollection" |
"Circle" |
"Measure" |
"Mover" |
"Edit" |
null

3
client/src/utils/math.ts Normal file
View File

@ -0,0 +1,3 @@
export function deg2rad(degrees: number) {
return degrees * (Math.PI / 180)
}

View File

@ -1,11 +1,13 @@
import { defineConfig } from 'vite'
import { defineConfig, type PluginOption } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import { visualizer } from 'rollup-plugin-visualizer'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
nodePolyfills(),
react(),
visualizer() as PluginOption
],
})

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,26 @@
services:
client_app:
container_name: client_app
web_client:
container_name: web_client
build:
context: ./client
dockerfile: Dockerfile
ports:
- 5173:5173
- ${CLIENT_PORT}:5173
restart: always
redis_db:
image: "redis:alpine"
container_name: redis_db
ports:
- ${REDIS_PORT}:${REDIS_PORT}
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ]
volumes:
- ./redis_data:/data
expose:
- ${REDIS_PORT}:${REDIS_PORT}
restart: unless-stopped
# redis_db:
# image: "redis:alpine"
# container_name: redis_db
# ports:
# - ${REDIS_PORT}:${REDIS_PORT}
# environment:
# - REDIS_PASSWORD=${REDIS_PASSWORD}
# command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ]
# volumes:
# - ./redis_data:/data
# expose:
# - ${REDIS_PORT}:${REDIS_PORT}
# restart: unless-stopped
ems:
container_name: ems
@ -29,12 +29,12 @@ services:
dockerfile: Dockerfile
volumes:
- ./ems/public:/app/public
links:
- redis_db:redis_db
- psql_db:psql_db
depends_on:
- redis_db
- psql_db
# links:
# - redis_db:redis_db
# - psql_db:psql_db
# depends_on:
# - redis_db
# - psql_db
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
- REDIS_HOST=${REDIS_HOST}
@ -45,24 +45,24 @@ services:
- ${EMS_PORT}:${EMS_PORT}
restart: always
monitor:
container_name: monitor
build:
context: ./monitor
dockerfile: Dockerfile
environment:
- MONITOR_PORT=${MONITOR_PORT}
ports:
- ${MONITOR_PORT}:${MONITOR_PORT}
volumes:
- ./monitor/data:/app/data
restart: always
# monitor:
# container_name: monitor
# build:
# context: ./monitor
# dockerfile: Dockerfile
# environment:
# - MONITOR_PORT=${MONITOR_PORT}
# ports:
# - ${MONITOR_PORT}:${MONITOR_PORT}
# volumes:
# - ./monitor/data:/app/data
# restart: always
psql_db:
container_name: psql_db
ems_db:
container_name: ems_db
image: postgres:16.4-alpine
volumes:
- ./psql_data:/var/lib/postgresql/data
- ./ems_db:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
@ -79,18 +79,3 @@ services:
retries: 5
start_period: 10s
restart: always
clickhouse_test:
container_name: clickhouse_test
image: clickhouse/clickhouse-server
environment:
- CLICKHOUSE_DB=${CLICKHOUSE_DB}
- CLICKHOUSE_USER=${CLICKHOUSE_USER}
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=${CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT}
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
ports:
- 8123:8123
- 9000:9000
expose:
- 8123
- 9000

716
ems/package-lock.json generated
View File

@ -13,7 +13,7 @@
"axios": "^1.7.4",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"dotenv": "^16.4.7",
"express": "^4.19.2",
"express-validator": "^7.2.0",
"ioredis": "^5.4.1",
@ -21,7 +21,8 @@
"multer": "^1.4.5-lts.1",
"pg": "^8.13.0",
"pump": "^3.0.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5",
"tedious": "^18.6.1"
},
"devDependencies": {
"@types/body-parser": "^1.19.5",
@ -38,6 +39,233 @@
"typescript": "^5.5.4"
}
},
"node_modules/@azure/abort-controller": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-auth": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
"integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.11.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-client": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz",
"integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.4.0",
"@azure/core-rest-pipeline": "^1.9.1",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.6.1",
"@azure/logger": "^1.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-http-compat": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz",
"integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-client": "^1.3.0",
"@azure/core-rest-pipeline": "^1.3.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-lro": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
"integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.2.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-paging": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz",
"integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.17.0.tgz",
"integrity": "sha512-62Vv8nC+uPId3j86XJ0WI+sBf0jlqTqPUFCBNrGtlaUeQUIXWV/D8GE5A1d+Qx8H7OQojn2WguC8kChD6v0shA==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.8.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.9.0",
"@azure/logger": "^1.0.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-tracing": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz",
"integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-util": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz",
"integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/identity": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.5.0.tgz",
"integrity": "sha512-EknvVmtBuSIic47xkOqyNabAme0RYTw52BTMz8eBgU1ysTyMrD1uOoM+JdS0J/4Yfp98IBT3osqq3BfwSaNaGQ==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.9.0",
"@azure/core-client": "^1.9.2",
"@azure/core-rest-pipeline": "^1.17.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0",
"@azure/msal-browser": "^3.26.1",
"@azure/msal-node": "^2.15.0",
"events": "^3.0.0",
"jws": "^4.0.0",
"open": "^8.0.0",
"stoppable": "^1.1.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/keyvault-common": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz",
"integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-client": "^1.5.0",
"@azure/core-rest-pipeline": "^1.8.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.10.0",
"@azure/logger": "^1.1.4",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/keyvault-keys": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.9.0.tgz",
"integrity": "sha512-ZBP07+K4Pj3kS4TF4XdkqFcspWwBHry3vJSOFM5k5ZABvf7JfiMonvaFk2nBF6xjlEbMpz5PE1g45iTMme0raQ==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-client": "^1.5.0",
"@azure/core-http-compat": "^2.0.1",
"@azure/core-lro": "^2.2.0",
"@azure/core-paging": "^1.1.1",
"@azure/core-rest-pipeline": "^1.8.1",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.0.0",
"@azure/keyvault-common": "^2.0.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/logger": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz",
"integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/msal-browser": {
"version": "3.26.1",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.26.1.tgz",
"integrity": "sha512-y78sr9g61aCAH9fcLO1um+oHFXc1/5Ap88RIsUSuzkm0BHzFnN+PXGaQeuM1h5Qf5dTnWNOd6JqkskkMPAhh7Q==",
"dependencies": {
"@azure/msal-common": "14.15.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "14.15.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz",
"integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-node": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.15.0.tgz",
"integrity": "sha512-gVPW8YLz92ZeCibQH2QUw96odJoiM3k/ZPH3f2HxptozmH6+OnyyvKXo/Egg39HAM230akarQKHf0W74UHlh0Q==",
"dependencies": {
"@azure/msal-common": "14.15.0",
"jsonwebtoken": "^9.0.0",
"uuid": "^8.3.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -431,6 +659,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@js-joda/core": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.3.tgz",
"integrity": "sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA=="
},
"node_modules/@prisma/client": {
"version": "5.19.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.19.1.tgz",
@ -659,7 +892,6 @@
"version": "22.4.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz",
"integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==",
"dev": true,
"dependencies": {
"undici-types": "~6.19.2"
}
@ -685,6 +917,20 @@
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true
},
"node_modules/@types/readable-stream": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz",
"integrity": "sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==",
"dependencies": {
"@types/node": "*",
"safe-buffer": "~5.1.1"
}
},
"node_modules/@types/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/@types/redis": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz",
@ -716,6 +962,17 @@
"@types/send": "*"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -752,6 +1009,38 @@
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dependencies": {
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/agent-base/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/agent-base/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@ -802,6 +1091,25 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -814,6 +1122,40 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bl": {
"version": "6.0.16",
"resolved": "https://registry.npmjs.org/bl/-/bl-6.0.16.tgz",
"integrity": "sha512-V/kz+z2Mx5/6qDfRCilmrukUXcXuCoXKg3/3hDvzKKoSUx8CJKudfIoT29XZc3UE9xBvxs5qictiHdprwtteEg==",
"dependencies": {
"@types/readable-stream": "^4.0.0",
"buffer": "^6.0.3",
"inherits": "^2.0.4",
"readable-stream": "^4.2.0"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/bl/node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@ -859,6 +1201,34 @@
"node": ">=8"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -1096,6 +1466,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"engines": {
"node": ">=8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1147,9 +1525,9 @@
}
},
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"engines": {
"node": ">=12"
},
@ -1157,6 +1535,14 @@
"url": "https://dotenvx.com"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -1210,6 +1596,22 @@
"node": ">= 0.6"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
@ -1480,6 +1882,72 @@
"node": ">= 0.8"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/http-proxy-agent/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/http-proxy-agent/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/https-proxy-agent": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/https-proxy-agent/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/https-proxy-agent/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -1491,6 +1959,25 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
@ -1576,6 +2063,20 @@
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -1606,11 +2107,91 @@
"node": ">=0.12.0"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/js-md4": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken/node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -1621,11 +2202,46 @@
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@ -1746,6 +2362,11 @@
"node": ">= 6.0.0"
}
},
"node_modules/native-duplexpair": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz",
"integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -1852,6 +2473,22 @@
"wrappy": "1"
}
},
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -2012,6 +2649,14 @@
"fsevents": "2.3.3"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -2333,6 +2978,11 @@
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
@ -2346,6 +2996,15 @@
"node": ">= 0.8"
}
},
"node_modules/stoppable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
"engines": {
"node": ">=4",
"npm": ">=6"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@ -2379,6 +3038,37 @@
"node": ">=4"
}
},
"node_modules/tedious": {
"version": "18.6.1",
"resolved": "https://registry.npmjs.org/tedious/-/tedious-18.6.1.tgz",
"integrity": "sha512-9AvErXXQTd6l7TDd5EmM+nxbOGyhnmdbp/8c3pw+tjaiSXW9usME90ET/CRG1LN1Y9tPMtz/p83z4Q97B4DDpw==",
"dependencies": {
"@azure/core-auth": "^1.7.2",
"@azure/identity": "^4.2.1",
"@azure/keyvault-keys": "^4.4.0",
"@js-joda/core": "^5.6.1",
"@types/node": ">=18",
"bl": "^6.0.11",
"iconv-lite": "^0.6.3",
"js-md4": "^0.3.2",
"native-duplexpair": "^1.0.0",
"sprintf-js": "^1.1.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tedious/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -2454,8 +3144,7 @@
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"optional": true
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
},
"node_modules/type-is": {
"version": "1.6.18",
@ -2496,8 +3185,7 @@
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/unpipe": {
"version": "1.0.0",
@ -2520,6 +3208,14 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",

View File

@ -17,7 +17,7 @@
"axios": "^1.7.4",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"dotenv": "^16.4.7",
"express": "^4.19.2",
"express-validator": "^7.2.0",
"ioredis": "^5.4.1",
@ -25,7 +25,8 @@
"multer": "^1.4.5-lts.1",
"pg": "^8.13.0",
"pump": "^3.0.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5",
"tedious": "^18.6.1"
},
"devDependencies": {
"@types/body-parser": "^1.19.5",

View File

@ -0,0 +1,362 @@
import express, { Request, Response } from 'express';
import { tediousQuery } from '../../utils/tedious';
import { GeneralDB, GisDB } from '../../constants/db';
const router = express.Router()
router.get('/regions/all', async (req: Request, res: Response) => {
try {
const result = await tediousQuery(
`
SELECT * FROM ${GeneralDB}..vRegions;
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/districts/all', async (req: Request, res: Response) => {
try {
const { region_id } = req.query
const result = await tediousQuery(
`
SELECT c.*, d.name AS district_name
FROM ${GeneralDB}..vCities c
JOIN ${GeneralDB}..vDistricts d ON d.id_region = c.id_region AND d.id = c.id_district
WHERE c.id_region = ${region_id};
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/cities/all', async (req: Request, res: Response) => {
try {
const { offset, limit, search, id } = req.query
const result = await tediousQuery(
`
SELECT * FROM ${GeneralDB}..Cities
${id ? `WHERE id = '${id}'` : ''}
${search ? `WHERE name LIKE '%${search || ''}%'` : ''}
ORDER BY id
OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/types/all', async (req: Request, res: Response) => {
try {
const result = await tediousQuery(
`
SELECT * FROM ${GeneralDB}..tTypes
ORDER BY id
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/objects/all', async (req: Request, res: Response) => {
try {
const { offset, limit, city_id } = req.query
const result = await tediousQuery(
`
SELECT * FROM ${GeneralDB}..vObjects
${city_id ? `WHERE id_city = ${city_id}` : ''}
ORDER BY object_id
OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/objects/list', async (req: Request, res: Response) => {
try {
const { city_id, year, planning, type } = req.query
if (type) {
const result = await tediousQuery(
// `
// SELECT
// *
// FROM
// vObjects
// WHERE
// vObjects.id_city = ${city_id}
// AND vObjects.year = ${year}
// AND type = ${type}
// AND
// (
// CASE
// WHEN TRY_CAST(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT)
// WHEN vObjects.planning = 'TRUE' THEN 1
// WHEN vObjects.planning = 'FALSE' THEN 0
// ELSE NULL
// END
// ) = ${planning};
// `
`
WITH cte_split(type_id, split_value, caption_params) AS
(
-- anchor member
SELECT DISTINCT
type_id,
CAST(LEFT(caption_params, CHARINDEX(',', caption_params + ',') - 1) AS VARCHAR(255)), -- Explicitly casting to VARCHAR
STUFF(caption_params, 1, CHARINDEX(',', caption_params + ','), '')
FROM ${GisDB}..caption_params
WHERE city_id = -1 AND user_id = -1
UNION ALL
-- recursive member
SELECT
type_id,
CAST(LEFT(caption_params, CHARINDEX(',', caption_params + ',') - 1) AS VARCHAR(255)), -- Explicitly casting to VARCHAR
STUFF(caption_params, 1, CHARINDEX(',', caption_params + ','), '')
FROM cte_split
WHERE caption_params > ''
)
SELECT
o.object_id,
o.type,
o.id_city,
o.year,
o.planning,
string_agg(cast(v.value as varchar), ',') as caption
FROM ${GeneralDB}..vObjects o
JOIN cte_split c ON o.type = c.type_id
JOIN ${GeneralDB}..tParameters p ON p.id = split_value
LEFT JOIN ${GeneralDB}..tValues v
ON
v.id_param = split_value
AND v.id_object = o.object_id
AND (v.date_po IS NULL)
AND (v.date_s < DATEFROMPARTS(${Number(year) + 1},01,01))
WHERE
o.id_city = ${city_id}
AND o.year = ${year}
AND o.type = ${type}
AND
(
CASE
WHEN TRY_CAST(o.planning AS BIT) IS NOT NULL THEN TRY_CAST(o.planning AS BIT)
WHEN o.planning = 'TRUE' THEN 1
WHEN o.planning = 'FALSE' THEN 0
ELSE NULL
END
) = ${planning}
GROUP BY object_id, type, id_city, year, planning;
`
)
res.status(200).json(result)
} else {
const result = await tediousQuery(
`
SELECT
tTypes.id AS id,
tTypes.name AS name,
COUNT(vObjects.type) AS count
FROM
vObjects
JOIN
tTypes ON vObjects.type = tTypes.id
WHERE
vObjects.id_city = ${city_id} AND vObjects.year = ${year}
AND
(
CASE
WHEN TRY_CAST(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT)
WHEN vObjects.planning = 'TRUE' THEN 1
WHEN vObjects.planning = 'FALSE' THEN 0
ELSE NULL
END
) = ${planning}
GROUP BY
tTypes.id,
tTypes.name;
`
)
res.status(200).json(result)
}
} catch (err) {
res.status(500)
}
})
router.get('/objects/:id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const result = await tediousQuery(
`
SELECT * FROM ${GeneralDB}..vObjects
${id ? `WHERE object_id = '${id}'` : ''}
`
)
if (Array.isArray(result) && result.length > 0) {
res.status(200).json(result[0])
}
} catch (err) {
res.status(500)
}
})
router.get('/values/all', async (req: Request, res: Response) => {
try {
const { object_id } = req.query
if (!object_id) {
res.status(500)
}
const result = await tediousQuery(
`
SELECT
id_object,
id_param,
CAST(v.value AS varchar(max)) AS value,
date_s,
date_po,
id_user
FROM
${GeneralDB}..tValues v
JOIN
${GeneralDB}..tParameters p ON v.id_param = p.id
WHERE id_object = '${object_id}'
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/params/all', async (req: Request, res: Response) => {
try {
const { param_id } = req.query
if (!param_id) {
res.status(500)
}
const result = await tediousQuery(
`
SELECT * FROM ${GeneralDB}..tParameters
WHERE id = '${param_id}'
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
const tcbParamQuery = (vtable: string, id_city: string) => {
switch (vtable) {
case 'vStreets':
return `SELECT * FROM ${GeneralDB}..${vtable} WHERE id_city = ${id_city};`
case 'vBoilers':
return `SELECT * FROM ${GeneralDB}..${vtable} WHERE id_city = ${id_city};`
default:
return `SELECT * FROM ${GeneralDB}..${vtable};`
}
}
// Get value from TCB parameter
router.get('/params/tcb', async (req: Request, res: Response) => {
try {
const { vtable, id, offset, limit, id_city } = req.query
if (!vtable) {
res.status(500)
}
if (id) {
const result = await tediousQuery(
`
SELECT * FROM ${GeneralDB}..${vtable}
WHERE id = '${id}'
`
)
res.status(200).json(result)
} else {
const result = await tediousQuery(
// `
// SELECT * FROM nGeneral..${vtable}
// ORDER BY id
// OFFSET ${Number(offset) || 0} ROWS
// FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
// `
tcbParamQuery(vtable as string, id_city as string)
)
res.status(200).json(result)
}
} catch (err) {
res.status(500)
}
})
router.get('/search/objects', async (req: Request, res: Response) => {
try {
const { q, id_city, year } = req.query
if (q && id_city && year) {
const result = await tediousQuery(
`
WITH RankedValues AS (
SELECT
id_object,
date_s,
CAST(value AS varchar(max)) AS value,
ROW_NUMBER() OVER (PARTITION BY id_object ORDER BY date_s DESC) AS rn,
o.id_city AS id_city,
o.year AS year
FROM ${GeneralDB}..tValues
JOIN ${GeneralDB}..tObjects o ON o.id = id_object
WHERE CAST(value AS varchar(max)) LIKE '%${q}%'
)
SELECT
id_object,
date_s,
value,
id_city,
year
FROM RankedValues
WHERE rn = 1 AND id_city = ${id_city} AND year = ${year};
`
)
res.status(200).json(result)
} else {
res.status(400).json("Bad request")
}
} catch (err) {
res.status(500).json({
message: "Error",
error: err
})
}
})
export default router

75
ems/src/api/gis/index.ts Normal file
View File

@ -0,0 +1,75 @@
import express, { Request, Response } from 'express';
import { tediousQuery } from '../../utils/tedious';
import { GeneralDB, GisDB } from '../../constants/db';
const router = express.Router()
router.get('/images/all', async (req: Request, res: Response) => {
try {
const { offset, limit, city_id } = req.query
const result = await tediousQuery(
`
SELECT * FROM ${GisDB}..images
${city_id ? `WHERE city_id = ${city_id}` : ''}
ORDER BY city_id
OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
// Get figures by year and city id
router.get('/figures/all', async (req: Request, res: Response) => {
try {
const { offset, limit, object_id, year, city_id } = req.query
const result = await tediousQuery(
`
SELECT * FROM ${GisDB}..figures f
JOIN ${GeneralDB}..vObjects o ON f.object_id = o.object_id WHERE o.id_city = ${city_id} AND f.year = ${year}
ORDER BY f.year
OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
// Get lines by year and city id
router.get('/lines/all', async (req: Request, res: Response) => {
try {
const { offset, limit, object_id, year, city_id } = req.query
const result = await tediousQuery(
`
SELECT * FROM ${GisDB}..lines l
JOIN ${GeneralDB}..vObjects o ON l.object_id = o.object_id WHERE o.id_city = ${city_id} AND l.year = ${year};
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/regions/borders', async (req: Request, res: Response) => {
try {
const result = await tediousQuery(
`
SELECT * FROM ${GisDB}..visual_regions`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
export default router

View File

@ -0,0 +1,72 @@
import express, { Request, Response } from 'express';
import { query, validationResult } from 'express-validator';
import { PrismaClient } from '@prisma/client';
const router = express.Router()
//const prisma = new PrismaClient()
// router.get('/all', async (req: Request, res: Response) => {
// try {
// const nodes = await prisma.nodes.findMany()
// res.json(nodes)
// } catch (error) {
// console.error('Error getting node:', error);
// res.status(500).json({ error: 'Failed to get node' });
// }
// })
// router.get('/', query('id').isString().isUUID(), async (req: Request, res: Response) => {
// try {
// const result = validationResult(req)
// if (!result.isEmpty()) {
// return res.send({ errors: result.array() })
// }
// const { id } = req.params
// const node = await prisma.nodes.findFirst({
// where: {
// id: id
// }
// })
// res.json(node)
// } catch (error) {
// console.error('Error getting node:', error);
// res.status(500).json({ error: 'Failed to get node' });
// }
// })
// router.post('/', async (req: Request, res: Response) => {
// try {
// const { coordinates, object_id, type } = req.body;
// // Convert the incoming array of coordinates into the shape structure
// const shape = coordinates.map((point: number[]) => ({
// object_id: object_id || null,
// x: point[0],
// y: point[1]
// }));
// console.log(shape)
// // Create a new node in the database
// const node = await prisma.nodes.create({
// data: {
// object_id: object_id || null, // Nullable if object_id is not provided
// shape_type: type, // You can adjust this dynamically
// shape: shape, // Store the shape array as Json[]
// label: 'Default'
// }
// });
// res.status(201).json(node);
// } catch (error) {
// console.error('Error creating node:', error);
// res.status(500).json({ error: 'Failed to create node' });
// }
// })
export default router

View File

@ -0,0 +1,24 @@
import express, { Request, Response } from 'express';
import path from 'path';
import fs from 'fs';
const router = express.Router()
const tileFolder = path.join(__dirname, '..', '..', '..', 'public', 'static')
router.get('/:city_id', async (req: Request, res: Response) => {
const { city_id } = req.params
const tilePath1 = path.join(tileFolder, `${city_id}.jpg`)
const tilePath2 = path.join(tileFolder, `${city_id}.png`)
if (fs.existsSync(tilePath1)) {
return res.sendFile(tilePath1)
} else if (fs.existsSync(tilePath2)) {
return res.sendFile(tilePath2)
} else {
res.status(404).send('Tile is not generated or not provided')
}
})
export default router

View File

@ -0,0 +1,83 @@
import express, { Request, Response } from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import { Coordinate } from '../../interfaces/map';
import { generateTilesForZoomLevel } from '../../utils/tiles';
import axios from 'axios';
const router = express.Router()
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, '..', 'public', 'temp'))
},
filename: function (req, file, cb) {
cb(null, Date.now() + path.extname(file.originalname))
}
})
const upload = multer({ storage: storage })
const tileFolder = path.join(__dirname, '..', '..', '..', 'public', 'tile_data')
const uploadDir = path.join(__dirname, '..', '..', '..', 'public', 'temp')
router.post('/upload', upload.single('file'), async (req: Request, res: Response) => {
const { extentMinX, extentMinY, extentMaxX, extentMaxY, blX, blY, tlX, tlY, trX, trY, brX, brY } = req.body
const bottomLeft: Coordinate = { x: blX, y: blY }
const topLeft: Coordinate = { x: tlX, y: tlY }
const topRight: Coordinate = { x: trX, y: trY }
const bottomRight: Coordinate = { x: brX, y: brY }
if (req.file) {
for (let z = 0; z <= 21; z++) {
await generateTilesForZoomLevel(uploadDir, tileFolder, req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z)
}
}
return res.status(200)
})
router.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => {
const { provider, z, x, y } = req.params
if (!['google', 'yandex', 'custom'].includes(provider)) {
return res.status(400).send('Invalid provider')
}
const tilePath = provider === 'custom' ? path.join(tileFolder, provider, z, x, `${y}.png`) : path.join(tileFolder, provider, z, x, `${y}.jpg`)
if (fs.existsSync(tilePath)) {
return res.sendFile(tilePath)
} else {
if (provider !== 'custom') {
try {
const tileData = await fetchTileFromAPI(provider, z, x, y)
fs.mkdirSync(path.dirname(tilePath), { recursive: true })
fs.writeFileSync(tilePath, tileData)
res.contentType('image/jpeg')
res.send(tileData)
} catch (error) {
console.error('Error fetching tile from API:', error)
res.status(500).send('Error fetching tile from API')
}
} else {
res.status(404).send('Tile is not generated or not provided')
}
}
})
const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise<Buffer> => {
const url = provider === 'google'
? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`
: `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`
const response = await axios.get(url, { responseType: 'arraybuffer' })
return response.data
}
export default router

7
ems/src/constants/db.ts Normal file
View File

@ -0,0 +1,7 @@
const GeneralDB = 'nGeneral'
const GisDB = 'New_Gis'
export {
GeneralDB,
GisDB
}

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