init commit
This commit is contained in:
commit
99faa78434
25 changed files with 895 additions and 0 deletions
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
.env
|
||||
.venv
|
||||
poetry.lock
|
||||
alembic/versions/
|
||||
unloading.db
|
||||
|
||||
# Backend
|
||||
backend/backend_transfer_tdms_tcs/__pycache__/
|
||||
backend/backend_transfer_tdms_tcs/*.db
|
||||
backend/backend_transfer_tdms_tcs/alembic/versions/
|
||||
|
||||
# Frontend
|
||||
frontend/node_modules/
|
||||
frontend/.next/
|
||||
frontend/out/
|
||||
frontend/build
|
||||
frontend/.env.local
|
||||
frontend/.env.*.local
|
||||
frontend/next-env.d.ts
|
||||
frontend/package-lock.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
0
backend/backend_transfer_tdms_tcs/README.md
Normal file
0
backend/backend_transfer_tdms_tcs/README.md
Normal file
46
backend/backend_transfer_tdms_tcs/alembic.ini
Normal file
46
backend/backend_transfer_tdms_tcs/alembic.ini
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
[alembic]
|
||||
script_location = alembic
|
||||
prepend_sys_path = .
|
||||
version_path_separator = os
|
||||
|
||||
sqlalchemy.url = sqlite:///./unloading.db
|
||||
|
||||
[post_write_hooks]
|
||||
hooks = black
|
||||
black.type = console_scripts
|
||||
black.entrypoint = black
|
||||
black.options = -l 88
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
0
backend/backend_transfer_tdms_tcs/app/__init__.py
Normal file
0
backend/backend_transfer_tdms_tcs/app/__init__.py
Normal file
83
backend/backend_transfer_tdms_tcs/app/admin.py
Normal file
83
backend/backend_transfer_tdms_tcs/app/admin.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from sqladmin import Admin, ModelView
|
||||
from sqladmin.authentication import AuthenticationBackend
|
||||
from fastapi import Request
|
||||
from wtforms import TextAreaField
|
||||
from wtforms.widgets import TextArea
|
||||
|
||||
from .database import engine
|
||||
from .models import Unloading, Product, UnloadCheck, TestLoad, TestCheck, ProdTransfer
|
||||
|
||||
class JSONTextAreaWidget(TextArea):
|
||||
def __call__(self, field, **kwargs):
|
||||
kwargs["class"] = "json-textarea"
|
||||
return super().__call__(field, **kwargs)
|
||||
|
||||
class UnloadingAdmin(ModelView, model=Unloading):
|
||||
column_list = [Unloading.guid, Unloading.name, Unloading.created_at, Unloading.computer_name]
|
||||
column_searchable_list = [Unloading.name, Unloading.guid, Unloading.computer_name]
|
||||
column_sortable_list = [Unloading.created_at, Unloading.unloading_time_seconds]
|
||||
form_overrides = {"warnings": TextAreaField}
|
||||
form_widget_args = {"warnings": {"widget": JSONTextAreaWidget(), "rows": 5}}
|
||||
|
||||
can_create = True
|
||||
can_edit = True
|
||||
can_delete = True
|
||||
can_view_details = True
|
||||
|
||||
class ProductAdmin(ModelView, model=Product):
|
||||
column_list = [Product.guid, Product.name]
|
||||
column_searchable_list = [Product.name, Product.guid]
|
||||
|
||||
can_create = False # Продукты создаются через триггер
|
||||
can_edit = True
|
||||
can_delete = False
|
||||
can_view_details = True
|
||||
|
||||
class UnloadCheckAdmin(ModelView, model=UnloadCheck):
|
||||
column_list = [UnloadCheck.id, UnloadCheck.product_guid, UnloadCheck.fio, UnloadCheck.date, UnloadCheck.is_completed]
|
||||
column_searchable_list = [UnloadCheck.fio, UnloadCheck.product_guid]
|
||||
|
||||
class TestLoadAdmin(ModelView, model=TestLoad):
|
||||
column_list = [TestLoad.id, TestLoad.product_guid, TestLoad.fio, TestLoad.date, TestLoad.is_completed]
|
||||
|
||||
class TestCheckAdmin(ModelView, model=TestCheck):
|
||||
column_list = [TestCheck.id, TestCheck.product_guid, TestCheck.fio, TestCheck.date, TestCheck.is_completed]
|
||||
|
||||
class ProdTransferAdmin(ModelView, model=ProdTransfer):
|
||||
column_list = [ProdTransfer.id, ProdTransfer.product_guid, ProdTransfer.fio, ProdTransfer.date, ProdTransfer.is_completed]
|
||||
|
||||
# Простая аутентификация (можно заменить на более сложную)
|
||||
class AdminAuth(AuthenticationBackend):
|
||||
async def login(self, request: Request) -> bool:
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
|
||||
# Простая проверка (в production используйте безопасную аутентификацию)
|
||||
if username == "admin" and password == "admin":
|
||||
request.session.update({"token": "admin-token"})
|
||||
return True
|
||||
return False
|
||||
|
||||
async def logout(self, request: Request) -> bool:
|
||||
request.session.clear()
|
||||
return True
|
||||
|
||||
async def authenticate(self, request: Request) -> bool:
|
||||
token = request.session.get("token")
|
||||
if token == "admin-token":
|
||||
return True
|
||||
return False
|
||||
|
||||
def setup_admin(app):
|
||||
authentication_backend = AdminAuth(secret_key="secret-key")
|
||||
admin = Admin(app=app, engine=engine, authentication_backend=authentication_backend)
|
||||
|
||||
admin.add_view(UnloadingAdmin)
|
||||
admin.add_view(ProductAdmin)
|
||||
admin.add_view(UnloadCheckAdmin)
|
||||
admin.add_view(TestLoadAdmin)
|
||||
admin.add_view(TestCheckAdmin)
|
||||
admin.add_view(ProdTransferAdmin)
|
||||
|
||||
return admin
|
||||
75
backend/backend_transfer_tdms_tcs/app/crud.py
Normal file
75
backend/backend_transfer_tdms_tcs/app/crud.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from sqlalchemy.orm import Session
|
||||
from . import models, schemas
|
||||
from typing import Optional, List
|
||||
import uuid
|
||||
|
||||
def create_unloading(db: Session, unloading: schemas.UnloadingCreate):
|
||||
db_unloading = models.Unloading(
|
||||
guid=unloading.guid or str(uuid.uuid4()).upper(),
|
||||
name=unloading.name,
|
||||
unloading_time_seconds=unloading.unloading_time_seconds,
|
||||
warnings=unloading.warnings,
|
||||
row_count=unloading.row_count,
|
||||
file_count=unloading.file_count,
|
||||
computer_name=unloading.computer_name
|
||||
)
|
||||
db.add(db_unloading)
|
||||
db.commit()
|
||||
db.refresh(db_unloading)
|
||||
return db_unloading
|
||||
|
||||
def get_unloadings(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(models.Unloading).offset(skip).limit(limit).all()
|
||||
|
||||
def get_unloading(db: Session, guid: str):
|
||||
return db.query(models.Unloading).filter(models.Unloading.guid == guid).first()
|
||||
|
||||
def get_products(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(models.Product).offset(skip).limit(limit).all()
|
||||
|
||||
def get_product(db: Session, guid: str):
|
||||
return db.query(models.Product).filter(models.Product.guid == guid).first()
|
||||
|
||||
def add_unload_check(db: Session, product_guid: str, stage: schemas.UnloadCheckCreate):
|
||||
db_stage = models.UnloadCheck(
|
||||
product_guid=product_guid,
|
||||
fio=stage.fio,
|
||||
is_completed=stage.is_completed
|
||||
)
|
||||
db.add(db_stage)
|
||||
db.commit()
|
||||
db.refresh(db_stage)
|
||||
return db_stage
|
||||
|
||||
def add_test_load(db: Session, product_guid: str, stage: schemas.TestLoadCreate):
|
||||
db_stage = models.TestLoad(
|
||||
product_guid=product_guid,
|
||||
fio=stage.fio,
|
||||
is_completed=stage.is_completed
|
||||
)
|
||||
db.add(db_stage)
|
||||
db.commit()
|
||||
db.refresh(db_stage)
|
||||
return db_stage
|
||||
|
||||
def add_test_check(db: Session, product_guid: str, stage: schemas.TestCheckCreate):
|
||||
db_stage = models.TestCheck(
|
||||
product_guid=product_guid,
|
||||
fio=stage.fio,
|
||||
is_completed=stage.is_completed
|
||||
)
|
||||
db.add(db_stage)
|
||||
db.commit()
|
||||
db.refresh(db_stage)
|
||||
return db_stage
|
||||
|
||||
def add_prod_transfer(db: Session, product_guid: str, stage: schemas.ProdTransferCreate):
|
||||
db_stage = models.ProdTransfer(
|
||||
product_guid=product_guid,
|
||||
fio=stage.fio,
|
||||
is_completed=stage.is_completed
|
||||
)
|
||||
db.add(db_stage)
|
||||
db.commit()
|
||||
db.refresh(db_stage)
|
||||
return db_stage
|
||||
23
backend/backend_transfer_tdms_tcs/app/database.py
Normal file
23
backend/backend_transfer_tdms_tcs/app/database.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from sqlalchemy import create_engine, event, DDL
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./unloading.db"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
72
backend/backend_transfer_tdms_tcs/app/main.py
Normal file
72
backend/backend_transfer_tdms_tcs/app/main.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from contextlib import asynccontextmanager # ДОБАВЬТЕ ЭТОТ ИМПОРТ!
|
||||
from fastapi import FastAPI, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
|
||||
from .database import engine, Base, get_db
|
||||
from .models import *
|
||||
from .routers import unloadings, products
|
||||
from .admin import setup_admin
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Создание таблиц при старте
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Создание триггера
|
||||
# Создание триггера
|
||||
from sqlalchemy import text
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("""
|
||||
CREATE TRIGGER IF NOT EXISTS insert_product_from_unloading
|
||||
AFTER INSERT ON unloading
|
||||
FOR EACH ROW
|
||||
WHEN NOT EXISTS (SELECT 1 FROM product WHERE guid = NEW.guid)
|
||||
BEGIN
|
||||
INSERT INTO product (guid, name)
|
||||
VALUES (NEW.guid, NEW.name);
|
||||
|
||||
-- Обновляем product_guid после создания продукта
|
||||
UPDATE unloading
|
||||
SET product_guid = NEW.guid
|
||||
WHERE guid = NEW.guid;
|
||||
END;
|
||||
"""))
|
||||
|
||||
yield
|
||||
|
||||
# Очистка при завершении
|
||||
pass
|
||||
|
||||
app = FastAPI(
|
||||
title="Unloading System API",
|
||||
description="Система управления выгрузками и продуктами",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# Настройка админки
|
||||
admin = setup_admin(app)
|
||||
|
||||
# Подключение роутеров
|
||||
app.include_router(unloadings.router)
|
||||
app.include_router(products.router)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "Unloading System API",
|
||||
"docs": "/docs",
|
||||
"admin": "/admin"
|
||||
}
|
||||
|
||||
@app.get("/test-db")
|
||||
def test_db(db: Session = Depends(get_db)):
|
||||
from sqlalchemy import text
|
||||
result = db.execute(text("SELECT name FROM sqlite_master WHERE type='table'"))
|
||||
tables = [row[0] for row in result]
|
||||
return {"tables": tables}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
65
backend/backend_transfer_tdms_tcs/app/models.py
Normal file
65
backend/backend_transfer_tdms_tcs/app/models.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from sqlalchemy import Column, String, Integer, DateTime, JSON, ForeignKey, Boolean
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship, declared_attr
|
||||
import uuid
|
||||
from .database import Base
|
||||
|
||||
class Unloading(Base):
|
||||
__tablename__ = "unloading"
|
||||
|
||||
guid = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()).upper())
|
||||
name = Column(String(500), nullable=False)
|
||||
unloading_time_seconds = Column(Integer, nullable=False)
|
||||
warnings = Column(JSON, default={})
|
||||
row_count = Column(Integer, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
file_count = Column(Integer, nullable=False)
|
||||
computer_name = Column(String(100), nullable=True)
|
||||
|
||||
# ДОБАВЬТЕ ЭТОТ ВНЕШНИЙ КЛЮЧ
|
||||
product_guid = Column(String(36), ForeignKey("product.guid"), unique=True)
|
||||
|
||||
# Исправленная связь
|
||||
product = relationship("Product", back_populates="unloading", uselist=False)
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = "product"
|
||||
|
||||
guid = Column(String(36), primary_key=True, index=True)
|
||||
name = Column(String(500), nullable=False)
|
||||
|
||||
# Связь остается без изменений
|
||||
unloading = relationship("Unloading", back_populates="product", uselist=False)
|
||||
|
||||
unload_checks = relationship("UnloadCheck", back_populates="product", cascade="all, delete-orphan")
|
||||
test_loads = relationship("TestLoad", back_populates="product", cascade="all, delete-orphan")
|
||||
test_checks = relationship("TestCheck", back_populates="product", cascade="all, delete-orphan")
|
||||
prod_transfers = relationship("ProdTransfer", back_populates="product", cascade="all, delete-orphan")
|
||||
|
||||
class BaseStage(Base):
|
||||
__abstract__ = True
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
fio = Column(String(200), nullable=False)
|
||||
date = Column(DateTime(timezone=True), server_default=func.now())
|
||||
is_completed = Column(Boolean, default=False)
|
||||
|
||||
@declared_attr
|
||||
def product_guid(cls):
|
||||
return Column(String(36), ForeignKey("product.guid"))
|
||||
|
||||
class UnloadCheck(BaseStage):
|
||||
__tablename__ = "unload_check"
|
||||
product = relationship("Product", back_populates="unload_checks")
|
||||
|
||||
class TestLoad(BaseStage):
|
||||
__tablename__ = "test_load"
|
||||
product = relationship("Product", back_populates="test_loads")
|
||||
|
||||
class TestCheck(BaseStage):
|
||||
__tablename__ = "test_check"
|
||||
product = relationship("Product", back_populates="test_checks")
|
||||
|
||||
class ProdTransfer(BaseStage):
|
||||
__tablename__ = "prod_transfer"
|
||||
product = relationship("Product", back_populates="prod_transfers")
|
||||
34
backend/backend_transfer_tdms_tcs/app/routers/products.py
Normal file
34
backend/backend_transfer_tdms_tcs/app/routers/products.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from .. import crud, schemas
|
||||
from ..database import get_db
|
||||
|
||||
router = APIRouter(prefix="/products", tags=["products"])
|
||||
|
||||
@router.get("/", response_model=List[schemas.Product])
|
||||
def read_products(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
return crud.get_products(db, skip=skip, limit=limit)
|
||||
|
||||
@router.get("/{guid}", response_model=schemas.Product)
|
||||
def read_product(guid: str, db: Session = Depends(get_db)):
|
||||
db_product = crud.get_product(db, guid=guid)
|
||||
if db_product is None:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
return db_product
|
||||
|
||||
@router.post("/{guid}/unload-check/", response_model=schemas.Stage)
|
||||
def create_unload_check(guid: str, stage: schemas.UnloadCheckCreate, db: Session = Depends(get_db)):
|
||||
return crud.add_unload_check(db, product_guid=guid, stage=stage)
|
||||
|
||||
@router.post("/{guid}/test-load/", response_model=schemas.Stage)
|
||||
def create_test_load(guid: str, stage: schemas.TestLoadCreate, db: Session = Depends(get_db)):
|
||||
return crud.add_test_load(db, product_guid=guid, stage=stage)
|
||||
|
||||
@router.post("/{guid}/test-check/", response_model=schemas.Stage)
|
||||
def create_test_check(guid: str, stage: schemas.TestCheckCreate, db: Session = Depends(get_db)):
|
||||
return crud.add_test_check(db, product_guid=guid, stage=stage)
|
||||
|
||||
@router.post("/{guid}/prod-transfer/", response_model=schemas.Stage)
|
||||
def create_prod_transfer(guid: str, stage: schemas.ProdTransferCreate, db: Session = Depends(get_db)):
|
||||
return crud.add_prod_transfer(db, product_guid=guid, stage=stage)
|
||||
23
backend/backend_transfer_tdms_tcs/app/routers/unloadings.py
Normal file
23
backend/backend_transfer_tdms_tcs/app/routers/unloadings.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from .. import crud, schemas
|
||||
from ..database import get_db
|
||||
|
||||
router = APIRouter(prefix="/unloadings", tags=["unloadings"])
|
||||
|
||||
@router.post("/", response_model=schemas.Unloading)
|
||||
def create_unloading(unloading: schemas.UnloadingCreate, db: Session = Depends(get_db)):
|
||||
return crud.create_unloading(db=db, unloading=unloading)
|
||||
|
||||
@router.get("/", response_model=List[schemas.Unloading])
|
||||
def read_unloadings(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
unloadings = crud.get_unloadings(db, skip=skip, limit=limit)
|
||||
return unloadings
|
||||
|
||||
@router.get("/{guid}", response_model=schemas.Unloading)
|
||||
def read_unloading(guid: str, db: Session = Depends(get_db)):
|
||||
db_unloading = crud.get_unloading(db, guid=guid)
|
||||
if db_unloading is None:
|
||||
raise HTTPException(status_code=404, detail="Unloading not found")
|
||||
return db_unloading
|
||||
59
backend/backend_transfer_tdms_tcs/app/schemas.py
Normal file
59
backend/backend_transfer_tdms_tcs/app/schemas.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, List
|
||||
import uuid
|
||||
|
||||
class UnloadingBase(BaseModel):
|
||||
name: str
|
||||
unloading_time_seconds: int
|
||||
warnings: Optional[Dict] = {}
|
||||
row_count: int
|
||||
file_count: int
|
||||
computer_name: Optional[str] = None
|
||||
|
||||
class UnloadingCreate(UnloadingBase):
|
||||
guid: Optional[str] = None
|
||||
|
||||
class Unloading(UnloadingBase):
|
||||
guid: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class StageBase(BaseModel):
|
||||
fio: str
|
||||
is_completed: bool = False
|
||||
|
||||
class UnloadCheckCreate(StageBase):
|
||||
pass
|
||||
|
||||
class TestLoadCreate(StageBase):
|
||||
pass
|
||||
|
||||
class TestCheckCreate(StageBase):
|
||||
pass
|
||||
|
||||
class ProdTransferCreate(StageBase):
|
||||
pass
|
||||
|
||||
class Stage(StageBase):
|
||||
id: int
|
||||
date: datetime
|
||||
product_guid: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ProductBase(BaseModel):
|
||||
name: str
|
||||
|
||||
class Product(ProductBase):
|
||||
guid: str
|
||||
unload_checks: List[Stage] = []
|
||||
test_loads: List[Stage] = []
|
||||
test_checks: List[Stage] = []
|
||||
prod_transfers: List[Stage] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
26
backend/backend_transfer_tdms_tcs/pyproject.toml
Normal file
26
backend/backend_transfer_tdms_tcs/pyproject.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[tool.poetry]
|
||||
name = "fastapi-unloading-app"
|
||||
version = "0.1.0"
|
||||
package-mode = false
|
||||
description = "FastAPI application for unloading management"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
fastapi = "^0.104.0"
|
||||
sqlalchemy = "1.4.42"
|
||||
databases = "0.8.0"
|
||||
aiosqlite = "^0.19.0"
|
||||
sqladmin = "^0.15.0"
|
||||
jinja2 = "^3.1.0"
|
||||
python-multipart = "^0.0.6"
|
||||
uvicorn = {extras = ["standard"], version = "^0.24.0"}
|
||||
itsdangerous = "^2.2.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
alembic = "^1.12.0"
|
||||
black = "^23.11.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
0
backend/backend_transfer_tdms_tcs/tests/__init__.py
Normal file
0
backend/backend_transfer_tdms_tcs/tests/__init__.py
Normal file
1
frontend/.gitignore
vendored
Normal file
1
frontend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
21
frontend/app/layout.tsx
Normal file
21
frontend/app/layout.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// src/app/layout.tsx
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Provider } from '@/components/ui/provider'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'My Chakra App',
|
||||
description: 'Приложение на Next.js с Chakra UI',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<Provider>{children}</Provider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
28
frontend/app/page.tsx
Normal file
28
frontend/app/page.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// src/app/page.tsx
|
||||
import { Button, Container, Heading, Text, VStack } from '@chakra-ui/react'
|
||||
import LineBox from '@/components/LineBox'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Container maxW="100%" py={1} px={3}>
|
||||
<VStack spacing={1} align="stretch">
|
||||
<Heading textAlign="center" color="teal.600">
|
||||
Добро пожаловать в Chakra UI + Next.js
|
||||
</Heading>
|
||||
{/* <Text fontSize="xl" textAlign="center">
|
||||
Приложение настроено по официальной документации
|
||||
</Text> */}
|
||||
{/* <Button colorScheme="teal" size="lg" alignSelf="center">
|
||||
Начать работу
|
||||
</Button> */}
|
||||
<LineBox
|
||||
title="Мой первый компонент"
|
||||
description="Это переиспользуемый компонент с настраиваемыми пропсами"
|
||||
buttonText="Нажми меня"
|
||||
//onButtonClick={handleClick}
|
||||
/>
|
||||
|
||||
</VStack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^3.32.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"framer-motion": "^12.31.0",
|
||||
"next": "^16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-icons": "^5.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack",
|
||||
"build": "next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "25.2.0",
|
||||
"@types/react": "19.2.11",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
54
frontend/src/components/LineBox.tsx
Normal file
54
frontend/src/components/LineBox.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// src/components/CustomBox.tsx
|
||||
import { Box, BoxProps, Heading, Text, Button, HStack } from '@chakra-ui/react'
|
||||
|
||||
interface CustomBoxProps extends BoxProps {
|
||||
title?: string
|
||||
description?: string
|
||||
buttonText?: string
|
||||
onButtonClick?: () => void
|
||||
}
|
||||
|
||||
export default function CustomBox({
|
||||
title = "Заголовок по умолчанию",
|
||||
description = "Описание по умолчанию",
|
||||
buttonText = "Кнопка",
|
||||
onButtonClick,
|
||||
children,
|
||||
...rest
|
||||
}: CustomBoxProps) {
|
||||
return (
|
||||
<Box
|
||||
p={4}
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
boxShadow="lg"
|
||||
bg="white"
|
||||
width="100%"
|
||||
_dark={{ bg: "gray.700" }}
|
||||
{...rest}
|
||||
>
|
||||
<HStack spacing={4} align="stretch">
|
||||
{description && (
|
||||
<Text fontSize="md" color="gray.600" _dark={{ color: "gray.300" }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
<Text fontSize="md" color="gray.600" _dark={{ color: "gray.300" }}>test</Text>
|
||||
|
||||
{/* Дочерние элементы (если переданы) */}
|
||||
{/* {children}
|
||||
|
||||
// {buttonText && (
|
||||
// <Button
|
||||
// colorScheme="teal"
|
||||
// onClick={onButtonClick}
|
||||
// alignSelf="flex-start"
|
||||
// >
|
||||
// {buttonText}
|
||||
// </Button>
|
||||
// )} */}
|
||||
|
||||
</HStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
90
frontend/src/components/ui/color-mode.jsx
Normal file
90
frontend/src/components/ui/color-mode.jsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
'use client'
|
||||
|
||||
import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react'
|
||||
import { ThemeProvider, useTheme } from 'next-themes'
|
||||
|
||||
import * as React from 'react'
|
||||
import { LuMoon, LuSun } from 'react-icons/lu'
|
||||
|
||||
export function ColorModeProvider(props) {
|
||||
return (
|
||||
<ThemeProvider attribute='class' disableTransitionOnChange {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export function useColorMode() {
|
||||
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
|
||||
const colorMode = forcedTheme || resolvedTheme
|
||||
const toggleColorMode = () => {
|
||||
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
return {
|
||||
colorMode: colorMode,
|
||||
setColorMode: setTheme,
|
||||
toggleColorMode,
|
||||
}
|
||||
}
|
||||
|
||||
export function useColorModeValue(light, dark) {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === 'dark' ? dark : light
|
||||
}
|
||||
|
||||
export function ColorModeIcon() {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === 'dark' ? <LuMoon /> : <LuSun />
|
||||
}
|
||||
|
||||
export const ColorModeButton = React.forwardRef(
|
||||
function ColorModeButton(props, ref) {
|
||||
const { toggleColorMode } = useColorMode()
|
||||
return (
|
||||
<ClientOnly fallback={<Skeleton boxSize='9' />}>
|
||||
<IconButton
|
||||
onClick={toggleColorMode}
|
||||
variant='ghost'
|
||||
aria-label='Toggle color mode'
|
||||
size='sm'
|
||||
ref={ref}
|
||||
{...props}
|
||||
css={{
|
||||
_icon: {
|
||||
width: '5',
|
||||
height: '5',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ColorModeIcon />
|
||||
</IconButton>
|
||||
</ClientOnly>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const LightMode = React.forwardRef(function LightMode(props, ref) {
|
||||
return (
|
||||
<Span
|
||||
color='fg'
|
||||
display='contents'
|
||||
className='chakra-theme light'
|
||||
colorPalette='gray'
|
||||
colorScheme='light'
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export const DarkMode = React.forwardRef(function DarkMode(props, ref) {
|
||||
return (
|
||||
<Span
|
||||
color='fg'
|
||||
display='contents'
|
||||
className='chakra-theme dark'
|
||||
colorPalette='gray'
|
||||
colorScheme='dark'
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
12
frontend/src/components/ui/provider.jsx
Normal file
12
frontend/src/components/ui/provider.jsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
'use client'
|
||||
|
||||
import { ChakraProvider, defaultSystem } from '@chakra-ui/react'
|
||||
import { ColorModeProvider } from './color-mode'
|
||||
|
||||
export function Provider(props) {
|
||||
return (
|
||||
<ChakraProvider value={defaultSystem}>
|
||||
<ColorModeProvider {...props} />
|
||||
</ChakraProvider>
|
||||
)
|
||||
}
|
||||
43
frontend/src/components/ui/toaster.jsx
Normal file
43
frontend/src/components/ui/toaster.jsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
'use client'
|
||||
|
||||
import {
|
||||
Toaster as ChakraToaster,
|
||||
Portal,
|
||||
Spinner,
|
||||
Stack,
|
||||
Toast,
|
||||
createToaster,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
export const toaster = createToaster({
|
||||
placement: 'bottom-end',
|
||||
pauseOnPageIdle: true,
|
||||
})
|
||||
|
||||
export const Toaster = () => {
|
||||
return (
|
||||
<Portal>
|
||||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
|
||||
{(toast) => (
|
||||
<Toast.Root width={{ md: 'sm' }}>
|
||||
{toast.type === 'loading' ? (
|
||||
<Spinner size='sm' color='blue.solid' />
|
||||
) : (
|
||||
<Toast.Indicator />
|
||||
)}
|
||||
<Stack gap='1' flex='1' maxWidth='100%'>
|
||||
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||
{toast.description && (
|
||||
<Toast.Description>{toast.description}</Toast.Description>
|
||||
)}
|
||||
</Stack>
|
||||
{toast.action && (
|
||||
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
||||
)}
|
||||
{toast.closable && <Toast.CloseTrigger />}
|
||||
</Toast.Root>
|
||||
)}
|
||||
</ChakraToaster>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
35
frontend/src/components/ui/tooltip.jsx
Normal file
35
frontend/src/components/ui/tooltip.jsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react'
|
||||
import * as React from 'react'
|
||||
|
||||
export const Tooltip = React.forwardRef(function Tooltip(props, ref) {
|
||||
const {
|
||||
showArrow,
|
||||
children,
|
||||
disabled,
|
||||
portalled = true,
|
||||
content,
|
||||
contentProps,
|
||||
portalRef,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
if (disabled) return children
|
||||
|
||||
return (
|
||||
<ChakraTooltip.Root {...rest}>
|
||||
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraTooltip.Positioner>
|
||||
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||
{showArrow && (
|
||||
<ChakraTooltip.Arrow>
|
||||
<ChakraTooltip.ArrowTip />
|
||||
</ChakraTooltip.Arrow>
|
||||
)}
|
||||
{content}
|
||||
</ChakraTooltip.Content>
|
||||
</ChakraTooltip.Positioner>
|
||||
</Portal>
|
||||
</ChakraTooltip.Root>
|
||||
)
|
||||
})
|
||||
42
frontend/tsconfig.json
Normal file
42
frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext", // ← ЗАМЕНИТЬ es5 на ESNext
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "ESNext", // ← ЗАМЕНИТЬ esnext на ESNext (капитализация)
|
||||
"moduleResolution": "Bundler", // ← ЗАМЕНИТЬ node на Bundler
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
] // ← ЕСЛИ используете src/ структуру
|
||||
// или "@/": ["./*"] если без src/
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue