Compare commits
53 Commits
c2f3f10c9b
...
feature/fr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e428e7f762 | ||
|
|
3f91dc91ec | ||
|
|
779c256e7b | ||
|
|
d686b26465 | ||
|
|
08e979ecb2 | ||
|
|
72f1d53051 | ||
|
|
b1e0693131 | ||
|
|
86713fc75f | ||
|
|
c24a1fa8c6 | ||
|
|
f108e013c2 | ||
| 1c33775f92 | |||
| 43ab114e1a | |||
| 946ad5c31f | |||
|
|
7b956d89bf | ||
| 0891abc0e1 | |||
| 08393f6685 | |||
| 9cde4e2c7d | |||
|
|
0a60a16344 | ||
|
|
496ef3fa9d | ||
|
|
3494e5d17c | ||
|
|
29b0070260 | ||
|
|
2e9cc918d5 | ||
|
|
d943894ae8 | ||
|
|
1c901ddb00 | ||
|
|
f9a188c927 | ||
|
|
1fce4b41c0 | ||
|
|
7ccbba06c1 | ||
|
|
63115f0328 | ||
| cb862c63a6 | |||
| 927d8d75a9 | |||
| 509a3ee913 | |||
| 86f0f9d977 | |||
| 0c4bf4b9fd | |||
| d71945ecb2 | |||
| 3cd5fe63b5 | |||
| 0c6001b297 | |||
| 682b6c4ffd | |||
| 9c859d4660 | |||
|
|
f6b720e4eb | ||
|
|
2fda39a449 | ||
| 291e5d0e25 | |||
| 45d8dfd870 | |||
|
|
4c6166c907 | ||
|
|
2c51bdc695 | ||
| 87c4ebe33e | |||
|
|
bf93bf0fdb | ||
|
|
027b1dc855 | ||
| 4a47746e9d | |||
| 12a63278f7 | |||
|
|
965695a693 | ||
|
|
b47e9391ba | ||
|
|
5259dc5292 | ||
|
|
a6eea28071 |
25
.gitignore
vendored
25
.gitignore
vendored
@@ -1,16 +1,21 @@
|
||||
*.csv
|
||||
.vscode
|
||||
instance
|
||||
.idea
|
||||
venv/
|
||||
instance/
|
||||
|
||||
__pychache__/
|
||||
123
|
||||
|
||||
|
||||
zabalans.csv
|
||||
project.db
|
||||
# Ignore Python bytecode caches everywhere
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
|
||||
# Migrations and DB files
|
||||
migrations/
|
||||
*.db
|
||||
|
||||
# Data and temp files
|
||||
*.csv
|
||||
c*.txt
|
||||
__pycache__/app.cpython-312.pyc
|
||||
__pycache__/models.cpython-312.pyc
|
||||
|
||||
# Legacy specific ignores (if present)
|
||||
backend/venv
|
||||
backend/__pycache__
|
||||
backend/routeres/__pycache__
|
||||
|
||||
BIN
VedomostMOL.xls
Normal file
BIN
VedomostMOL.xls
Normal file
Binary file not shown.
BIN
VedomostMOL.xlsx
Normal file
BIN
VedomostMOL.xlsx
Normal file
Binary file not shown.
132
app.py
132
app.py
@@ -8,7 +8,6 @@ import csv
|
||||
import random
|
||||
from urllib.parse import unquote
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///project.db"
|
||||
@@ -22,6 +21,12 @@ db.init_app(app)
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
|
||||
|
||||
@app.route("/login", methods=['GET', 'POST'])
|
||||
def login():
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@app.route("/", methods=['GET', 'POST'])
|
||||
def index():
|
||||
results = []
|
||||
@@ -119,26 +124,33 @@ def alloborud():
|
||||
|
||||
@app.route('/getall')
|
||||
def getall():
|
||||
oboruds = Oboruds.query.all()
|
||||
oboruds_json = [{
|
||||
'id': oborud.id,
|
||||
'invNumber': oborud.invNumber,
|
||||
'nazvanie': oborud.nazvanie,
|
||||
'raspologenie': oborud.raspologenie,
|
||||
'numberved': oborud.numberved,
|
||||
'balancenumber': oborud.balancenumber,
|
||||
'kolichestvo': oborud.kolichestvo,
|
||||
'balancenumber': oborud.balancenumber,
|
||||
'aud_id': oborud.aud_id
|
||||
} for oborud in oboruds]
|
||||
oborud = db.session.query(Oboruds).order_by(Oboruds.invNumber).all()
|
||||
|
||||
results = []
|
||||
for oboruds in oborud:
|
||||
if oboruds.aud_id is None:
|
||||
results.append({
|
||||
'invNumber': oboruds.invNumber,
|
||||
'nazvanie': oboruds.nazvanie,
|
||||
'raspologenie': oboruds.raspologenie,
|
||||
'balancenumber': oboruds.balancenumber,
|
||||
'kolichestvo': oboruds.kolichestvo,
|
||||
'numberppasu': oboruds.numberppasu,
|
||||
'numberved': oboruds.numberved,
|
||||
'aud': ""})
|
||||
else:
|
||||
aud = db.session.query(Auditory).filter_by(id=oboruds.aud_id).first()
|
||||
results.append({
|
||||
'invNumber': oboruds.invNumber,
|
||||
'nazvanie': oboruds.nazvanie,
|
||||
'raspologenie': oboruds.raspologenie,
|
||||
'balancenumber': oboruds.balancenumber,
|
||||
'kolichestvo': oboruds.kolichestvo,
|
||||
'numberppasu': oboruds.numberppasu,
|
||||
'numberved': oboruds.numberved,
|
||||
'aud': aud.audnazvanie})
|
||||
|
||||
return jsonify(oboruds_json)
|
||||
|
||||
@app.route('/updateduplicate', methods=['GET', 'POST'])
|
||||
def updateduplicate():
|
||||
if request.method == 'POST':
|
||||
aud = request.form.get('auditory_dubl')
|
||||
return jsonify(results)
|
||||
|
||||
|
||||
@app.route('/vneaud', methods=['GET', 'POST'])
|
||||
@@ -149,7 +161,7 @@ def vneaud():
|
||||
ak = db.session.query(Oboruds).all()
|
||||
|
||||
for dt in data:
|
||||
res.append([dt.invNumber, dt.nazvanie, dt.typeBalanse])
|
||||
res.append([dt.invNumber, dt.nazvanie])
|
||||
|
||||
return render_template('vneaud.html', res=res, kolvo=len(data), all_kol=len(ak))
|
||||
|
||||
@@ -192,33 +204,52 @@ def zamsearch():
|
||||
@app.route('/addraspved', methods=['GET', 'POST'])
|
||||
def addraspved():
|
||||
if request.method == 'POST':
|
||||
|
||||
query_string = request.data.decode()
|
||||
|
||||
print(query_string)
|
||||
un_query_string = unquote(unquote(query_string)).split(',')
|
||||
|
||||
print(un_query_string)
|
||||
|
||||
|
||||
|
||||
ob = db.session.query(Oboruds).filter_by(invNumber=un_query_string[0]).first()
|
||||
ob.raspologenie=un_query_string[2]
|
||||
|
||||
ob.numberved = un_query_string[1]
|
||||
ob.kolichestvo = un_query_string[2]
|
||||
ob.balancenumber = un_query_string[3]
|
||||
ob.raspologenie = un_query_string[4]
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return jsonify({'success': True}, 200, {'ContentType': 'application/json'})
|
||||
|
||||
|
||||
@app.route('/addoborudasu', methods=['GET', 'POST'])
|
||||
def addoborud():
|
||||
if request.method == 'POST':
|
||||
query_string = request.data.decode()
|
||||
|
||||
un_query_string = unquote(unquote(query_string)).split(',')
|
||||
db.session.add(
|
||||
Oboruds(invNumber=un_query_string[0],
|
||||
nazvanie=un_query_string[5],
|
||||
raspologenie=un_query_string[4],
|
||||
numberved=un_query_string[1],
|
||||
kolichestvo=un_query_string[2],
|
||||
balancenumber=un_query_string[3]
|
||||
)
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True}, 200, {'ContentType': 'application/json'})
|
||||
|
||||
|
||||
# ==================================================================================
|
||||
|
||||
|
||||
def ranomraspr():
|
||||
with app.app_context():
|
||||
while len(db.session.query(Oboruds).filter(Oboruds.aud_id == None).all()) > 0:
|
||||
while len(db.session.query(Oboruds).filter(Oboruds.aud_id is None).all()) > 0:
|
||||
audid = random.choice(db.session.query(Auditory).all())
|
||||
oborud = random.choice(db.session.query(Oboruds).filter(Oboruds.aud_id == None).all())
|
||||
oborud = random.choice(db.session.query(Oboruds).filter(Oboruds.aud_id is None).all())
|
||||
oborud.aud_id = audid.id
|
||||
db.session.commit()
|
||||
|
||||
@@ -266,9 +297,48 @@ def createdb():
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def write2excell():
|
||||
wb = xlrd.open_workbook("VedIsh.xls")
|
||||
sheet = wb.sheet_by_index(0)
|
||||
|
||||
newFile = copy(wb)
|
||||
newSheet = newFile.get_sheet(0)
|
||||
invNomerColum = 6
|
||||
|
||||
column_index = 1
|
||||
for row_idx in range(sheet.nrows):
|
||||
cell_value = sheet.cell(row_idx, column_index)
|
||||
|
||||
if cell_value:
|
||||
|
||||
tmp_inv_number = str(cell_value).split(':')[1]
|
||||
|
||||
try:
|
||||
a = tmp_inv_number[1:-1]
|
||||
|
||||
inv_number = int(tmp_inv_number[1:-1])
|
||||
|
||||
with app.app_context():
|
||||
auditory_obj = db.session.query(Auditory).join(Oboruds, Oboruds.aud_id == Auditory.id).filter(
|
||||
Oboruds.invNumber == inv_number).first()
|
||||
|
||||
print(auditory_obj.audnazvanie)
|
||||
|
||||
# cur.execute("SELECT aud.audnazvanie FROM oboruds AS ob JOIN auditory AS aud ON ob.aud_id = aud.id WHERE ob.invNumber = ?", (inv_number,))
|
||||
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
"""
|
||||
else:
|
||||
#newSheet.write(row_idx, invNomerColum, "Нет инв номера")
|
||||
pass
|
||||
newFile.save("Ved31.xls")
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
#write2excell()
|
||||
|
||||
#ranomraspr()
|
||||
#createdb()
|
||||
app.run(debug=True, host='0.0.0.0', port='3800')
|
||||
|
||||
2
backend/__init__.py
Normal file
2
backend/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Backend package initializer."""
|
||||
|
||||
26
backend/create_new_db.py
Normal file
26
backend/create_new_db.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from backend.models import Base, User
|
||||
from backend.database import engine
|
||||
from backend.security import get_password_hash
|
||||
|
||||
|
||||
def recreate_db_with_admin():
|
||||
# Drop and recreate all tables
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Seed default admin user (username: admin, password: admin)
|
||||
Session = sessionmaker(bind=engine)
|
||||
db = Session()
|
||||
try:
|
||||
if not db.query(User).filter(User.username == 'admin').first():
|
||||
db.add(User(username='admin', password_hash=get_password_hash('admin'), role='admin'))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
recreate_db_with_admin()
|
||||
|
||||
15
backend/database.py
Normal file
15
backend/database.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# backend/database.py
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./app.db" # или PostgreSQL URL
|
||||
|
||||
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
SessionLocal = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
56
backend/main.py
Normal file
56
backend/main.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# backend/main.py
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import RedirectResponse
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from backend.routers.equipment_types import equipment_types
|
||||
from backend.routers.auditories import auditories
|
||||
from backend.routers.oboruds import oboruds
|
||||
from backend.routers.components import components
|
||||
from backend.routers.rashodniki import consumables
|
||||
from backend.routers.zametki import zametki
|
||||
from backend.routers.auth import auth
|
||||
from backend.routers.owners import owners
|
||||
|
||||
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Для фронтенда Vue.js
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # заменить на ['http://localhost:5173'] для безопасности
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/ping")
|
||||
def ping():
|
||||
return {"message": "pong"}
|
||||
|
||||
|
||||
# Serve static assets and frontend
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
app.mount("/app", StaticFiles(directory="frontend", html=True), name="frontend")
|
||||
|
||||
@app.get("/")
|
||||
def root():
|
||||
return RedirectResponse(url="/app/")
|
||||
|
||||
@app.get("/login")
|
||||
def login_page():
|
||||
return RedirectResponse(url="/app/login.html")
|
||||
|
||||
|
||||
# Подключение роутов
|
||||
app.include_router(equipment_types)
|
||||
app.include_router(auditories)
|
||||
app.include_router(oboruds)
|
||||
app.include_router(components)
|
||||
app.include_router(consumables)
|
||||
app.include_router(zametki)
|
||||
app.include_router(auth)
|
||||
app.include_router(owners)
|
||||
89
backend/migrate_data.py
Normal file
89
backend/migrate_data.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import sys
|
||||
from sqlalchemy import create_engine, text, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from backend.database import SessionLocal as NewSession, engine as new_engine
|
||||
from backend import models
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
OLD_DB_URL = "sqlite:///./instance/project.-10-11-25.db"
|
||||
old_engine = create_engine(OLD_DB_URL, connect_args={"check_same_thread": False})
|
||||
OldSession = sessionmaker(bind=old_engine)
|
||||
old_db = OldSession()
|
||||
|
||||
new_db = NewSession()
|
||||
|
||||
def log(msg: str):
|
||||
print(f"[INFO] {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def ensure_schema():
|
||||
"""Ensure new DB has required tables/columns (owners table and oboruds.owner_id)."""
|
||||
# Create any missing tables defined in models (e.g., owners)
|
||||
models.Base.metadata.create_all(bind=new_engine)
|
||||
|
||||
# If oboruds.owner_id is missing (older DB), add the column (SQLite allows simple ALTER)
|
||||
try:
|
||||
inspector = inspect(new_engine)
|
||||
cols = [c["name"] for c in inspector.get_columns("oboruds")]
|
||||
if "owner_id" not in cols:
|
||||
with new_engine.begin() as conn:
|
||||
conn.execute(text("ALTER TABLE oboruds ADD COLUMN owner_id INTEGER"))
|
||||
log("Добавлен столбец oboruds.owner_id")
|
||||
except Exception as e:
|
||||
log(f"Предупреждение: проверка/добавление owner_id не выполнена: {e}")
|
||||
|
||||
def migrate():
|
||||
ensure_schema()
|
||||
log("Запуск переноса данных из old_app.db → app.db")
|
||||
|
||||
# Тип оборудования по умолчанию
|
||||
log("Добавление типа оборудования по умолчанию: 'Неизвестно'")
|
||||
default_type = models.EquipmentType(name="Неизвестно")
|
||||
new_db.add(default_type)
|
||||
new_db.commit()
|
||||
|
||||
# Перенос аудиторий
|
||||
log("Перенос аудиторий...")
|
||||
auditory_map = {}
|
||||
aud_rows = old_db.execute(text("SELECT id, audnazvanie FROM auditory")).fetchall()
|
||||
for aud in aud_rows:
|
||||
new_aud = models.Auditory(audnazvanie=aud.audnazvanie)
|
||||
new_db.add(new_aud)
|
||||
new_db.flush()
|
||||
auditory_map[aud.id] = new_aud.id
|
||||
log(f" → перенесено: {len(aud_rows)}")
|
||||
|
||||
# Перенос оборудования
|
||||
log("Перенос оборудования...")
|
||||
ob_rows = old_db.execute(text("SELECT * FROM oboruds")).fetchall()
|
||||
for ob in ob_rows:
|
||||
new_ob = models.Oboruds(
|
||||
invNumber=ob.invNumber,
|
||||
nazvanie=ob.nazvanie,
|
||||
raspologenie=ob.raspologenie,
|
||||
numberppasu=ob.numberppasu,
|
||||
kolichestvo=ob.kolichestvo,
|
||||
aud_id=auditory_map.get(ob.aud_id),
|
||||
type_id=default_type.id
|
||||
)
|
||||
new_db.add(new_ob)
|
||||
log(f" → перенесено: {len(ob_rows)}")
|
||||
|
||||
# Перенос заметок
|
||||
log("Перенос заметок...")
|
||||
z_rows = old_db.execute(text("SELECT * FROM zametki")).fetchall()
|
||||
for z in z_rows:
|
||||
new_z = models.Zametki(
|
||||
txtzam=z.txtzam,
|
||||
created_date=z.created_date,
|
||||
rmdt=z.rmdt
|
||||
)
|
||||
new_db.add(new_z)
|
||||
log(f" → перенесено: {len(z_rows)}")
|
||||
|
||||
new_db.commit()
|
||||
log("✅ Перенос завершён успешно.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
100
backend/models.py
Normal file
100
backend/models.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# backend/models.py
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import relationship, declarative_base
|
||||
import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
username = Column(String(150), unique=True, nullable=False)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
role = Column(String(50), nullable=False, default='viewer') # 'admin' | 'editor' | 'viewer'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Auditory(Base):
|
||||
__tablename__ = 'auditories'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
audnazvanie = Column(String)
|
||||
|
||||
oboruds = relationship("Oboruds", back_populates="auditory")
|
||||
|
||||
|
||||
class EquipmentType(Base):
|
||||
__tablename__ = 'equipment_types'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, unique=True, nullable=False)
|
||||
|
||||
oboruds = relationship("Oboruds", back_populates="type")
|
||||
|
||||
class Owner(Base):
|
||||
__tablename__ = 'owners'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, unique=True, nullable=False)
|
||||
|
||||
oboruds = relationship("Oboruds", back_populates="owner")
|
||||
|
||||
|
||||
class Oboruds(Base):
|
||||
__tablename__ = 'oboruds'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
invNumber = Column(Integer)
|
||||
nazvanie = Column(String(500))
|
||||
raspologenie = Column(String(200))
|
||||
numberppasu = Column(String(100))
|
||||
kolichestvo = Column(Integer)
|
||||
|
||||
aud_id = Column(Integer, ForeignKey("auditories.id"))
|
||||
auditory = relationship("Auditory", back_populates="oboruds")
|
||||
|
||||
type_id = Column(Integer, ForeignKey("equipment_types.id"))
|
||||
type = relationship("EquipmentType", back_populates="oboruds")
|
||||
|
||||
owner_id = Column(Integer, ForeignKey("owners.id"))
|
||||
owner = relationship("Owner", back_populates="oboruds")
|
||||
|
||||
components = relationship("Component", back_populates="oborud")
|
||||
consumables = relationship("Consumable", back_populates="oborud")
|
||||
|
||||
|
||||
class Component(Base):
|
||||
__tablename__ = 'components'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
|
||||
oborud_id = Column(Integer, ForeignKey("oboruds.id"))
|
||||
oborud = relationship("Oboruds", back_populates="components")
|
||||
|
||||
|
||||
class Consumable(Base):
|
||||
__tablename__ = 'consumables'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
|
||||
oborud_id = Column(Integer, ForeignKey("oboruds.id"))
|
||||
oborud = relationship("Oboruds", back_populates="consumables")
|
||||
|
||||
|
||||
class Zametki(Base):
|
||||
__tablename__ = 'zametki'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
txtzam = Column(String(10000))
|
||||
created_date = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
rmdt = Column(DateTime)
|
||||
BIN
backend/requirements.txt
Normal file
BIN
backend/requirements.txt
Normal file
Binary file not shown.
2
backend/routers/__init__.py
Normal file
2
backend/routers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Routers package initializer."""
|
||||
|
||||
19
backend/routers/auditories.py
Normal file
19
backend/routers/auditories.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import models, schemas, database
|
||||
from ..security import require_roles
|
||||
|
||||
auditories = APIRouter(prefix="/auditories", tags=["auditories"])
|
||||
|
||||
@auditories.post("/", response_model=schemas.AuditoryRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||
def create_auditory(item: schemas.AuditoryCreate, db: Session = Depends(database.get_db)):
|
||||
obj = models.Auditory(**item.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
@auditories.get("/", response_model=list[schemas.AuditoryRead])
|
||||
def list_auditories(db: Session = Depends(database.get_db)):
|
||||
return db.query(models.Auditory).all()
|
||||
|
||||
63
backend/routers/auth.py
Normal file
63
backend/routers/auth.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import schemas, database
|
||||
from ..security import authenticate_user, create_access_token, get_password_hash, require_roles
|
||||
from ..models import User
|
||||
|
||||
|
||||
auth = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@auth.post("/token", response_model=schemas.Token)
|
||||
def login_for_access_token(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(database.get_db),
|
||||
):
|
||||
user = authenticate_user(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
|
||||
access_token_expires = timedelta(minutes=60)
|
||||
access_token = create_access_token(data={"sub": user.username, "role": user.role}, expires_delta=access_token_expires)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@auth.post("/users", response_model=schemas.UserRead, dependencies=[Depends(require_roles(["admin"]))])
|
||||
def create_user(item: schemas.UserCreate, db: Session = Depends(database.get_db)):
|
||||
if db.query(User).filter(User.username == item.username).first():
|
||||
raise HTTPException(status_code=400, detail="Username already exists")
|
||||
obj = User(username=item.username, password_hash=get_password_hash(item.password), role=item.role)
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@auth.post("/users/admin", response_model=schemas.UserRead, dependencies=[Depends(require_roles(["admin"]))])
|
||||
def create_admin_user(item: schemas.UserCreate, db: Session = Depends(database.get_db)):
|
||||
if db.query(User).filter(User.username == item.username).first():
|
||||
raise HTTPException(status_code=400, detail="Username already exists")
|
||||
obj = User(username=item.username, password_hash=get_password_hash(item.password), role="admin")
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@auth.get("/users", response_model=list[schemas.UserRead], dependencies=[Depends(require_roles(["admin"]))])
|
||||
def list_users(db: Session = Depends(database.get_db)):
|
||||
return db.query(User).all()
|
||||
|
||||
|
||||
@auth.patch("/users/{user_id}/role", response_model=schemas.UserRead, dependencies=[Depends(require_roles(["admin"]))])
|
||||
def update_user_role(user_id: int, payload: schemas.UserRoleUpdate, db: Session = Depends(database.get_db)):
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
user.role = payload.role
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
19
backend/routers/components.py
Normal file
19
backend/routers/components.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import models, schemas, database
|
||||
from ..security import require_roles
|
||||
|
||||
|
||||
components = APIRouter(prefix="/components", tags=["components"])
|
||||
|
||||
@components.post("/", response_model=schemas.ComponentRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||
def create_component(item: schemas.ComponentCreate, db: Session = Depends(database.get_db)):
|
||||
obj = models.Component(**item.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
@components.get("/", response_model=list[schemas.ComponentRead])
|
||||
def list_components(db: Session = Depends(database.get_db)):
|
||||
return db.query(models.Component).all()
|
||||
18
backend/routers/equipment_types.py
Normal file
18
backend/routers/equipment_types.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import models, schemas, database
|
||||
from ..security import require_roles
|
||||
|
||||
equipment_types = APIRouter(prefix="/equipment-types", tags=["equipment_types"])
|
||||
|
||||
@equipment_types.post("/", response_model=schemas.EquipmentTypeRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||
def create_equipment_type(item: schemas.EquipmentTypeCreate, db: Session = Depends(database.get_db)):
|
||||
obj = models.EquipmentType(**item.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
@equipment_types.get("/", response_model=list[schemas.EquipmentTypeRead])
|
||||
def list_equipment_types(db: Session = Depends(database.get_db)):
|
||||
return db.query(models.EquipmentType).all()
|
||||
42
backend/routers/oboruds.py
Normal file
42
backend/routers/oboruds.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import models, schemas, database
|
||||
from ..security import require_roles
|
||||
|
||||
oboruds = APIRouter(prefix="/oboruds", tags=["oboruds"])
|
||||
|
||||
@oboruds.post("/", response_model=schemas.OborudRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||
def create_oborud(item: schemas.OborudCreate, db: Session = Depends(database.get_db)):
|
||||
obj = models.Oboruds(**item.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
@oboruds.get("/", response_model=list[schemas.OborudRead])
|
||||
def list_oboruds(aud_id: Optional[int] = None, db: Session = Depends(database.get_db)):
|
||||
query = db.query(models.Oboruds)
|
||||
if aud_id is not None:
|
||||
query = query.filter(models.Oboruds.aud_id == aud_id)
|
||||
return query.all()
|
||||
|
||||
@oboruds.get("/{oborud_id}", response_model=schemas.OborudRead)
|
||||
def get_oborud(oborud_id: int, db: Session = Depends(database.get_db)):
|
||||
obj = db.query(models.Oboruds).filter(models.Oboruds.id == oborud_id).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Oborud not found")
|
||||
return obj
|
||||
|
||||
|
||||
@oboruds.patch("/{oborud_id}", response_model=schemas.OborudRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||
def update_oborud(oborud_id: int, payload: schemas.OborudUpdate, db: Session = Depends(database.get_db)):
|
||||
obj = db.query(models.Oboruds).filter(models.Oboruds.id == oborud_id).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Oborud not found")
|
||||
data = payload.dict(exclude_unset=True)
|
||||
for k, v in data.items():
|
||||
setattr(obj, k, v)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
22
backend/routers/owners.py
Normal file
22
backend/routers/owners.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import models, schemas, database
|
||||
from ..security import require_roles
|
||||
|
||||
|
||||
owners = APIRouter(prefix="/owners", tags=["owners"])
|
||||
|
||||
|
||||
@owners.post("/", response_model=schemas.OwnerRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||
def create_owner(item: schemas.OwnerCreate, db: Session = Depends(database.get_db)):
|
||||
obj = models.Owner(**item.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@owners.get("/", response_model=list[schemas.OwnerRead])
|
||||
def list_owners(db: Session = Depends(database.get_db)):
|
||||
return db.query(models.Owner).all()
|
||||
|
||||
18
backend/routers/rashodniki.py
Normal file
18
backend/routers/rashodniki.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import models, schemas, database
|
||||
from ..security import require_roles
|
||||
|
||||
consumables = APIRouter(prefix="/consumables", tags=["consumables"])
|
||||
|
||||
@consumables.post("/", response_model=schemas.ConsumableRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||
def create_consumable(item: schemas.ConsumableCreate, db: Session = Depends(database.get_db)):
|
||||
obj = models.Consumable(**item.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
@consumables.get("/", response_model=list[schemas.ConsumableRead])
|
||||
def list_consumables(db: Session = Depends(database.get_db)):
|
||||
return db.query(models.Consumable).all()
|
||||
18
backend/routers/zametki.py
Normal file
18
backend/routers/zametki.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import models, schemas, database
|
||||
from ..security import require_roles
|
||||
|
||||
zametki = APIRouter(prefix="/zametki", tags=["zametki"])
|
||||
|
||||
@zametki.post("/", response_model=schemas.ZametkaRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||
def create_zametka(item: schemas.ZametkaCreate, db: Session = Depends(database.get_db)):
|
||||
obj = models.Zametki(**item.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
@zametki.get("/", response_model=list[schemas.ZametkaRead])
|
||||
def list_zametki(db: Session = Depends(database.get_db)):
|
||||
return db.query(models.Zametki).all()
|
||||
158
backend/schemas.py
Normal file
158
backend/schemas.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# backend/schemas.py
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Literal
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# === Equipment Type ===
|
||||
class EquipmentTypeBase(BaseModel):
|
||||
name: str
|
||||
|
||||
class EquipmentTypeCreate(EquipmentTypeBase):
|
||||
pass
|
||||
|
||||
class EquipmentTypeRead(EquipmentTypeBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# === Component ===
|
||||
class ComponentBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class ComponentCreate(ComponentBase):
|
||||
oborud_id: int
|
||||
|
||||
class ComponentRead(ComponentBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# === Consumable ===
|
||||
class ConsumableBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class ConsumableCreate(ConsumableBase):
|
||||
oborud_id: int
|
||||
|
||||
class ConsumableRead(ConsumableBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# === Owner ===
|
||||
class OwnerBase(BaseModel):
|
||||
name: str
|
||||
|
||||
class OwnerCreate(OwnerBase):
|
||||
pass
|
||||
|
||||
class OwnerRead(OwnerBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# === Oborud ===
|
||||
class OborudBase(BaseModel):
|
||||
invNumber: Optional[int]
|
||||
nazvanie: str
|
||||
raspologenie: Optional[str] = None
|
||||
numberppasu: Optional[str] = None
|
||||
kolichestvo: Optional[int] = None
|
||||
aud_id: int
|
||||
type_id: int
|
||||
owner_id: Optional[int] = None
|
||||
|
||||
class OborudCreate(OborudBase):
|
||||
pass
|
||||
|
||||
class OborudUpdate(BaseModel):
|
||||
invNumber: Optional[int] = None
|
||||
nazvanie: Optional[str] = None
|
||||
raspologenie: Optional[str] = None
|
||||
numberppasu: Optional[str] = None
|
||||
kolichestvo: Optional[int] = None
|
||||
aud_id: Optional[int] = None
|
||||
type_id: Optional[int] = None
|
||||
owner_id: Optional[int] = None
|
||||
|
||||
class OborudRead(OborudBase):
|
||||
id: int
|
||||
type: EquipmentTypeRead
|
||||
owner: Optional[OwnerRead] = None
|
||||
components: List[ComponentRead] = []
|
||||
consumables: List[ConsumableRead] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# === Auditory ===
|
||||
class AuditoryBase(BaseModel):
|
||||
audnazvanie: str
|
||||
|
||||
class AuditoryCreate(AuditoryBase):
|
||||
pass
|
||||
|
||||
class AuditoryRead(AuditoryBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# === Zametka ===
|
||||
class ZametkaBase(BaseModel):
|
||||
txtzam: str
|
||||
rmdt: Optional[datetime] = None
|
||||
|
||||
class ZametkaCreate(ZametkaBase):
|
||||
pass
|
||||
|
||||
class ZametkaRead(ZametkaBase):
|
||||
id: int
|
||||
created_date: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# === Auth/User ===
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
|
||||
Role = Literal["admin", "editor", "viewer"]
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
username: str
|
||||
role: Role = "viewer"
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
class UserRead(UserBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserRoleUpdate(BaseModel):
|
||||
role: Role
|
||||
79
backend/security.py
Normal file
79
backend/security.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Callable, Iterable
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import models
|
||||
from .database import get_db
|
||||
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-change-me")
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60"))
|
||||
|
||||
# Use pbkdf2_sha256 to avoid external bcrypt backend issues
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
|
||||
|
||||
|
||||
def verify_password(plain_password: str, password_hash: str) -> bool:
|
||||
return pwd_context.verify(plain_password, password_hash)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
|
||||
def get_user_by_username(db: Session, username: str) -> Optional[models.User]:
|
||||
return db.query(models.User).filter(models.User.username == username).first()
|
||||
|
||||
|
||||
def authenticate_user(db: Session, username: str, password: str) -> Optional[models.User]:
|
||||
user = get_user_by_username(db, username)
|
||||
if not user:
|
||||
return None
|
||||
if not verify_password(password, user.password_hash):
|
||||
return None
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> models.User:
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = get_user_by_username(db, username=username)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
def require_roles(allowed_roles: Iterable[str]) -> Callable[[models.User], models.User]:
|
||||
allowed = set(allowed_roles)
|
||||
|
||||
def _dependency(user: models.User = Depends(get_current_user)) -> models.User:
|
||||
if user.role not in allowed:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
return user
|
||||
|
||||
return _dependency
|
||||
221
frontend/app.js
Normal file
221
frontend/app.js
Normal file
@@ -0,0 +1,221 @@
|
||||
const { createApp } = Vue;
|
||||
|
||||
const api = {
|
||||
auds: "/auditories/",
|
||||
oboruds: (audId) => `/oboruds/?aud_id=${encodeURIComponent(audId)}`,
|
||||
owners: "/owners/",
|
||||
};
|
||||
|
||||
async function fetchJSON(url) {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
view: 'byAud',
|
||||
auditories: [],
|
||||
selectedAudId: '',
|
||||
oboruds: [],
|
||||
status: '',
|
||||
error: '',
|
||||
// auth/user management
|
||||
token: '',
|
||||
role: '',
|
||||
users: [],
|
||||
newAdminUsername: '',
|
||||
newAdminPassword: '',
|
||||
newAudName: '',
|
||||
owners: [],
|
||||
newOwnerName: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isAuth() { return !!this.token; },
|
||||
isAdmin() { return this.role === 'admin'; },
|
||||
isEditor() { return this.role === 'editor'; },
|
||||
canEdit() { return this.isAdmin || this.isEditor; },
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
try {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('role');
|
||||
} catch {}
|
||||
this.token = '';
|
||||
this.role = '';
|
||||
this.users = [];
|
||||
this.status = '';
|
||||
this.error = '';
|
||||
this.view = 'byAud';
|
||||
// опционально: редирект на страницу логина
|
||||
// window.location.href = '/login';
|
||||
},
|
||||
authHeaders() {
|
||||
const h = {};
|
||||
if (this.token) h['Authorization'] = `Bearer ${this.token}`;
|
||||
return h;
|
||||
},
|
||||
async fetchAuth(url, options = {}) {
|
||||
const opt = { ...options, headers: { ...(options.headers||{}), ...this.authHeaders() } };
|
||||
const res = await fetch(url, opt);
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`HTTP ${res.status}: ${text}`);
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
async loadAuditories() {
|
||||
this.status = 'Загрузка аудиторий…';
|
||||
this.error = '';
|
||||
try {
|
||||
this.auditories = await fetchJSON(api.auds);
|
||||
this.status = '';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = 'Не удалось загрузить аудитории';
|
||||
this.status = '';
|
||||
}
|
||||
},
|
||||
async loadOboruds() {
|
||||
if (!this.selectedAudId) {
|
||||
this.error = '';
|
||||
this.status = 'Выберите аудиторию';
|
||||
return;
|
||||
}
|
||||
this.status = 'Загрузка оборудования…';
|
||||
this.error = '';
|
||||
try {
|
||||
this.oboruds = await fetchJSON(api.oboruds(this.selectedAudId));
|
||||
// init selected owner helper field
|
||||
this.oboruds.forEach(o => { o.selectedOwnerId = o.owner?.id || ''; });
|
||||
this.status = '';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = 'Не удалось загрузить оборудование';
|
||||
this.status = '';
|
||||
}
|
||||
},
|
||||
async saveOwner(item) {
|
||||
try {
|
||||
this.status = 'Сохранение владельца…';
|
||||
await this.fetchAuth(`/oboruds/${item.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ owner_id: item.selectedOwnerId || null }),
|
||||
});
|
||||
this.status = 'Сохранено';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = 'Не удалось сохранить владельца';
|
||||
this.status = '';
|
||||
}
|
||||
},
|
||||
async loadOwners() {
|
||||
try {
|
||||
this.status = this.status || 'Загрузка владельцев…';
|
||||
const data = await fetchJSON(api.owners);
|
||||
this.owners = data;
|
||||
this.status = '';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = 'Не удалось загрузить владельцев';
|
||||
this.status = '';
|
||||
}
|
||||
},
|
||||
async createOwner() {
|
||||
if (!this.newOwnerName) {
|
||||
this.status = 'Укажите имя владельца';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.status = 'Добавление владельца…';
|
||||
await this.fetchAuth(api.owners, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: this.newOwnerName }),
|
||||
});
|
||||
this.newOwnerName = '';
|
||||
await this.loadOwners();
|
||||
// refresh table owner names if visible
|
||||
if (this.selectedAudId) await this.loadOboruds();
|
||||
this.status = 'Владелец добавлен';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = 'Не удалось добавить владельца';
|
||||
this.status = '';
|
||||
}
|
||||
},
|
||||
async loadUsers() {
|
||||
try {
|
||||
this.status = 'Загрузка пользователей…';
|
||||
this.error = '';
|
||||
this.users = await this.fetchAuth('/auth/users');
|
||||
this.status = '';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = 'Не удалось загрузить пользователей';
|
||||
this.status = '';
|
||||
}
|
||||
},
|
||||
async createAdmin() {
|
||||
if (!this.newAdminUsername || !this.newAdminPassword) {
|
||||
this.status = 'Укажите логин и пароль';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.status = 'Создание администратора…';
|
||||
this.error = '';
|
||||
await this.fetchAuth('/auth/users/admin', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: this.newAdminUsername, password: this.newAdminPassword }),
|
||||
});
|
||||
this.newAdminUsername = '';
|
||||
this.newAdminPassword = '';
|
||||
await this.loadUsers();
|
||||
this.status = 'Администратор создан';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = 'Не удалось создать администратора';
|
||||
this.status = '';
|
||||
}
|
||||
},
|
||||
async createAuditory() {
|
||||
if (!this.newAudName) {
|
||||
this.status = 'Укажите название аудитории';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.status = 'Добавление аудитории…';
|
||||
this.error = '';
|
||||
await this.fetchAuth('/auditories/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ audnazvanie: this.newAudName }),
|
||||
});
|
||||
this.newAudName = '';
|
||||
await this.loadAuditories();
|
||||
this.status = 'Аудитория добавлена';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = 'Не удалось добавить аудиторию';
|
||||
this.status = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// read auth from localStorage
|
||||
try {
|
||||
this.token = localStorage.getItem('access_token') || '';
|
||||
this.role = localStorage.getItem('role') || '';
|
||||
} catch {}
|
||||
this.loadAuditories();
|
||||
this.loadOwners();
|
||||
if (this.isAdmin) {
|
||||
this.loadUsers();
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
229
frontend/index.html
Normal file
229
frontend/index.html
Normal file
@@ -0,0 +1,229 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>АСУ Инвентаризация</title>
|
||||
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/static/css/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>
|
||||
<a href="/app/">АСУ Инвентаризация</a>
|
||||
</h1>
|
||||
<h2>Учет оборудования. Демоверсия</h2>
|
||||
</header>
|
||||
|
||||
<div id="app">
|
||||
<div class="row no-print">
|
||||
<nav class="no-print navbar navbar-expand-lg navbar-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/app/">Главная</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="#" @click.prevent="view='byAud'">По аудитории</a></li>
|
||||
<li class="nav-item" v-if="isAdmin"><a class="nav-link" href="#" @click.prevent="view='users'">Пользователи</a></li>
|
||||
<li class="nav-item" v-if="isAdmin"><a class="nav-link" href="#" @click.prevent="view='audManage'">Аудитории</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/docs" target="_blank">API Docs</a></li>
|
||||
<li class="nav-item" v-if="canEdit"><a class="nav-link" href="#" @click.prevent="view='owners'">Владельцы</a></li>
|
||||
</ul>
|
||||
<span class="navbar-text ms-auto" v-if="isAuth">Роль: {{ role }}</span>
|
||||
<a class="btn btn-outline-primary ms-2" v-if="!isAuth" href="/login">Войти</a>
|
||||
<button class="btn btn-outline-secondary ms-2" v-else @click.prevent="logout">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div v-if="view==='byAud'" class="row">
|
||||
<div class="card col-md-10 col-10">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Оборудование по аудитории</h3>
|
||||
<div class="mb-2 d-flex align-items-center gap-2">
|
||||
<label for="aud-select" class="me-2">Аудитория:</label>
|
||||
<select id="aud-select" class="form-select w-auto" v-model="selectedAudId">
|
||||
<option value="">— выберите аудиторию —</option>
|
||||
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" @click="loadOboruds">Показать</button>
|
||||
</div>
|
||||
<div class="status" :class="{error: !!error}">{{ status }}</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Инв. номер</th>
|
||||
<th scope="col">Название</th>
|
||||
<th scope="col">Расположение</th>
|
||||
<th scope="col">Кол-во</th>
|
||||
<th scope="col">Тип</th>
|
||||
<th scope="col">Владелец</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="it in oboruds" :key="it.id">
|
||||
<td>{{ it.id }}</td>
|
||||
<td class="inv">{{ it.invNumber ?? '' }}</td>
|
||||
<td>{{ it.nazvanie ?? '' }}</td>
|
||||
<td class="rasp">{{ it.raspologenie ?? '' }}</td>
|
||||
<td>{{ it.kolichestvo ?? '' }}</td>
|
||||
<td>{{ it.type?.name ?? '' }}</td>
|
||||
<td>
|
||||
<template v-if="canEdit">
|
||||
<select class="form-select form-select-sm d-inline w-auto" v-model="it.selectedOwnerId">
|
||||
<option value="">— нет —</option>
|
||||
<option v-for="ow in owners" :key="ow.id" :value="ow.id">{{ ow.name }}</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary ms-2" @click="saveOwner(it)">Сохранить</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ it.owner?.name ?? '' }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="view==='users'" class="row">
|
||||
<div class="card col-md-10 col-10">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Администрирование пользователей</h3>
|
||||
<div v-if="role!=='admin'" class="alert alert-warning">Недостаточно прав. Войдите как администратор.</div>
|
||||
|
||||
<div v-else>
|
||||
<h5 class="mt-2">Создать пользователя-админа</h5>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label">Логин</label>
|
||||
<input class="form-control" v-model="newAdminUsername" placeholder="username" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label">Пароль</label>
|
||||
<input type="password" class="form-control" v-model="newAdminPassword" placeholder="password" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-success" @click="createAdmin">Создать админа</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
|
||||
|
||||
<h5 class="mt-4">Пользователи</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Логин</th>
|
||||
<th>Роль</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="u in users" :key="u.id">
|
||||
<td>{{ u.id }}</td>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.role }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="view==='audManage'" class="row">
|
||||
<div class="card col-md-10 col-10">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Управление аудиториями</h3>
|
||||
<div v-if="role!=='admin'" class="alert alert-warning">Недостаточно прав. Войдите как администратор.</div>
|
||||
|
||||
<div v-else>
|
||||
<h5 class="mt-2">Добавить аудиторию</h5>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label">Название аудитории</label>
|
||||
<input class="form-control" v-model="newAudName" placeholder="например, 519" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-success" @click="createAuditory">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
|
||||
|
||||
<h5 class="mt-4">Список аудиторий</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="a in auditories" :key="a.id">
|
||||
<td>{{ a.id }}</td>
|
||||
<td>{{ a.audnazvanie }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="view==='owners'" class="row">
|
||||
<div class="card col-md-10 col-10">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Управление владельцами</h3>
|
||||
<div v-if="!canEdit" class="alert alert-warning">Недостаточно прав. Войдите как администратор или редактор.</div>
|
||||
<div v-else>
|
||||
<h5 class="mt-2">Добавить владельца</h5>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label">Имя владельца</label>
|
||||
<input class="form-control" v-model="newOwnerName" placeholder="например, Иванов И.И." />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-success" @click="createOwner">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
|
||||
|
||||
<h5 class="mt-4">Список владельцев</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Имя</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="o in owners" :key="o.id">
|
||||
<td>{{ o.id }}</td>
|
||||
<td>{{ o.name }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="/app/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
44
frontend/login.html
Normal file
44
frontend/login.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Вход — АСУ Инвентаризация</title>
|
||||
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/static/css/index.css" />
|
||||
<style>
|
||||
.login-card { max-width: 420px; margin: 40px auto; }
|
||||
.muted { color: #6c757d; font-size: 0.9rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="/app/">АСУ Инвентаризация</a></h1>
|
||||
<h2>Авторизация</h2>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<div class="card login-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">Вход</h5>
|
||||
<form id="login-form">
|
||||
<div class="mb-3 text-start">
|
||||
<label for="username" class="form-label">Логин</label>
|
||||
<input type="text" id="username" class="form-control" autocomplete="username" required />
|
||||
</div>
|
||||
<div class="mb-3 text-start">
|
||||
<label for="password" class="form-label">Пароль</label>
|
||||
<input type="password" id="password" class="form-control" autocomplete="current-password" required />
|
||||
</div>
|
||||
<div id="status" class="muted mb-2"></div>
|
||||
<button type="submit" class="btn btn-primary w-100">Войти</button>
|
||||
</form>
|
||||
<div class="muted mt-3">Демо: admin / admin (после инициализации БД)</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/app/login.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
58
frontend/login.js
Normal file
58
frontend/login.js
Normal file
@@ -0,0 +1,58 @@
|
||||
function setStatus(msg, type = "info") {
|
||||
const el = document.getElementById("status");
|
||||
el.textContent = msg || "";
|
||||
el.style.color = type === 'error' ? '#b91c1c' : type === 'ok' ? '#16a34a' : '';
|
||||
}
|
||||
|
||||
function decodeRoleFromJWT(token) {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
|
||||
return payload.role || null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function login(username, password) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('username', username);
|
||||
params.set('password', password);
|
||||
try {
|
||||
const res = await fetch('/auth/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: params.toString(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
throw new Error(`Ошибка входа (${res.status}): ${txt}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const token = data.access_token;
|
||||
if (!token) throw new Error('Токен не получен');
|
||||
localStorage.setItem('access_token', token);
|
||||
const role = decodeRoleFromJWT(token);
|
||||
if (role) localStorage.setItem('role', role);
|
||||
return { token, role };
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.getElementById('login-form');
|
||||
form.addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
setStatus('Входим…');
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
try {
|
||||
const { role } = await login(username, password);
|
||||
setStatus(`Успешный вход${role ? ' (' + role + ')' : ''}`, 'ok');
|
||||
// Небольшая задержка для визуального отклика
|
||||
setTimeout(() => { window.location.href = '/app/'; }, 400);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setStatus(e.message || 'Ошибка авторизации', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
19
frontend/styles.css
Normal file
19
frontend/styles.css
Normal file
@@ -0,0 +1,19 @@
|
||||
:root { --bg: #0f172a; --fg: #e2e8f0; --muted: #94a3b8; --accent: #38bdf8; --err: #ef4444; --warn: #f59e0b; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background: var(--bg); color: var(--fg); }
|
||||
header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid #1f2937; }
|
||||
header h1 { margin: 0; font-size: 20px; }
|
||||
header nav a { color: var(--accent); text-decoration: none; }
|
||||
main { padding: 20px; max-width: 1000px; margin: 0 auto; }
|
||||
.panel { background: #0b1220; border: 1px solid #1f2937; border-radius: 8px; padding: 16px; }
|
||||
.controls { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
|
||||
select, button { padding: 8px 10px; border-radius: 6px; border: 1px solid #1f2937; background: #0a0f1a; color: var(--fg); }
|
||||
button { cursor: pointer; }
|
||||
button:hover { border-color: var(--accent); }
|
||||
.status { min-height: 20px; color: var(--muted); margin-bottom: 8px; }
|
||||
.status.error { color: var(--err); }
|
||||
.status.warn { color: var(--warn); }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { border-bottom: 1px solid #1f2937; padding: 8px; text-align: left; }
|
||||
th { color: var(--muted); font-weight: 600; }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
32
migrations/versions/10da3140ab2e_.py
Normal file
32
migrations/versions/10da3140ab2e_.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 10da3140ab2e
|
||||
Revises: 4eacd6dcd461
|
||||
Create Date: 2024-04-05 01:25:48.931573
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '10da3140ab2e'
|
||||
down_revision = '4eacd6dcd461'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oboruds', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('kolichestvo', sa.Integer(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oboruds', schema=None) as batch_op:
|
||||
batch_op.drop_column('kolichestvo')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
54
migrations/versions/4eacd6dcd461_.py
Normal file
54
migrations/versions/4eacd6dcd461_.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 4eacd6dcd461
|
||||
Revises: c208cbc25232
|
||||
Create Date: 2024-04-03 23:25:42.271936
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4eacd6dcd461'
|
||||
down_revision = 'c208cbc25232'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oboruds', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('numberved', sa.String(length=100), nullable=True))
|
||||
batch_op.add_column(sa.Column('numberppasu', sa.String(length=100), nullable=True))
|
||||
batch_op.add_column(sa.Column('balancenumber', sa.Integer(), nullable=True))
|
||||
batch_op.alter_column('nazvanie',
|
||||
existing_type=sa.TEXT(length=500),
|
||||
type_=sa.String(length=500),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('raspologenie',
|
||||
existing_type=sa.TEXT(length=200),
|
||||
type_=sa.String(length=200),
|
||||
existing_nullable=True)
|
||||
batch_op.drop_column('buhnumberpp')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oboruds', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('buhnumberpp', sa.VARCHAR(length=100), nullable=True))
|
||||
batch_op.alter_column('raspologenie',
|
||||
existing_type=sa.String(length=200),
|
||||
type_=sa.TEXT(length=200),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('nazvanie',
|
||||
existing_type=sa.String(length=500),
|
||||
type_=sa.TEXT(length=500),
|
||||
existing_nullable=True)
|
||||
batch_op.drop_column('balancenumber')
|
||||
batch_op.drop_column('numberppasu')
|
||||
batch_op.drop_column('numberved')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
34
migrations/versions/6fc3d1adb061_.py
Normal file
34
migrations/versions/6fc3d1adb061_.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 6fc3d1adb061
|
||||
Revises: be7c94c549e5
|
||||
Create Date: 2024-04-02 16:32:23.180273
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6fc3d1adb061'
|
||||
down_revision = 'be7c94c549e5'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oboruds', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('buhnumberpp', sa.String(length=100), nullable=True))
|
||||
batch_op.add_column(sa.Column('kolichestvo', sa.Integer(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oboruds', schema=None) as batch_op:
|
||||
batch_op.drop_column('kolichestvo')
|
||||
batch_op.drop_column('buhnumberpp')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
54
migrations/versions/8e5efc4de919_.py
Normal file
54
migrations/versions/8e5efc4de919_.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 8e5efc4de919
|
||||
Revises: c208cbc25232
|
||||
Create Date: 2024-04-03 22:36:46.208266
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8e5efc4de919'
|
||||
down_revision = 'c208cbc25232'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oboruds', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('numberved', sa.String(length=100), nullable=True))
|
||||
batch_op.add_column(sa.Column('numberppasu', sa.String(length=100), nullable=True))
|
||||
batch_op.add_column(sa.Column('balancenumber', sa.Integer(), nullable=True))
|
||||
batch_op.alter_column('nazvanie',
|
||||
existing_type=sa.TEXT(length=500),
|
||||
type_=sa.String(length=500),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('raspologenie',
|
||||
existing_type=sa.TEXT(length=200),
|
||||
type_=sa.String(length=200),
|
||||
existing_nullable=True)
|
||||
batch_op.drop_column('buhnumberpp')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oboruds', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('buhnumberpp', sa.TEXT(length=100), nullable=True))
|
||||
batch_op.alter_column('raspologenie',
|
||||
existing_type=sa.String(length=200),
|
||||
type_=sa.TEXT(length=200),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('nazvanie',
|
||||
existing_type=sa.String(length=500),
|
||||
type_=sa.TEXT(length=500),
|
||||
existing_nullable=True)
|
||||
batch_op.drop_column('balancenumber')
|
||||
batch_op.drop_column('numberppasu')
|
||||
batch_op.drop_column('numberved')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
24
migrations/versions/c208cbc25232_.py
Normal file
24
migrations/versions/c208cbc25232_.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: c208cbc25232
|
||||
Revises: 6fc3d1adb061, 885bdd7b5161
|
||||
Create Date: 2024-04-03 22:11:15.008480
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c208cbc25232'
|
||||
down_revision = ('6fc3d1adb061', '885bdd7b5161')
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
24
migrations/versions/d4a8e4c9e65a_.py
Normal file
24
migrations/versions/d4a8e4c9e65a_.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: d4a8e4c9e65a
|
||||
Revises: 10da3140ab2e, 8e5efc4de919
|
||||
Create Date: 2024-05-06 08:27:39.088982
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd4a8e4c9e65a'
|
||||
down_revision = ('10da3140ab2e', '8e5efc4de919')
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
81
models.py
81
models.py
@@ -1,35 +1,76 @@
|
||||
# backend/models.py
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import relationship, declarative_base
|
||||
import datetime
|
||||
|
||||
db = SQLAlchemy()
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Auditory(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
audnazvanie=db.Column(db.String)
|
||||
oboruds = db.relationship('Oboruds')
|
||||
class Auditory(Base):
|
||||
__tablename__ = 'auditories'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
audnazvanie = Column(String)
|
||||
|
||||
oboruds = relationship("Oboruds", back_populates="auditory")
|
||||
|
||||
|
||||
class EquipmentType(Base):
|
||||
__tablename__ = 'equipment_types'
|
||||
|
||||
class Oboruds(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
invNumber=db.Column(db.Integer)
|
||||
nazvanie=db.Column(db.String(500))
|
||||
balancenumber = db.Column(db.String(30))
|
||||
raspologenie = db.Column(db.String(200))
|
||||
numberved = db.Column(db.String(100))
|
||||
kolichestvo = db.Column(db.Integer)
|
||||
aud_id = db.Column(db.Integer, db.ForeignKey(Auditory.id))
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, unique=True, nullable=False)
|
||||
|
||||
oboruds = relationship("Oboruds", back_populates="type")
|
||||
|
||||
|
||||
class Oboruds(Base):
|
||||
__tablename__ = 'oboruds'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
invNumber = Column(Integer)
|
||||
nazvanie = Column(String(500))
|
||||
raspologenie = Column(String(200))
|
||||
numberppasu = Column(String(100))
|
||||
kolichestvo = Column(Integer)
|
||||
|
||||
aud_id = Column(Integer, ForeignKey("auditories.id"))
|
||||
auditory = relationship("Auditory", back_populates="oboruds")
|
||||
|
||||
type_id = Column(Integer, ForeignKey("equipment_types.id"))
|
||||
type = relationship("EquipmentType", back_populates="oboruds")
|
||||
|
||||
components = relationship("Component", back_populates="oborud")
|
||||
consumables = relationship("Consumable", back_populates="oborud")
|
||||
|
||||
|
||||
class Component(Base):
|
||||
__tablename__ = 'components'
|
||||
|
||||
class Zametki(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
txtzam=db.Column(db.String(10000))
|
||||
created_date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
rmdt = db.Column(db.DateTime)
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
|
||||
oborud_id = Column(Integer, ForeignKey("oboruds.id"))
|
||||
oborud = relationship("Oboruds", back_populates="components")
|
||||
|
||||
|
||||
class Consumable(Base):
|
||||
__tablename__ = 'consumables'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
|
||||
oborud_id = Column(Integer, ForeignKey("oboruds.id"))
|
||||
oborud = relationship("Oboruds", back_populates="consumables")
|
||||
|
||||
|
||||
class Zametki(Base):
|
||||
__tablename__ = 'zametki'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
txtzam = Column(String(10000))
|
||||
created_date = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
rmdt = Column(DateTime)
|
||||
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
6
serve.py
Normal file
6
serve.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from waitress import serve
|
||||
from app import app
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
serve(app, port='3800')
|
||||
@@ -54,7 +54,7 @@ button {
|
||||
|
||||
.card {
|
||||
|
||||
width: 200px;
|
||||
/*width: 200px; */
|
||||
margin: 10px;
|
||||
border-radius: 15px;
|
||||
border-color: #E07D54;
|
||||
@@ -67,7 +67,6 @@ h5 {
|
||||
}
|
||||
|
||||
|
||||
|
||||
.hidden-column {
|
||||
display: none;
|
||||
}
|
||||
@@ -78,8 +77,17 @@ nav{
|
||||
|
||||
}
|
||||
|
||||
.table{
|
||||
table {
|
||||
word-break: break-all;
|
||||
border-collapse: separate !important;
|
||||
}
|
||||
|
||||
.table td {
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
max-width: 10rem;
|
||||
word-break: break-all;
|
||||
border-collapse: separate !important;
|
||||
}
|
||||
|
||||
.aud {
|
||||
@@ -91,8 +99,9 @@ nav{
|
||||
}
|
||||
|
||||
.rasp {
|
||||
max-width: 200px;
|
||||
width: 200px;
|
||||
word-break: break-word;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#modal_matcenn {
|
||||
@@ -105,8 +114,10 @@ nav{
|
||||
}
|
||||
|
||||
|
||||
.datatable th:nth-child(7) {
|
||||
width: 200px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@media print {
|
||||
@@ -139,6 +150,7 @@ nav{
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
@@ -149,11 +161,13 @@ nav{
|
||||
font-size: 14pt;
|
||||
|
||||
}
|
||||
|
||||
table.rs-table-bordered > thead > tr > th {
|
||||
border: 1px solid #000000;
|
||||
padding: 2px;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
table.rs-table-bordered > tbody > tr > td {
|
||||
border: 1px solid #000000;
|
||||
padding: 10px;
|
||||
@@ -162,6 +176,4 @@ nav{
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,35 +1,282 @@
|
||||
function clearTable() {
|
||||
var table = document.getElementById("alldatatable");
|
||||
var rowCount = table.rows.length;
|
||||
|
||||
// Iterate through each row and remove it
|
||||
for (var i = rowCount - 1; i > 0; i--) {
|
||||
table.deleteRow(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getAllData() {
|
||||
$.ajax({
|
||||
|
||||
url: "/getall",
|
||||
type: "get",
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
clearTable();
|
||||
|
||||
success: function(response){
|
||||
console.log(response)
|
||||
let data = response;
|
||||
$('#datatable tbody').empty();
|
||||
$.each(response, function(index, item) {
|
||||
$('#datatable tbody').append(
|
||||
'<tr>' +
|
||||
'<td>' + item.invNumber + '</td>' +
|
||||
'<td>' + item.nazvanie + '</td>' +
|
||||
'<td>' + item.raspologenie + '</td>' +
|
||||
'<td>' + item.numberved + '</td>' +
|
||||
'<td>' + item.buhnumberpp + '</td>' +
|
||||
'<td>' + item.kolichestvo + '</td>' +
|
||||
'<td>' + item.balancenumber + '</td>' +
|
||||
'</tr>'
|
||||
);
|
||||
})
|
||||
},
|
||||
let tableBody = document.getElementById('alldatatable').getElementsByTagName("tbody")[0];
|
||||
i = 0;
|
||||
$.getJSON("/getall", function (data) {
|
||||
$.each(data, function (index, item) {
|
||||
let newRow = tableBody.insertRow(tableBody.rows.length);
|
||||
let cell1 = newRow.insertCell(0);
|
||||
let cell2 = newRow.insertCell(1);
|
||||
let cell3 = newRow.insertCell(2);
|
||||
let cell4 = newRow.insertCell(3);
|
||||
let cell5 = newRow.insertCell(4);
|
||||
let cell6 = newRow.insertCell(5);
|
||||
let cell7 = newRow.insertCell(6);
|
||||
let cell8 = newRow.insertCell(7);
|
||||
|
||||
i++;
|
||||
|
||||
cell1.innerText = i;
|
||||
cell2.innerText = item.numberved;
|
||||
cell3.innerText = item.invNumber;
|
||||
cell4.innerText = item.nazvanie;
|
||||
cell5.innerText = item.kolichestvo;
|
||||
cell6.innerText = item.balancenumber;
|
||||
cell7.innerText = item.aud;
|
||||
cell8.innerText = item.raspologenie;
|
||||
|
||||
|
||||
})
|
||||
$(newRow).data('itemData', i);
|
||||
|
||||
|
||||
$(newRow).on("click", function () {
|
||||
|
||||
let vednumbertxt = newRow.cells[1].innerText;
|
||||
let invnomertxt = newRow.cells[2].innerText;
|
||||
let nazvanietxt = newRow.cells[3].innerText;
|
||||
let kolvotxt = newRow.cells[4].innerText;
|
||||
let schettxt = newRow.cells[5].innerText;
|
||||
let raspologtxt = newRow.cells[7].innerText;
|
||||
|
||||
|
||||
$('#getmodal').modal('show');
|
||||
|
||||
|
||||
let vedomost = document.getElementById('modal_vednumber')
|
||||
let invnom = document.getElementById('modal_invnom')
|
||||
let matcen = document.getElementById('modal_matcenn')
|
||||
let kolvo = document.getElementById('modal_kolvo')
|
||||
let balancenumber = document.getElementById('modal_balance')
|
||||
let rasp = document.getElementById('modal_rapolog')
|
||||
|
||||
invnom.innerText = invnomertxt
|
||||
matcen.innerText = nazvanietxt.substring(0, 20)
|
||||
|
||||
if (vednumbertxt.length > 0) {
|
||||
vedomost.value = vednumbertxt;
|
||||
}
|
||||
|
||||
if (kolvotxt.length > 0) {
|
||||
kolvo.value = kolvotxt;
|
||||
}
|
||||
|
||||
if (schettxt.length > 0) {
|
||||
balancenumber.value = schettxt;
|
||||
}
|
||||
|
||||
if (raspologtxt.length > 0) {
|
||||
rasp.value = raspologtxt;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
getAllData()
|
||||
getAllData();
|
||||
|
||||
});
|
||||
|
||||
|
||||
$('#modalclose').click(function () {
|
||||
|
||||
|
||||
let vednumber = document.getElementById('modal_vednumber')
|
||||
let kolvo = document.getElementById('modal_kolvo')
|
||||
let balancenumber = document.getElementById('modal_balance')
|
||||
let matcen = document.getElementById('modal_matcenn')
|
||||
let rasp = document.getElementById('modal_rapolog')
|
||||
|
||||
|
||||
vednumber.value = '';
|
||||
kolvo.value = '';
|
||||
balancenumber.value = '';
|
||||
matcen.value = '';
|
||||
rasp.value = '';
|
||||
$('#getmodal').modal('hide')
|
||||
|
||||
|
||||
})
|
||||
|
||||
$('#modalsavetodb').click(function () {
|
||||
|
||||
let invnom = document.getElementById('modal_invnom')
|
||||
let vednumber = document.getElementById('modal_vednumber')
|
||||
let kolvo = document.getElementById('modal_kolvo')
|
||||
let balancenumber = document.getElementById('modal_balance')
|
||||
let matcen = document.getElementById('modal_matcenn')
|
||||
let rasp = document.getElementById('modal_rapolog')
|
||||
let nazv = document.getElementById('modal_nazvanie')
|
||||
|
||||
|
||||
let changeddata = new Array()
|
||||
|
||||
changeddata[0] = invnom.text;
|
||||
changeddata[1] = vednumber.value;
|
||||
changeddata[2] = kolvo.value;
|
||||
changeddata[3] = balancenumber.value;
|
||||
changeddata[4] = rasp.value;
|
||||
|
||||
|
||||
let sendData = changeddata.join(',')
|
||||
console.log(sendData)
|
||||
|
||||
$.ajax({
|
||||
|
||||
url: "/addraspved",
|
||||
type: "POST",
|
||||
contentType: "application/json;charset=utf-8",
|
||||
dataType: "json",
|
||||
data: sendData,
|
||||
|
||||
success: function () {
|
||||
$('#getmodal').modal('hide')
|
||||
|
||||
vednumber.value = '';
|
||||
kolvo.value = '';
|
||||
balancenumber.value = '';
|
||||
matcen.value = '';
|
||||
rasp.value = '';
|
||||
changeddata = [];
|
||||
window.location.reload();
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
$('#addoborud').click(function () {
|
||||
|
||||
$('#addmodal').modal('show');
|
||||
|
||||
})
|
||||
|
||||
$('#modal2savetodb').click(function () {
|
||||
|
||||
let invnomer = document.getElementById('modal2_invnom')
|
||||
let vednumber = document.getElementById('modal2_vednumber')
|
||||
let kolvo = document.getElementById('modal2_kolvo')
|
||||
let balancenumber = document.getElementById('modal2_balance')
|
||||
let matcen = document.getElementById('modal2_matcenn')
|
||||
let rasp = document.getElementById('modal2_rapolog')
|
||||
let nazv = document.getElementById('modal2_nazvanie')
|
||||
|
||||
|
||||
let changeddata = new Array()
|
||||
|
||||
changeddata[0] = invnomer.value;
|
||||
changeddata[1] = vednumber.value;
|
||||
changeddata[2] = kolvo.value;
|
||||
changeddata[3] = balancenumber.value;
|
||||
changeddata[4] = rasp.value;
|
||||
changeddata[5] = nazv.value;
|
||||
|
||||
|
||||
let sendData = changeddata.join(',')
|
||||
console.log(sendData)
|
||||
|
||||
$.ajax({
|
||||
|
||||
url: "/addoborudasu",
|
||||
type: "POST",
|
||||
contentType: "application/json;charset=utf-8",
|
||||
dataType: "json",
|
||||
data: sendData,
|
||||
|
||||
success: function () {
|
||||
vednumber.value = '';
|
||||
invnomer.value = '';
|
||||
nazvanie.value = '';
|
||||
kolvo.value = '';
|
||||
balancenumber.value = '';
|
||||
matcen.value = '';
|
||||
rasp.value = '';
|
||||
|
||||
$('#addmodal').modal('hide')
|
||||
window.location.reload()
|
||||
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
$('#modal2close').click(function () {
|
||||
|
||||
|
||||
let vednumber = document.getElementById('modal2_vednumber')
|
||||
let invnomer = document.getElementById('modal2_invnom')
|
||||
let nazvanie = document.getElementById('modal2_nazvanie')
|
||||
let kolvo = document.getElementById('modal2_kolvo')
|
||||
let balancenumber = document.getElementById('modal2_balance')
|
||||
let matcen = document.getElementById('modal2_matcenn')
|
||||
let rasp = document.getElementById('modal2_rapolog')
|
||||
|
||||
|
||||
vednumber.value = '';
|
||||
invnomer.value = '';
|
||||
nazvanie.value = '';
|
||||
kolvo.value = '';
|
||||
balancenumber.value = '';
|
||||
matcen.value = '';
|
||||
rasp.value = '';
|
||||
|
||||
$('#addmodal').modal('hide')
|
||||
|
||||
|
||||
})
|
||||
|
||||
$('#modal2').on('hidden.bs.modal', function () {
|
||||
location.reload();
|
||||
})
|
||||
|
||||
$(document).ready(function () {
|
||||
// Слушаем событие клика по заголовкам таблицы
|
||||
$('#alldatatable thead th').on('click', function () {
|
||||
var columnIndex = $(this).index(); // Индекс колонки
|
||||
var sortColumn = $(this).text().toLowerCase(); // Текст заголовка колонки
|
||||
|
||||
// Сортируем таблицу по выбранной колонке
|
||||
$('#alldatatable tbody tr').sort(function (a, b) {
|
||||
var valA;
|
||||
var valB;
|
||||
|
||||
if (isNaN(parseFloat($(a).find('td:eq(' + columnIndex + ')').text()))) {
|
||||
// если это текстовая колонка, то сортируем по алфавиту
|
||||
valA = $(a).find('td:eq(' + columnIndex + ')').text().toLowerCase();
|
||||
valB = $(b).find('td:eq(' + columnIndex + ')').text().toLowerCase();
|
||||
} else {
|
||||
// если это числовая колонка, то сортируем по числовому значению
|
||||
valA = parseFloat($(a).find('td:eq(' + columnIndex + ')').text());
|
||||
valB = parseFloat($(b).find('td:eq(' + columnIndex + ')').text());
|
||||
}
|
||||
|
||||
if (valA < valB) return -1;
|
||||
if (valA > valB) return 1;
|
||||
return 0;
|
||||
}).appendTo('#alldatatable tbody');
|
||||
|
||||
// Обновляем классы для активной колонки
|
||||
$('#alldatatable thead th').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
});
|
||||
});
|
||||
@@ -16,9 +16,9 @@ function getData(){
|
||||
|
||||
var data = response;
|
||||
const table = document.getElementById('datatable')
|
||||
table.innerHTML = ''
|
||||
|
||||
var headTable = '<tr> <td >Номер в Инв. вед</td> <td id="invnomer">Инв. номер</td><td>Название</td><td class="no-print aud">Аудитория</td> <td >Расположение</td> <td id="proverka"class="hidden-column"> Проверено</td> </tr>'
|
||||
table.innerHTML += headTable
|
||||
|
||||
var tr =""
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ function getData(){
|
||||
|
||||
tr += '</tr>'
|
||||
});
|
||||
table.innerHTML += tr
|
||||
table.innerHTML = headTable + tr
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,33 +3,145 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row col-12">
|
||||
<div class=" card col-11">
|
||||
<h3 class=" no-print"> Все мат. ценности </h3>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="getmodal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body" id="textarea">
|
||||
|
||||
<div class="row">
|
||||
<a id="modal_invnom"> </a><a id="modal_matcenn"></a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
№ из ведомости
|
||||
<input type="text" class="form-control" id ='modal_vednumber' placeholder="Номер из ведомости">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
Количество
|
||||
<input type="text" class="form-control" id ='modal_kolvo' placeholder="Количество">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Балансовый счёт
|
||||
<input type="text" class="form-control" id ='modal_balance' placeholder="Балансовый счёт">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Расположение
|
||||
<input type="text" class="form-control" id ='modal_rapolog' placeholder="Введите расположение">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modalclose">Закрыть</button>
|
||||
<button type="button" class="btn btn-primary" id="modalsavetodb" >Сохранить изменения</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Modal2 -->
|
||||
<div class="modal fade" id="addmodal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body" id="textarea">
|
||||
|
||||
<div class="row">
|
||||
<a id="modal_invnom"> </a><a id="modal2_matcenn"></a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
№ из ведомости
|
||||
<input type="text" class="form-control" id ='modal2_vednumber' placeholder="Номер из ведомости">
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
Инвентарный номер
|
||||
<input type="text" class="form-control" id ='modal2_invnom' placeholder="Инвентарный номер">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Название
|
||||
<input type="text" class="form-control" id ='modal2_nazvanie' placeholder="Название">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
Количество
|
||||
<input type="text" class="form-control" id ='modal2_kolvo' placeholder="Количество">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Балансовый счёт
|
||||
<input type="text" class="form-control" id ='modal2_balance' placeholder="Балансовый счёт">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Расположение
|
||||
<input type="text" class="form-control" id ='modal2_rapolog' placeholder="Введите расположение">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modal2close">Закрыть</button>
|
||||
<button type="button" class="btn btn-primary" id="modal2savetodb" >Сохранить изменения</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
||||
<h3 id ='123' class=" no-print"> Все мат. ценности </h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row col-12">
|
||||
<button class="button" id="printallbutton"> Печать </button>
|
||||
</div>
|
||||
|
||||
<div class="row col-12">
|
||||
<button class="button" id="addoborud"> Добавить </button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="card col-md-11" >
|
||||
<table id="datable" class="datable table pagebreak" >
|
||||
<div class="card col-md-11 table-responsive">
|
||||
<table id="alldatatable" class="alldatable table pagebreak" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">№ п/п</th>
|
||||
<th scope="col">№ п/п вед</th>
|
||||
<th scope="col">№ <br>п/п <br>АСУ</th>
|
||||
<th scope="col">№ п/п <br>вед</th>
|
||||
<th scope="col">Инв. номер</th>
|
||||
<th scope="col">Название</th>
|
||||
<th scope="col">Кол-во</th>
|
||||
<th scope="col">Счёт</th>
|
||||
<th scope="col">Аудитория</th>
|
||||
<th scope="col">Ауд - я</th>
|
||||
<th scope="col">Расположение</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{url_for('static', filename='js/allmatc.js') }}"></script>
|
||||
|
||||
{% endblock %}
|
||||
147
templates/all_OLD.html
Normal file
147
templates/all_OLD.html
Normal file
@@ -0,0 +1,147 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="getmodal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body" id="textarea">
|
||||
|
||||
<div class="row">
|
||||
<a id="modal_invnom"> </a><a id="modal_matcenn"></a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
№ из ведомости
|
||||
<input type="text" class="form-control" id ='modal_vednumber' placeholder="Номер из ведомости">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
Количество
|
||||
<input type="text" class="form-control" id ='modal_kolvo' placeholder="Количество">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Балансовый счёт
|
||||
<input type="text" class="form-control" id ='modal_balance' placeholder="Балансовый счёт">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Расположение
|
||||
<input type="text" class="form-control" id ='modal_rapolog' placeholder="Введите расположение">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modalclose">Закрыть</button>
|
||||
<button type="button" class="btn btn-primary" id="modalsavetodb" >Сохранить изменения</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Modal2 -->
|
||||
<div class="modal fade" id="addmodal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body" id="textarea">
|
||||
|
||||
<div class="row">
|
||||
<a id="modal_invnom"> </a><a id="modal2_matcenn"></a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
№ из ведомости
|
||||
<input type="text" class="form-control" id ='modal2_vednumber' placeholder="Номер из ведомости">
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
Инвентарный номер
|
||||
<input type="text" class="form-control" id ='modal2_invnom' placeholder="Инвентарный номер">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Название
|
||||
<input type="text" class="form-control" id ='modal2_nazvanie' placeholder="Название">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
Количество
|
||||
<input type="text" class="form-control" id ='modal2_kolvo' placeholder="Количество">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Балансовый счёт
|
||||
<input type="text" class="form-control" id ='modal2_balance' placeholder="Балансовый счёт">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Расположение
|
||||
<input type="text" class="form-control" id ='modal2_rapolog' placeholder="Введите расположение">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modal2close">Закрыть</button>
|
||||
<button type="button" class="btn btn-primary" id="modal2savetodb" >Сохранить изменения</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
||||
<h3 id ='123' class=" no-print"> Все мат. ценности </h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row col-12">
|
||||
<button class="button" id="printallbutton"> Печать </button>
|
||||
</div>
|
||||
|
||||
<div class="row col-12">
|
||||
<button class="button" id="addoborud"> Добавить </button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="card col-md-11 table-responsive">
|
||||
<table id="alldatatable" class="alldatable table pagebreak" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">№ <br>п/п <br>АСУ</th>
|
||||
<th scope="col">№ п/п <br>вед</th>
|
||||
<th scope="col">Инв. номер</th>
|
||||
<th scope="col">Название</th>
|
||||
<th scope="col">Кол-во</th>
|
||||
<th scope="col">Счёт</th>
|
||||
<th scope="col">Ауд - я</th>
|
||||
<th scope="col">Расположение</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{url_for('static', filename='js/allmatc.js') }}"></script>
|
||||
|
||||
{% endblock %}
|
||||
62
templates/login.html
Normal file
62
templates/login.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="card col-4" >
|
||||
<div class="card-body">
|
||||
|
||||
|
||||
<form class="form-horizontal">
|
||||
<fieldset>
|
||||
|
||||
<!-- Form Name -->
|
||||
<legend>Авторизация</legend>
|
||||
|
||||
<!-- Text input-->
|
||||
<div class="form-group">
|
||||
<label control-label" for="logininput">Логин</label>
|
||||
<div>
|
||||
<input id="logininput" name="logininput" type="text" placeholder="Введите логин" class="form-control input-md">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password input-->
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="passwordinput">Пароль</label>
|
||||
<input id="passwordinput" name="passwordinput" type="password" placeholder="Введите пароль" class="form-control input-md">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<button id="btn_login" name="btn_login" class="btn btn-primary">Войти</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="getmodal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
|
||||
<div class="modal fade" id="getmodal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body" id="textarea">
|
||||
@@ -12,11 +13,36 @@
|
||||
<input type="text" class="form-control" id='vednumber' placeholder="Номер из веломости">
|
||||
<input type="text" class="form-control" id='rapolog' placeholder="Введите расположение">
|
||||
|
||||
|
||||
<div class="row">
|
||||
<a id="modal_invnom"> </a><a id="modal_matcenn"></a>
|
||||
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/addraspved">
|
||||
<div class="row">
|
||||
№ из ведомости
|
||||
<input type="text" class="form-control" name="modal_vednumber" id='modal_vednumber'
|
||||
placeholder="Номер из ведомости">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Расположение
|
||||
<input type="text" class="form-control" name="modal_rapolog" id='modal_rapolog'
|
||||
placeholder="Введите расположение">
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modalclose">Закрыть</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modalclose">Закрыть
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="modalsavetodb">Сохранить изменения</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,16 +71,16 @@
|
||||
<div class="card-body">
|
||||
<h3 class="card-title no-print"> Поаудиторно </h3>
|
||||
|
||||
<table class="table" id="datatable" col-md-10>
|
||||
<table class="table " id="datatable">
|
||||
|
||||
<td >Номер в Инв. вед</td>
|
||||
<td >Инв. номер</td>
|
||||
<td >Название</td>
|
||||
<td class="no-print">Аудитория</td>
|
||||
<td >Расположение</td>
|
||||
<th>Номер в Инв. вед</th>
|
||||
<th>Инв. номер</th>
|
||||
<th>Название</th>
|
||||
<th class="no-print">Аудитория</th>
|
||||
<th>Расположение</th>
|
||||
|
||||
|
||||
{% for item in res: %}
|
||||
{% for item in res %}
|
||||
|
||||
<td><input type="hidden" name="invnomer" value="{{ item[0] }}"> {{ item[0] }} </td>
|
||||
<td> {{ item[1] }} </td>
|
||||
|
||||
Reference in New Issue
Block a user