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