forked from VinokurovVE/tests
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
2bf657e8ed | |||
59fded5cab | |||
242ed1aee2 | |||
75d6420d6b | |||
7e8d1f50c8 | |||
f04fc5f575 | |||
d2cb6d9cac | |||
a31ede2669 | |||
b1df6ca398 | |||
b54c2f0e20 | |||
65b7e275fe | |||
59dddfa02c | |||
5218ee851f | |||
71055e7cd0 | |||
9054c576a1 | |||
dec796b75e | |||
87866e4e51 | |||
eeae97288a | |||
e9595f9703 | |||
bd0a317e76 | |||
a4513e7e7a | |||
f51835584d | |||
115c6ec417 | |||
edb6ae00fb | |||
974fc12b34 |
@ -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=
|
@ -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)
|
@ -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()
|
@ -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'))
|
@ -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"
|
@ -1,3 +0,0 @@
|
||||
import pandas as pd
|
||||
import asyncio
|
||||
|
@ -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')
|
@ -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
|
@ -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
1
client/.gitignore
vendored
@ -11,6 +11,7 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
stats.html
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
@ -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>
|
||||
|
2844
client/package-lock.json
generated
2844
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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
34
client/public/logo1.svg
Normal 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
34
client/public/logo2.svg
Normal 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
@ -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
23
client/src/actions/map.ts
Normal 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);
|
||||
}
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
51
client/src/components/CustomTable.module.scss
Normal file
51
client/src/components/CustomTable.module.scss
Normal 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%;
|
||||
}
|
111
client/src/components/CustomTable.tsx
Normal file
111
client/src/components/CustomTable.tsx
Normal 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;
|
@ -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;
|
@ -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={() => {
|
||||
if (!isLoading) {
|
||||
setShouldFetch(true)
|
||||
}
|
||||
}}
|
||||
sx={{ ml: 'auto' }}
|
||||
>
|
||||
{isLoading ?
|
||||
<CircularProgress size={24} variant='indeterminate' />
|
||||
:
|
||||
<Download />
|
||||
<Flex>
|
||||
<ActionIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!isLoading) {
|
||||
setShouldFetch(true)
|
||||
}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
}}
|
||||
variant='subtle'>
|
||||
{isLoading ?
|
||||
<Loader size='sm' />
|
||||
:
|
||||
<IconDownload />
|
||||
}
|
||||
</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,9 +109,10 @@ export default function FolderViewer() {
|
||||
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
|
||||
}
|
||||
|
||||
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
|
||||
const handleFileInput = (files: File[] | null) => {
|
||||
if (files !== null) {
|
||||
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
|
||||
}
|
||||
}
|
||||
|
||||
const uploadFiles = async () => {
|
||||
@ -176,169 +136,157 @@ export default function FolderViewer() {
|
||||
|
||||
if (foldersLoading || documentsLoading) {
|
||||
return (
|
||||
<CircularProgress />
|
||||
<Loader />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
p: '16px'
|
||||
}}>
|
||||
<FileViewer
|
||||
open={fileViewerModal}
|
||||
setOpen={setFileViewerModal}
|
||||
currentFileNo={currentFileNo}
|
||||
setCurrentFileNo={setCurrentFileNo}
|
||||
docs={documents}
|
||||
/>
|
||||
|
||||
<Breadcrumbs>
|
||||
<Link
|
||||
underline='hover'
|
||||
color='inherit'
|
||||
onClick={() => {
|
||||
setCurrentFolder(null)
|
||||
setBreadcrumbs([])
|
||||
}}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
Главная
|
||||
</Link>
|
||||
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<Link
|
||||
key={breadcrumb.id}
|
||||
underline="hover"
|
||||
color="inherit"
|
||||
onClick={() => handleBreadcrumbClick(index)}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
{breadcrumb.name}
|
||||
</Link>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
|
||||
{currentFolder &&
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
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>
|
||||
|
||||
{filesToUpload.length > 0 &&
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Upload />}
|
||||
onClick={uploadFiles}
|
||||
>
|
||||
Загрузить все
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='outlined'
|
||||
startIcon={<Cancel />}
|
||||
onClick={() => {
|
||||
setFilesToUpload([])
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{filesToUpload.length > 0 &&
|
||||
<Box>
|
||||
{filesToUpload.map((file, index) => (
|
||||
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
|
||||
<Box>
|
||||
<InsertDriveFile />
|
||||
<span>{file.name}</span>
|
||||
</Box>
|
||||
|
||||
<IconButton sx={{ ml: 'auto' }} onClick={() => {
|
||||
setFilesToUpload(prev => {
|
||||
return prev.filter((_, i) => i != index)
|
||||
})
|
||||
}}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<ScrollAreaAutosize w={'100%'} h={'100%'} p={'sm'}>
|
||||
{fileViewerModal &&
|
||||
<FileViewer
|
||||
open={fileViewerModal}
|
||||
setOpen={setFileViewerModal}
|
||||
currentFileNo={currentFileNo}
|
||||
setCurrentFileNo={setCurrentFileNo}
|
||||
docs={documents}
|
||||
/>
|
||||
}
|
||||
|
||||
<List
|
||||
dense
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
sx={{
|
||||
backgroundColor: dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit'
|
||||
}}
|
||||
>
|
||||
{currentFolder ? (
|
||||
documents?.map((doc: IDocument, index: number) => (
|
||||
<div key={`${doc.id}-${doc.name}`}>
|
||||
<ItemDocument
|
||||
doc={doc}
|
||||
index={index}
|
||||
handleDocumentClick={handleDocumentClick}
|
||||
/>
|
||||
{index < documents.length - 1 && <Divider />}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
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>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Stack>
|
||||
<Breadcrumbs>
|
||||
<Anchor
|
||||
onClick={() => {
|
||||
setCurrentFolder(null)
|
||||
setBreadcrumbs([])
|
||||
}}
|
||||
>
|
||||
Главная
|
||||
</Anchor>
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<Anchor
|
||||
key={breadcrumb.id}
|
||||
onClick={() => handleBreadcrumbClick(index)}
|
||||
>
|
||||
{breadcrumb.name}
|
||||
</Anchor>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
|
||||
{currentFolder &&
|
||||
<Flex direction='column' gap='sm'>
|
||||
<Flex direction='column' gap='sm' p='sm' style={{
|
||||
border: filesToUpload.length > 0 ? '1px dashed gray' : 'none',
|
||||
borderRadius: '8px',
|
||||
}}>
|
||||
<Flex gap='sm'>
|
||||
<FileButton multiple onChange={handleFileInput}>
|
||||
{(props) => <Button variant='filled' leftSection={isUploading ? <Loader /> : <IconFilePlus />} {...props}>Добавить</Button>}
|
||||
</FileButton>
|
||||
|
||||
{filesToUpload.length > 0 &&
|
||||
<>
|
||||
<Button
|
||||
variant='filled'
|
||||
leftSection={isUploading ? <RingProgress sections={[{ value: uploadProgress, color: 'blue' }]} /> : <IconFileUpload />}
|
||||
onClick={uploadFiles}
|
||||
>
|
||||
Загрузить все
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='outline'
|
||||
leftSection={<IconCancel />}
|
||||
onClick={() => {
|
||||
setFilesToUpload([])
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
</Flex>
|
||||
|
||||
<Divider />
|
||||
|
||||
{filesToUpload.length > 0 &&
|
||||
<Flex direction='column'>
|
||||
{filesToUpload.map((file, index) => (
|
||||
<Flex key={index} p='8px'>
|
||||
<Flex gap='sm' direction='row' align='center'>
|
||||
<IconFile />
|
||||
<Text>{file.name}</Text>
|
||||
</Flex>
|
||||
|
||||
<ActionIcon onClick={() => {
|
||||
setFilesToUpload(prev => {
|
||||
return prev.filter((_, i) => i != index)
|
||||
})
|
||||
}} ml='auto' variant='subtle'>
|
||||
<IconX />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
|
||||
<Table
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
bg={dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit'}
|
||||
highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Название</Table.Th>
|
||||
<Table.Th p={0}>Дата создания</Table.Th>
|
||||
<Table.Th p={0}></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{currentFolder ? (
|
||||
documents?.map((doc: IDocument, index: number) => (
|
||||
<Table.Tr key={doc.id} onClick={() => handleDocumentClick(index)} style={{ cursor: 'pointer' }}>
|
||||
<Table.Td p={0}>
|
||||
<Flex style={FileItemStyle}>
|
||||
<IconFileFilled />
|
||||
{doc.name}
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
<Table.Td p={0}>
|
||||
{new Date(doc.create_date).toLocaleDateString()}
|
||||
</Table.Td>
|
||||
<Table.Td p={0}>
|
||||
<ItemDocument
|
||||
doc={doc}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
folders?.map((folder: IDocumentFolder) => (
|
||||
<Table.Tr key={folder.id} onClick={() => handleFolderClick(folder)} style={{ cursor: 'pointer' }}>
|
||||
<Table.Td p={0}>
|
||||
<Flex style={FileItemStyle}>
|
||||
<IconFolderFilled />
|
||||
{folder.name}
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
<Table.Td p={0} align='left'>
|
||||
{new Date(folder.create_date).toLocaleDateString()}
|
||||
</Table.Td>
|
||||
<Table.Td p={0}>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</ScrollAreaAutosize>
|
||||
)
|
||||
}
|
@ -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>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack gap='sm' w='100%'>
|
||||
{title.length > 0 &&
|
||||
<Text size="xl" fw={500}>
|
||||
{title}
|
||||
</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>
|
||||
</Box>
|
||||
<Button disabled={isSubmitting || Object.keys(dirtyFields).length === 0 || !isValid} type='submit'>
|
||||
{isSubmitting ? <Loader size={16} /> : submitButtonText}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
onClick={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-label="close"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{currentServerData &&
|
||||
<ServerData
|
||||
id={currentServerData?.id}
|
||||
region_id={currentServerData?.region_id}
|
||||
name={currentServerData?.name}
|
||||
/>
|
||||
}
|
||||
</Dialog>
|
||||
<form>
|
||||
<Autocomplete
|
||||
placeholder="Сервер"
|
||||
flex={'1'}
|
||||
data={servers ? servers.map((item: IServer) => ({ label: item.name, value: item.id.toString() })) : []}
|
||||
onSelect={(e) => console.log(e.currentTarget.value)}
|
||||
//onChange={(value) => setSearch(value)}
|
||||
onOptionSubmit={(value) => setSelectedOption(Number(value))}
|
||||
rightSection={
|
||||
//search !== '' &&
|
||||
(
|
||||
<CloseButton
|
||||
size="sm"
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
//setSearch('')
|
||||
setSelectedOption(undefined)
|
||||
}}
|
||||
aria-label="Clear value"
|
||||
/>
|
||||
)
|
||||
}
|
||||
//value={search}
|
||||
/>
|
||||
</form>
|
||||
|
||||
{serversLoading ?
|
||||
<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>
|
||||
}
|
||||
</>
|
||||
)
|
||||
|
@ -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"
|
||||
onClick={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-label="close"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{currentServerData &&
|
||||
<ServerData
|
||||
id={currentServerData?.id}
|
||||
region_id={currentServerData?.region_id}
|
||||
name={currentServerData?.name}
|
||||
/>
|
||||
}
|
||||
</Dialog>
|
||||
<form>
|
||||
<Autocomplete
|
||||
placeholder="Сервер"
|
||||
flex={'1'}
|
||||
data={servers ? servers.map((item: IServer) => ({ label: item.name, value: item.id.toString() })) : []}
|
||||
onSelect={(e) => console.log(e.currentTarget.value)}
|
||||
//onChange={(value) => setSearch(value)}
|
||||
onOptionSubmit={(value) => setSelectedOption(Number(value))}
|
||||
rightSection={
|
||||
//search !== '' &&
|
||||
(
|
||||
<CloseButton
|
||||
size="sm"
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
//setSearch('')
|
||||
setSelectedOption(null)
|
||||
}}
|
||||
aria-label="Clear value"
|
||||
/>
|
||||
)
|
||||
}
|
||||
//value={search}
|
||||
/>
|
||||
</form>
|
||||
|
||||
{serversLoading ?
|
||||
<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>
|
||||
}
|
||||
</>
|
||||
)
|
||||
|
@ -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>
|
||||
}
|
||||
</>
|
||||
)
|
||||
|
@ -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={
|
||||
<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>
|
||||
)
|
||||
<form>
|
||||
<Autocomplete
|
||||
placeholder="Район"
|
||||
flex={'1'}
|
||||
data={regions ? regions.map((item: IRegion) => ({ label: item.name, value: item.id.toString() })) : []}
|
||||
onSelect={(e) => console.log(e.currentTarget.value)}
|
||||
onChange={(value) => setSearch(value)}
|
||||
onOptionSubmit={(value) => setSelectedOption(Number(value))}
|
||||
rightSection={
|
||||
search !== '' && (
|
||||
<CloseButton
|
||||
size="sm"
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
setSearch('')
|
||||
setSelectedOption(null)
|
||||
}}
|
||||
aria-label="Clear value"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
125
client/src/components/Tree/ObjectTree.tsx
Normal file
125
client/src/components/Tree/ObjectTree.tsx
Normal 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
|
@ -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
|
||||
}
|
@ -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
61
client/src/components/map/MapLayers/MapLayers.tsx
Normal file
61
client/src/components/map/MapLayers/MapLayers.tsx
Normal 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
|
@ -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
|
||||
}
|
45
client/src/components/map/MapStatusbar/MapStatusbar.tsx
Normal file
45
client/src/components/map/MapStatusbar/MapStatusbar.tsx
Normal 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
|
@ -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
|
||||
}
|
94
client/src/components/map/MapToolbar/MapToolbar.tsx
Normal file
94
client/src/components/map/MapToolbar/MapToolbar.tsx
Normal 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
|
34
client/src/components/map/MapTree/MapTreeCheckbox.tsx
Normal file
34
client/src/components/map/MapTree/MapTreeCheckbox.tsx
Normal 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>
|
||||
);
|
||||
};
|
199
client/src/components/map/Measure/MeasureStyles.ts
Normal file
199
client/src/components/map/Measure/MeasureStyles.ts
Normal 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;
|
||||
}
|
25
client/src/components/map/ObjectData.tsx
Normal file
25
client/src/components/map/ObjectData.tsx
Normal 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
|
87
client/src/components/map/ObjectParameter.tsx
Normal file
87
client/src/components/map/ObjectParameter.tsx
Normal 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
|
@ -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
|
25
client/src/components/map/RegionSelect.tsx
Normal file
25
client/src/components/map/RegionSelect.tsx
Normal 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
|
100
client/src/components/map/TCBParameter.tsx
Normal file
100
client/src/components/map/TCBParameter.tsx
Normal 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
|
71
client/src/components/map/TableValue.tsx
Normal file
71
client/src/components/map/TableValue.tsx
Normal 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
|
45
client/src/components/map/TabsPane/TabsPane.tsx
Normal file
45
client/src/components/map/TabsPane/TabsPane.tsx
Normal 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
|
@ -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
|
||||
}
|
@ -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,118 +151,106 @@ 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>
|
||||
|
||||
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
|
||||
{currentFileNo != -1 && docs[currentFileNo].name}
|
||||
</Typography>
|
||||
|
||||
<div>
|
||||
<IconButton
|
||||
color='inherit'
|
||||
onClick={() => {
|
||||
if (currentFileNo >= 0 && currentFileNo > 0) {
|
||||
setCurrentFileNo(currentFileNo - 1)
|
||||
}
|
||||
}}
|
||||
disabled={currentFileNo >= 0 && currentFileNo === 0}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
color='inherit'
|
||||
onClick={() => {
|
||||
if (currentFileNo >= 0 && currentFileNo < docs.length) {
|
||||
setCurrentFileNo(currentFileNo + 1)
|
||||
}
|
||||
}}
|
||||
disabled={currentFileNo >= 0 && currentFileNo >= docs.length - 1}
|
||||
>
|
||||
<ChevronRight />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
autoFocus
|
||||
color="inherit"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: '1',
|
||||
overflowY: 'hidden'
|
||||
<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'
|
||||
}}>
|
||||
{fileIsLoading || fileTypeIsLoading ?
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
:
|
||||
fileType === 'application/pdf' ?
|
||||
<PdfViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ?
|
||||
<ExcelViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ?
|
||||
<DocxViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType?.startsWith('image/') ?
|
||||
<ImageViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType && file ?
|
||||
<Box sx={{ display: 'flex', gap: '16px', flexDirection: 'column', p: '16px' }}>
|
||||
<Box sx={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<Warning />
|
||||
<Typography>
|
||||
Предпросмотр данного файла невозможен.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Modal.Header>
|
||||
<Modal.Title component='div' w='100%'>
|
||||
<Flex align='center'>
|
||||
<Text mr='auto'>{currentFileNo != -1 && docs[currentFileNo].name}</Text>
|
||||
|
||||
<Box>
|
||||
<Button variant='contained' onClick={() => {
|
||||
handleSave()
|
||||
}}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Grid>
|
||||
<Grid.Col span='auto'>
|
||||
<Button
|
||||
variant='transparent'
|
||||
onClick={() => {
|
||||
if (currentFileNo >= 0 && currentFileNo > 0) {
|
||||
setCurrentFileNo(currentFileNo - 1)
|
||||
}
|
||||
}}
|
||||
disabled={currentFileNo >= 0 && currentFileNo === 0}
|
||||
>
|
||||
<IconChevronLeft />
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span='auto'>
|
||||
<Button
|
||||
variant='transparent'
|
||||
onClick={() => {
|
||||
if (currentFileNo >= 0 && currentFileNo < docs.length) {
|
||||
setCurrentFileNo(currentFileNo + 1)
|
||||
}
|
||||
}}
|
||||
disabled={currentFileNo >= 0 && currentFileNo >= docs.length - 1}
|
||||
>
|
||||
<IconChevronRight />
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Button
|
||||
autoFocus
|
||||
variant='subtle'
|
||||
onClick={handleSave}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Flex>
|
||||
</Modal.Title>
|
||||
<Modal.CloseButton ml='xl' />
|
||||
</Modal.Header>
|
||||
<Modal.Body style={{ display: 'flex', flexGrow: 1, height: '100%', width: '100vw' }}>
|
||||
{fileIsLoading || fileTypeIsLoading ?
|
||||
<Flex style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<Loader />
|
||||
</Flex>
|
||||
:
|
||||
fileType === 'application/pdf' ?
|
||||
<PdfViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ?
|
||||
<ExcelViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ?
|
||||
<DocxViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType?.startsWith('image/') ?
|
||||
<ImageViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
null
|
||||
}
|
||||
</Box>
|
||||
</Dialog>
|
||||
fileType && file ?
|
||||
<Flex style={{ display: 'flex', gap: '16px', flexDirection: 'column', p: '16px' }}>
|
||||
<Flex style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<IconAlertTriangle />
|
||||
<Text>
|
||||
Предпросмотр данного файла невозможен.
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
<Button variant='contained' onClick={() => {
|
||||
handleSave()
|
||||
}}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
:
|
||||
null
|
||||
}
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
)
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
160
client/src/constants/app.tsx
Normal file
160
client/src/constants/app.tsx
Normal 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
|
||||
}
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -9,7 +9,6 @@ export interface ICity {
|
||||
}
|
||||
|
||||
export interface IBoiler {
|
||||
id: string;
|
||||
id_object: string;
|
||||
boiler_name: string;
|
||||
boiler_code: string;
|
||||
|
36
client/src/interfaces/gis.ts
Normal file
36
client/src/interfaces/gis.ts
Normal 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
|
||||
}
|
@ -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]
|
44
client/src/interfaces/objects.ts
Normal file
44
client/src/interfaces/objects.ts
Normal 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
|
||||
}
|
@ -7,4 +7,5 @@ export interface IUser {
|
||||
name: string;
|
||||
surname: string;
|
||||
is_active: boolean;
|
||||
role_id: number;
|
||||
}
|
@ -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
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={toggleDrawer}
|
||||
sx={{
|
||||
marginRight: '36px',
|
||||
//...(open && { display: 'none' }),
|
||||
}}
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: desktopOpened ? 200 : 50,
|
||||
breakpoint: 'sm',
|
||||
collapsed: { mobile: !mobileOpened },
|
||||
}}
|
||||
>
|
||||
<AppShell.Header>
|
||||
<Flex h="100%" px="md" w='100%' align='center' gap='sm'>
|
||||
<Group>
|
||||
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
||||
</Group>
|
||||
|
||||
<Group w='auto'>
|
||||
{getPageTitle()}
|
||||
</Group>
|
||||
|
||||
<Group id='header-portal' w='auto' ml='auto'>
|
||||
|
||||
</Group>
|
||||
|
||||
<Group style={{ flexShrink: 0 }}>
|
||||
<Menu
|
||||
width={260}
|
||||
position="bottom-end"
|
||||
transitionProps={{ transition: 'pop-top-right' }}
|
||||
withinPortal
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h6"
|
||||
color="inherit"
|
||||
noWrap
|
||||
sx={{ flexGrow: 1 }}
|
||||
>
|
||||
{getPageTitle()}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: "flex", gap: "8px" }}>
|
||||
<Box>
|
||||
<Typography>{userData?.name} {userData?.surname}</Typography>
|
||||
<Divider />
|
||||
<Typography variant="caption">{userData?.login}</Typography>
|
||||
</Box>
|
||||
|
||||
<AccountMenu />
|
||||
</Box>
|
||||
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Drawer variant="permanent" open={open}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
px: [1],
|
||||
}}
|
||||
>
|
||||
<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
|
||||
>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
navigate(item.path)
|
||||
}}
|
||||
style={{ background: location.pathname === item.path ? innerTheme.palette.action.selected : "transparent" }}
|
||||
selected={location.pathname === item.path}
|
||||
<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')}
|
||||
>
|
||||
<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>
|
||||
Тема: {colorScheme === 'dark' ? 'тёмная' : 'светлая'}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconSettings style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
|
||||
}
|
||||
onClick={() => navigate('/settings')}
|
||||
>
|
||||
Настройки профиля
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
logout()
|
||||
navigate("/auth/signin")
|
||||
}}
|
||||
leftSection={<IconLogout style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
|
||||
>
|
||||
Выход
|
||||
</Menu.Item>
|
||||
|
||||
<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
|
@ -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>
|
||||
);
|
||||
}
|
@ -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',
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
<Flex align='center' justify='center' h='100%' w='100%'>
|
||||
<Outlet />
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -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(',')
|
||||
},
|
||||
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: {
|
||||
}
|
||||
}
|
||||
},
|
||||
const overrides = createTheme({
|
||||
// Set this color to `--mantine-color-body` CSS variable
|
||||
white: '#F0F0F0',
|
||||
colors: {
|
||||
// ...
|
||||
},
|
||||
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>,
|
||||
)
|
@ -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>
|
||||
)
|
||||
}
|
@ -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'>
|
||||
Котельные
|
||||
</Typography>
|
||||
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
|
||||
<Text size="xl" fw={600}>
|
||||
Котельные
|
||||
</Text>
|
||||
|
||||
{boilers &&
|
||||
<DataGrid
|
||||
rows={boilers.map((boiler: IBoiler) => {
|
||||
return { ...boiler, id: boiler.id_object }
|
||||
})}
|
||||
columns={boilersColumns}
|
||||
/>
|
||||
}
|
||||
{boilers &&
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{boilersColumns.map(column => (
|
||||
<Table.Th key={column.field}>{column.headerName}</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{boilers.map((boiler: IBoiler) => (
|
||||
<Table.Tr key={boiler.id_object}>
|
||||
{boilersColumns.map(column => {
|
||||
if (column.field === 'activity') {
|
||||
return (
|
||||
boiler.activity ? (
|
||||
<Table.Td key={`${boiler.id_object}-${boiler[column.field]}`}>
|
||||
<Badge fullWidth variant="light">
|
||||
Активен
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
|
12
client/src/pages/ComponentTest.tsx
Normal file
12
client/src/pages/ComponentTest.tsx
Normal 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
|
@ -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() {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
|
||||
<Typography variant='h6' fontWeight='700'>
|
||||
Последние файлы
|
||||
</Typography>
|
||||
const navigate = useNavigate()
|
||||
|
||||
<Card>
|
||||
interface CustomCardProps {
|
||||
link: string;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
}
|
||||
const CustomCard = ({
|
||||
link,
|
||||
icon,
|
||||
label
|
||||
}: CustomCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
onClick={() => navigate(link)}
|
||||
withBorder
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
>
|
||||
<Flex mih='50'>
|
||||
{icon}
|
||||
</Flex>
|
||||
|
||||
<Text fw={500} size="lg" mt="md">
|
||||
{label}
|
||||
</Text>
|
||||
</Card>
|
||||
</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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 => {
|
||||
{report &&
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{[
|
||||
{ field: 'id', headerName: '№', width: 70 },
|
||||
...Object.keys(report).map(key => ({
|
||||
field: key,
|
||||
headerName: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
width: 150
|
||||
}))
|
||||
].map(column => (
|
||||
<Table.Th key={column.headerName}>{column.headerName}</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{[...new Set(Object.keys(report).flatMap(key => Object.keys(report[key])))].map(id => {
|
||||
const row: any = { id: Number(id) };
|
||||
Object.keys(report).forEach(key => {
|
||||
row[key] = report[key][id];
|
||||
});
|
||||
return row;
|
||||
})
|
||||
:
|
||||
[]
|
||||
}
|
||||
columns={[
|
||||
{ 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
|
||||
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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
const [currentTab, setCurrentTab] = useState<string | null>('0')
|
||||
|
||||
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" />
|
||||
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
|
||||
<Flex gap='sm' direction='column'>
|
||||
<Tabs value={currentTab} onChange={setCurrentTab}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="0">Серверы</Tabs.Tab>
|
||||
<Tabs.Tab value="1">IP-адреса</Tabs.Tab>
|
||||
<Tabs.Tab value="3">Hardware</Tabs.Tab>
|
||||
<Tabs.Tab value="4">Storages</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="0" pt='sm'>
|
||||
<ServersView />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="1" pt='sm'>
|
||||
<ServerIpsView />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="2" pt='sm'>
|
||||
<ServerHardware />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="3" pt='sm'>
|
||||
<ServerStorage />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<CustomTabPanel value={currentTab} index={0}>
|
||||
<ServersView />
|
||||
</CustomTabPanel>
|
||||
|
||||
<CustomTabPanel value={currentTab} index={1}>
|
||||
<ServerIpsView />
|
||||
</CustomTabPanel>
|
||||
|
||||
<CustomTabPanel value={currentTab} index={2}>
|
||||
<ServerHardware />
|
||||
</CustomTabPanel>
|
||||
|
||||
<CustomTabPanel value={currentTab} index={3}>
|
||||
<ServerStorage />
|
||||
</CustomTabPanel>
|
||||
</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>
|
||||
)
|
||||
}
|
@ -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,37 +39,30 @@ 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%'>
|
||||
<FormFields
|
||||
fields={profileFields}
|
||||
defaultValues={currentUser}
|
||||
mutateHandler={(data: any) => {
|
||||
setUserData(data)
|
||||
}}
|
||||
submitHandler={(data) => UserService.updateUser({ id: currentUser.id, ...data })}
|
||||
title="Пользователь"
|
||||
/>
|
||||
</Stack>
|
||||
<Flex direction='column' gap='sm' w='100%'>
|
||||
<FormFields
|
||||
fields={profileFields}
|
||||
defaultValues={currentUser}
|
||||
mutateHandler={(data: any) => {
|
||||
setUserData(data)
|
||||
}}
|
||||
submitHandler={(data) => UserService.updateUser({ id: currentUser.id, ...data })}
|
||||
title="Пользователь"
|
||||
/>
|
||||
|
||||
<Stack width='100%'>
|
||||
<FormFields
|
||||
fields={passwordFields}
|
||||
submitHandler={(data) => AuthService.updatePassword({ id: currentUser.id, ...data })}
|
||||
title="Смена пароля"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<FormFields
|
||||
fields={passwordFields}
|
||||
submitHandler={(data) => AuthService.updatePassword({ id: currentUser.id, ...data })}
|
||||
title="Смена пароля"
|
||||
/>
|
||||
</Flex>
|
||||
}
|
||||
</Box>
|
||||
</ScrollAreaAutosize>
|
||||
)
|
||||
}
|
13
client/src/pages/TableTest.tsx
Normal file
13
client/src/pages/TableTest.tsx
Normal 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
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
|
||||
</Typography>
|
||||
{!success &&
|
||||
<Transition mounted={!success} transition='fade'>
|
||||
{(styles) =>
|
||||
<Flex style={styles} direction='column' gap={'md'}>
|
||||
<Text>
|
||||
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
|
||||
</Text>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="E-mail"
|
||||
required
|
||||
{...register('email', { required: 'Введите E-mail' })}
|
||||
error={!!errors.email}
|
||||
helperText={errors.email?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label='E-mail'
|
||||
required
|
||||
{...register('email', { required: 'Введите E-mail' })}
|
||||
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} /> : 'Восстановить пароль'}
|
||||
</Button>
|
||||
<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>
|
||||
</Box>
|
||||
<Button flex={1} component='a' href="/auth/signin" type="button" variant='light'>
|
||||
Назад
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
</Stack>
|
||||
</Fade>}
|
||||
</Flex>
|
||||
}
|
||||
|
||||
</Transition>
|
||||
}
|
||||
{success &&
|
||||
<Grow in={success}>
|
||||
<Stack spacing={2}>
|
||||
<Stack direction='row' alignItems='center' spacing={2}>
|
||||
<CheckCircle color='success' />
|
||||
<Typography>
|
||||
На указанный адрес было отправлено письмо с новыми данными для авторизации.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Box sx={{ display: 'flex', gap: '16px' }}>
|
||||
<Button fullWidth href="/auth/signin" type="button" variant="contained" color="primary">
|
||||
Войти
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grow>
|
||||
<Transition mounted={!success} transition='scale'>
|
||||
{(styles) =>
|
||||
<Flex style={styles} direction='column' gap='sm'>
|
||||
<Flex align='center' gap='sm'>
|
||||
<IconCheck />
|
||||
<Text>
|
||||
На указанный адрес было отправлено письмо с новыми данными для авторизации.
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex gap='sm'>
|
||||
<Button component='a' href="/auth/signin" type="button">
|
||||
Войти
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
</Transition>
|
||||
}
|
||||
</form>
|
||||
</Box>
|
||||
</Container>
|
||||
</Flex>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
setError('password', {
|
||||
message: error?.response?.data?.detail
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
if ((error as AxiosError).response?.data) {
|
||||
const err = (error as AxiosError).response?.data
|
||||
setError('password', {
|
||||
message: (err as { detail: string })?.detail
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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"
|
||||
required
|
||||
{...register('email', { required: 'Email обязателен' })}
|
||||
error={!!errors.email}
|
||||
helperText={errors.email?.message}
|
||||
/>
|
||||
<Flex direction='column' gap='sm'>
|
||||
<TextInput
|
||||
label='Email'
|
||||
required
|
||||
{...register('email', { required: 'Email обязателен' })}
|
||||
error={errors.email?.message}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Логин"
|
||||
required
|
||||
{...register('login', { required: 'Логин обязателен' })}
|
||||
error={!!errors.login}
|
||||
helperText={errors.login?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label='Логин'
|
||||
required
|
||||
{...register('login', { required: 'Логин обязателен' })}
|
||||
error={errors.login?.message}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Телефон"
|
||||
{...register('phone')}
|
||||
error={!!errors.phone}
|
||||
helperText={errors.phone?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label='Телефон'
|
||||
required
|
||||
{...register('phone')}
|
||||
error={errors.phone?.message}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Имя"
|
||||
{...register('name')}
|
||||
error={!!errors.name}
|
||||
helperText={errors.name?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label='Имя'
|
||||
required
|
||||
{...register('name')}
|
||||
error={errors.name?.message}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Фамилия"
|
||||
{...register('surname')}
|
||||
error={!!errors.surname}
|
||||
helperText={errors.surname?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label='Фамилия'
|
||||
required
|
||||
{...register('surname')}
|
||||
error={errors.surname?.message}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
type="password"
|
||||
label="Пароль"
|
||||
required
|
||||
{...register('password', { required: 'Пароль обязателен' })}
|
||||
error={!!errors.password}
|
||||
helperText={errors.password?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label='Пароль'
|
||||
type="password"
|
||||
required
|
||||
{...register('password', { required: 'Пароль обязателен' })}
|
||||
error={errors.password?.message}
|
||||
/>
|
||||
|
||||
<Button type="submit" variant="contained" color="primary">
|
||||
Зарегистрироваться
|
||||
</Button>
|
||||
<Flex gap='sm'>
|
||||
<Button disabled={!isValid} type="submit" flex={1} variant='filled'>
|
||||
{isSubmitting ? <Loader size={16} /> : 'Зарегистрироваться'}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
</Box>
|
||||
</Container>
|
||||
</Flex>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
|
10
client/src/pages/dark.scss
Normal file
10
client/src/pages/dark.scss
Normal 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
108
client/src/store/map.ts
Normal 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
|
||||
}
|
60
client/src/store/objects.ts
Normal file
60
client/src/store/objects.ts
Normal 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
14
client/src/types/tools.ts
Normal 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
3
client/src/utils/math.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function deg2rad(degrees: number) {
|
||||
return degrees * (Math.PI / 180)
|
||||
}
|
@ -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
|
||||
],
|
||||
})
|
||||
|
1568
client/yarn.lock
1568
client/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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
716
ems/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
362
ems/src/api/general/index.ts
Normal file
362
ems/src/api/general/index.ts
Normal 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
75
ems/src/api/gis/index.ts
Normal 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
|
72
ems/src/api/nodes/index.ts
Normal file
72
ems/src/api/nodes/index.ts
Normal 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
|
24
ems/src/api/static/index.ts
Normal file
24
ems/src/api/static/index.ts
Normal 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
|
83
ems/src/api/tiles/index.ts
Normal file
83
ems/src/api/tiles/index.ts
Normal 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
7
ems/src/constants/db.ts
Normal 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
Reference in New Issue
Block a user