Compare commits
11 Commits
dev
...
61d12f4972
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61d12f4972 | ||
|
|
2ae18dea27 | ||
|
|
1950bd4d45 | ||
|
|
9491909f24 | ||
|
|
54534ee490 | ||
|
|
bef4af4644 | ||
|
|
35bd29c223 | ||
|
|
e2ff0f9a05 | ||
|
|
e428e7f762 | ||
|
|
3f91dc91ec | ||
|
|
779c256e7b |
17
.claude/settings.local.json
Normal file
17
.claude/settings.local.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(backend\\\\venv\\\\Scripts\\\\python.exe -m uvicorn backend.main:app --reload)",
|
||||||
|
"Bash(python:*)",
|
||||||
|
"Bash(backend/venv/Scripts/python.exe:*)",
|
||||||
|
"Bash(backend/venv/Scripts/pip.exe list:*)",
|
||||||
|
"Bash(backend/venv/Scripts/pip.exe install:*)",
|
||||||
|
"Bash(backend/venv/Scripts/uvicorn.exe backend.main:app)",
|
||||||
|
"Bash(curl -s http://localhost:8000/ping)",
|
||||||
|
"Bash(curl -s http://127.0.0.1:8000/ping)",
|
||||||
|
"Bash(taskkill /F /IM python.exe)",
|
||||||
|
"Bash(netstat -ano)",
|
||||||
|
"Bash(taskkill:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
142
INSPECTION_GUIDE.md
Normal file
142
INSPECTION_GUIDE.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# Руководство по использованию системы проверки оборудования
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Система проверки оборудования позволяет проводить инвентаризацию с использованием штрихкод-сканера или ручного ввода инвентарных номеров.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- ✅ Проверка оборудования по аудиториям или всего подряд
|
||||||
|
- ✅ Поддержка штрихкод-сканеров (работают как клавиатура)
|
||||||
|
- ✅ Автоматическое обновление времени при повторном сканировании
|
||||||
|
- ✅ Сохранение неизвестных номеров
|
||||||
|
- ✅ Отслеживание прогресса в реальном времени
|
||||||
|
- ✅ История всех проверок
|
||||||
|
- ✅ Доступ для всех авторизованных пользователей (включая viewer)
|
||||||
|
|
||||||
|
## Как использовать
|
||||||
|
|
||||||
|
### 1. Начать новую проверку
|
||||||
|
|
||||||
|
1. Войдите в систему
|
||||||
|
2. Перейдите в раздел "Проверка" в меню
|
||||||
|
3. (Опционально) Выберите аудиторию для проверки
|
||||||
|
- Если аудитория не выбрана, будет проверяться всё оборудование
|
||||||
|
4. Нажмите "Начать проверку"
|
||||||
|
|
||||||
|
### 2. Сканирование оборудования
|
||||||
|
|
||||||
|
После начала проверки:
|
||||||
|
|
||||||
|
1. Фокус автоматически установлен на поле ввода
|
||||||
|
2. Отсканируйте штрихкод или введите инвентарный номер вручную
|
||||||
|
3. Нажмите Enter (или сканер сделает это автоматически)
|
||||||
|
4. Система покажет результат:
|
||||||
|
- ✅ **Зелёное уведомление** - оборудование найдено
|
||||||
|
- ❌ **Красное уведомление** - номер не найден (сохранён в неизвестные)
|
||||||
|
5. Продолжайте сканирование следующих позиций
|
||||||
|
|
||||||
|
### 3. Отслеживание прогресса
|
||||||
|
|
||||||
|
Во время проверки отображается:
|
||||||
|
|
||||||
|
- **Проверено** - количество отсканированных позиций
|
||||||
|
- **Всего** - ожидаемое количество оборудования
|
||||||
|
- **Не найдено** - количество неизвестных штрихкодов
|
||||||
|
- **Прогресс** - процент завершения
|
||||||
|
|
||||||
|
### 4. Завершение проверки
|
||||||
|
|
||||||
|
1. После завершения сканирования нажмите "Завершить проверку"
|
||||||
|
2. Подтвердите действие
|
||||||
|
3. Проверка будет сохранена в историю
|
||||||
|
|
||||||
|
### 5. Просмотр истории
|
||||||
|
|
||||||
|
1. На главном экране проверок нажмите "Загрузить историю"
|
||||||
|
2. Выберите нужную проверку
|
||||||
|
3. Нажмите "Детали" для просмотра отчёта
|
||||||
|
|
||||||
|
## Работа со сканером штрихкодов
|
||||||
|
|
||||||
|
### Подключение
|
||||||
|
|
||||||
|
1. Подключите USB штрихкод-сканер к компьютеру
|
||||||
|
2. Сканер работает как клавиатура - не требует драйверов
|
||||||
|
3. Откройте страницу проверки в браузере
|
||||||
|
|
||||||
|
### Сканирование
|
||||||
|
|
||||||
|
1. Убедитесь, что фокус на поле ввода
|
||||||
|
2. Наведите сканер на штрихкод
|
||||||
|
3. Нажмите кнопку сканирования
|
||||||
|
4. Сканер введёт номер и нажмёт Enter автоматически
|
||||||
|
|
||||||
|
### Советы
|
||||||
|
|
||||||
|
- После каждого сканирования фокус автоматически возвращается на поле ввода
|
||||||
|
- Можно сканировать штрихкоды быстро один за другим
|
||||||
|
- Уведомления о результатах исчезают автоматически через 3 секунды
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
### Повторное сканирование
|
||||||
|
|
||||||
|
- При повторном сканировании одного и того же оборудования обновляется только время проверки
|
||||||
|
- В таблице остаётся одна запись на единицу оборудования
|
||||||
|
|
||||||
|
### Неизвестные номера
|
||||||
|
|
||||||
|
- Если номер не найден в базе данных, он сохраняется в список неизвестных
|
||||||
|
- Неизвестные номера отображаются отдельным списком
|
||||||
|
- Это помогает выявить ошибки маркировки или новое оборудование
|
||||||
|
|
||||||
|
### Доступ
|
||||||
|
|
||||||
|
- Все авторизованные пользователи могут проводить проверки
|
||||||
|
- Проверки можно просматривать, но нельзя удалять
|
||||||
|
- Каждая проверка привязана к пользователю, который её создал
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
Система предоставляет следующие API эндпоинты:
|
||||||
|
|
||||||
|
- `POST /inspections/sessions` - Начать новую проверку
|
||||||
|
- `POST /inspections/sessions/{id}/check` - Сканировать штрихкод
|
||||||
|
- `GET /inspections/sessions/{id}` - Получить статистику проверки
|
||||||
|
- `POST /inspections/sessions/{id}/complete` - Завершить проверку
|
||||||
|
- `GET /inspections/sessions` - Список всех проверок
|
||||||
|
- `GET /inspections/sessions/{id}/records` - Детальный отчёт
|
||||||
|
|
||||||
|
Полная документация доступна по адресу: http://localhost:8000/docs
|
||||||
|
|
||||||
|
## Устранение неполадок
|
||||||
|
|
||||||
|
### Сканер не работает
|
||||||
|
|
||||||
|
1. Проверьте USB подключение
|
||||||
|
2. Убедитесь, что фокус на поле ввода
|
||||||
|
3. Попробуйте отсканировать в текстовый редактор для проверки
|
||||||
|
4. Проверьте настройки сканера (должен добавлять Enter в конце)
|
||||||
|
|
||||||
|
### Оборудование не находится
|
||||||
|
|
||||||
|
1. Проверьте инвентарный номер в базе данных
|
||||||
|
2. Убедитесь, что штрихкод читается правильно
|
||||||
|
3. Проверьте список неизвестных штрихкодов
|
||||||
|
|
||||||
|
### Прогресс не обновляется
|
||||||
|
|
||||||
|
1. Нажмите кнопку "Обновить данные"
|
||||||
|
2. Проверьте подключение к серверу
|
||||||
|
3. Обновите страницу браузера
|
||||||
|
|
||||||
|
## База данных
|
||||||
|
|
||||||
|
Система использует три новые таблицы:
|
||||||
|
|
||||||
|
1. **inspection_sessions** - Сессии проверок
|
||||||
|
2. **inspection_records** - Записи о проверенном оборудовании
|
||||||
|
3. **unknown_barcodes** - Неизвестные штрихкоды
|
||||||
|
|
||||||
|
Таблицы создаются автоматически при запуске скрипта `create_inspection_tables.py`.
|
||||||
344
app.py
344
app.py
@@ -1,344 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from flask import Flask, render_template, redirect, url_for, request, jsonify
|
|
||||||
from models import db, Oboruds, Auditory, Zametki
|
|
||||||
from flask_migrate import Migrate
|
|
||||||
from datetime import *
|
|
||||||
import csv
|
|
||||||
import random
|
|
||||||
from urllib.parse import unquote
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///project.db"
|
|
||||||
app.jinja_env.auto_reload = True
|
|
||||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
|
||||||
|
|
||||||
app.secret_key = '6523e58bc0eec42c31b9635d5e0dfc23b6d119b73e633bf3a5284c79bb4a1ede'
|
|
||||||
|
|
||||||
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 = []
|
|
||||||
results1 = []
|
|
||||||
all_aud = db.session.query(Auditory).all()
|
|
||||||
auds = []
|
|
||||||
for item in all_aud:
|
|
||||||
auds.append(item.audnazvanie)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
p = request.form.get('srch').strip()
|
|
||||||
all_aud = db.session.query(Auditory).all()
|
|
||||||
s = db.session.query(Oboruds).filter(
|
|
||||||
Oboruds.invNumber.contains(p)).first()
|
|
||||||
if s:
|
|
||||||
for item in all_aud:
|
|
||||||
auds.append(item.audnazvanie)
|
|
||||||
if s.aud_id is None:
|
|
||||||
results.append([s.invNumber, s.nazvanie])
|
|
||||||
else:
|
|
||||||
results1.append(s.invNumber)
|
|
||||||
results1.append(s.nazvanie)
|
|
||||||
aud = db.session.get(Auditory, s.aud_id)
|
|
||||||
results1.append(aud.audnazvanie)
|
|
||||||
return render_template('index.html', aud=all_aud, results=results, res1=results1)
|
|
||||||
return render_template('index.html', aud=all_aud, results=results, res1=results1)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/perenos", methods=['GET', 'POST'])
|
|
||||||
def perenos():
|
|
||||||
audid = request.args.get('audid')
|
|
||||||
invnomer = request.args.get('invnum')
|
|
||||||
ob = db.session.query(Oboruds).filter_by(invNumber=invnomer).first()
|
|
||||||
ob.aud_id = audid
|
|
||||||
db.session.commit()
|
|
||||||
return jsonify({'success': True}, 200, {'ContentType': 'application/json'})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/addaud", methods=['GET', 'POST'])
|
|
||||||
def addAud():
|
|
||||||
return render_template('addaud.html', methods=['GET', 'POST'])
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/searchonaud', methods=['GET', 'POST'])
|
|
||||||
def searchonaud():
|
|
||||||
all_aud = db.session.query(Auditory).all()
|
|
||||||
res = []
|
|
||||||
if request.method == 'GET' and request.args.get('auditory'):
|
|
||||||
audid = request.args.get('auditory')
|
|
||||||
q = db.session.query(Auditory, Oboruds).filter(
|
|
||||||
Auditory.id == Oboruds.aud_id
|
|
||||||
).filter(Auditory.id == audid
|
|
||||||
).order_by(Oboruds.invNumber).all()
|
|
||||||
results = []
|
|
||||||
for auditory, oboruds in q:
|
|
||||||
results.append({
|
|
||||||
'num_ved': oboruds.numberved,
|
|
||||||
'auditory_id': auditory.id,
|
|
||||||
'auditory_name': auditory.audnazvanie,
|
|
||||||
'inv_number': oboruds.invNumber,
|
|
||||||
'oboruds_id': oboruds.nazvanie,
|
|
||||||
'raspolog': oboruds.raspologenie,
|
|
||||||
})
|
|
||||||
return jsonify(results)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return render_template('searchonaud.html', aud=all_aud, res=res)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/addaudtodb", methods=['GET', 'POST'])
|
|
||||||
def addaudtodb():
|
|
||||||
if request.method == 'POST':
|
|
||||||
aud = request.form.get('auditory')
|
|
||||||
db.session.add(Auditory(audnazvanie=aud))
|
|
||||||
db.session.commit()
|
|
||||||
return redirect(url_for('addAud'))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/addoborudtodb", methods=['GET', 'POST'])
|
|
||||||
def addoborudtodb():
|
|
||||||
res = []
|
|
||||||
if request.method == 'POST':
|
|
||||||
audid = request.form.get('auditory')
|
|
||||||
invnomer = request.form.get('invnomer')
|
|
||||||
ob = db.session.query(Oboruds).filter_by(invNumber=invnomer).first()
|
|
||||||
ob.aud_id = audid
|
|
||||||
db.session.commit()
|
|
||||||
return redirect(url_for('index'))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/all')
|
|
||||||
def alloborud():
|
|
||||||
return render_template('all.html')
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/getall')
|
|
||||||
def getall():
|
|
||||||
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(results)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/vneaud', methods=['GET', 'POST'])
|
|
||||||
def vneaud():
|
|
||||||
res = []
|
|
||||||
data = db.session.query(Oboruds).filter(Oboruds.aud_id == None).all()
|
|
||||||
|
|
||||||
ak = db.session.query(Oboruds).all()
|
|
||||||
|
|
||||||
for dt in data:
|
|
||||||
res.append([dt.invNumber, dt.nazvanie])
|
|
||||||
|
|
||||||
return render_template('vneaud.html', res=res, kolvo=len(data), all_kol=len(ak))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/zametki', methods=['GET', 'POST'])
|
|
||||||
def zametki():
|
|
||||||
zam = db.session.query(Zametki).filter(Zametki.rmdt == None).all()
|
|
||||||
if request.method == 'POST':
|
|
||||||
textzam = request.form.get('textzam')
|
|
||||||
timeadd = datetime.now(timezone.utc)
|
|
||||||
if len(textzam) > 0:
|
|
||||||
db.session.add(Zametki(txtzam=textzam, created_date=timeadd))
|
|
||||||
db.session.commit()
|
|
||||||
return redirect(url_for('zametki'))
|
|
||||||
|
|
||||||
return render_template('zametki.html', zam=zam)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/zamrm', methods=['GET', 'POST'])
|
|
||||||
def js2():
|
|
||||||
zmid = request.args.get('zmid')
|
|
||||||
ob = db.session.query(Zametki).filter_by(id=zmid).first()
|
|
||||||
ob.rmdt = datetime.now(timezone.utc)
|
|
||||||
db.session.commit()
|
|
||||||
return jsonify({'success': True}), 200, {'ContentType': 'application/json'}
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/zamsearch', methods=['GET', 'POST'])
|
|
||||||
def zamsearch():
|
|
||||||
p = request.form.get('srch')
|
|
||||||
|
|
||||||
searchedZam = db.session.query(Zametki).filter(
|
|
||||||
Zametki.txtzam.contains(p)).ll()
|
|
||||||
zam = []
|
|
||||||
for item in searchedZam:
|
|
||||||
zam.append([item.txtzam, item.created_date])
|
|
||||||
return render_template('zametki.html', zam=zam)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/addraspved', methods=['GET', 'POST'])
|
|
||||||
def addraspved():
|
|
||||||
if request.method == 'POST':
|
|
||||||
query_string = request.data.decode()
|
|
||||||
|
|
||||||
un_query_string = unquote(unquote(query_string)).split(',')
|
|
||||||
|
|
||||||
ob = db.session.query(Oboruds).filter_by(invNumber=un_query_string[0]).first()
|
|
||||||
|
|
||||||
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 is None).all()) > 0:
|
|
||||||
audid = random.choice(db.session.query(Auditory).all())
|
|
||||||
oborud = random.choice(db.session.query(Oboruds).filter(Oboruds.aud_id is None).all())
|
|
||||||
oborud.aud_id = audid.id
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def createdb():
|
|
||||||
with app.app_context():
|
|
||||||
db.create_all()
|
|
||||||
auds = ['519', '521', '521a', '522', '523',
|
|
||||||
'601л',
|
|
||||||
'602л',
|
|
||||||
'603л',
|
|
||||||
'604л',
|
|
||||||
'605л',
|
|
||||||
'606л',
|
|
||||||
'607л',
|
|
||||||
'608л',
|
|
||||||
'609л',
|
|
||||||
'610л',
|
|
||||||
'611л',
|
|
||||||
'612л',
|
|
||||||
'613л',
|
|
||||||
'616л',
|
|
||||||
'617л',
|
|
||||||
'618л',
|
|
||||||
'619л',
|
|
||||||
'620л',
|
|
||||||
'621л',
|
|
||||||
'622л',
|
|
||||||
'626л',
|
|
||||||
'627л',
|
|
||||||
'703л',
|
|
||||||
'710л',
|
|
||||||
'713л']
|
|
||||||
for aud in auds:
|
|
||||||
db.session.add(Auditory(audnazvanie=aud))
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
with open('inventMavrin.csv', encoding='cp1251') as csv_file:
|
|
||||||
csv_reader = csv.reader(csv_file, delimiter=';')
|
|
||||||
for row in csv_reader:
|
|
||||||
db.session.add(
|
|
||||||
Oboruds(invNumber=row[0], nazvanie=row[1], typeBalanse='баланс'))
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
app.run(debug=True, host='0.0.0.0', port='3800')
|
|
||||||
11
backend/.claude/settings.local.json
Normal file
11
backend/.claude/settings.local.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(.venvScriptsactivate)",
|
||||||
|
"Bash(python:*)",
|
||||||
|
"Bash(venv/Scripts/python.exe:*)",
|
||||||
|
"Bash(timeout 5 tail:*)",
|
||||||
|
"Bash(curl:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
28
backend/create_inspection_tables.py
Normal file
28
backend/create_inspection_tables.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
Скрипт для создания таблиц системы проверок оборудования.
|
||||||
|
Создаёт только новые таблицы, не затрагивая существующие.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from backend.database import engine
|
||||||
|
from backend.models import Base, InspectionSession, InspectionRecord, UnknownBarcode
|
||||||
|
|
||||||
|
|
||||||
|
def create_inspection_tables():
|
||||||
|
"""Создать таблицы для системы проверок"""
|
||||||
|
print("Creating inspection tables...")
|
||||||
|
|
||||||
|
# Создать только новые таблицы
|
||||||
|
Base.metadata.create_all(bind=engine, tables=[
|
||||||
|
InspectionSession.__table__,
|
||||||
|
InspectionRecord.__table__,
|
||||||
|
UnknownBarcode.__table__
|
||||||
|
])
|
||||||
|
|
||||||
|
print("Inspection tables created successfully!")
|
||||||
|
print("- inspection_sessions")
|
||||||
|
print("- inspection_records")
|
||||||
|
print("- unknown_barcodes")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_inspection_tables()
|
||||||
@@ -1,5 +1,26 @@
|
|||||||
from backend.models import Base
|
from sqlalchemy.orm import sessionmaker
|
||||||
from backend.database import engine
|
|
||||||
|
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()
|
||||||
|
|
||||||
Base.metadata.drop_all(bind=engine) # Опционально, если вдруг есть
|
|
||||||
Base.metadata.create_all(bind=engine)
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# backend/main.py
|
# backend/main.py
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
from starlette.staticfiles import StaticFiles
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from backend.routers.equipment_types import equipment_types
|
from backend.routers.equipment_types import equipment_types
|
||||||
@@ -9,6 +10,9 @@ from backend.routers.oboruds import oboruds
|
|||||||
from backend.routers.components import components
|
from backend.routers.components import components
|
||||||
from backend.routers.rashodniki import consumables
|
from backend.routers.rashodniki import consumables
|
||||||
from backend.routers.zametki import zametki
|
from backend.routers.zametki import zametki
|
||||||
|
from backend.routers.auth import auth
|
||||||
|
from backend.routers.owners import owners
|
||||||
|
from backend.routers.inspections import inspections
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -29,9 +33,16 @@ def ping():
|
|||||||
return {"message": "pong"}
|
return {"message": "pong"}
|
||||||
|
|
||||||
|
|
||||||
|
# Serve frontend
|
||||||
|
app.mount("/app", StaticFiles(directory="frontend", html=True), name="frontend")
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
return RedirectResponse(url="/docs")
|
return RedirectResponse(url="/app/")
|
||||||
|
|
||||||
|
@app.get("/login")
|
||||||
|
def login_page():
|
||||||
|
return RedirectResponse(url="/app/login.html")
|
||||||
|
|
||||||
|
|
||||||
# Подключение роутов
|
# Подключение роутов
|
||||||
@@ -41,3 +52,6 @@ app.include_router(oboruds)
|
|||||||
app.include_router(components)
|
app.include_router(components)
|
||||||
app.include_router(consumables)
|
app.include_router(consumables)
|
||||||
app.include_router(zametki)
|
app.include_router(zametki)
|
||||||
|
app.include_router(auth)
|
||||||
|
app.include_router(owners)
|
||||||
|
app.include_router(inspections)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import sys
|
import sys
|
||||||
from sqlalchemy import create_engine, text
|
from sqlalchemy import create_engine, text, inspect
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from backend.database import SessionLocal as NewSession
|
from backend.database import SessionLocal as NewSession, engine as new_engine
|
||||||
from backend import models
|
from backend import models
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
OLD_DB_URL = "sqlite:///./backend/old_app.db"
|
|
||||||
|
OLD_DB_URL = "sqlite:///./instance/project.-10-11-25.db"
|
||||||
old_engine = create_engine(OLD_DB_URL, connect_args={"check_same_thread": False})
|
old_engine = create_engine(OLD_DB_URL, connect_args={"check_same_thread": False})
|
||||||
OldSession = sessionmaker(bind=old_engine)
|
OldSession = sessionmaker(bind=old_engine)
|
||||||
old_db = OldSession()
|
old_db = OldSession()
|
||||||
@@ -14,7 +16,25 @@ new_db = NewSession()
|
|||||||
def log(msg: str):
|
def log(msg: str):
|
||||||
print(f"[INFO] {msg}", file=sys.stderr)
|
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():
|
def migrate():
|
||||||
|
ensure_schema()
|
||||||
log("Запуск переноса данных из old_app.db → app.db")
|
log("Запуск переноса данных из old_app.db → app.db")
|
||||||
|
|
||||||
# Тип оборудования по умолчанию
|
# Тип оборудования по умолчанию
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
# backend/models.py
|
# backend/models.py
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, UniqueConstraint
|
||||||
from sqlalchemy.orm import relationship, declarative_base
|
from sqlalchemy.orm import relationship, declarative_base
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
Base = declarative_base()
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -28,6 +37,14 @@ class EquipmentType(Base):
|
|||||||
|
|
||||||
oboruds = relationship("Oboruds", back_populates="type")
|
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):
|
class Oboruds(Base):
|
||||||
__tablename__ = 'oboruds'
|
__tablename__ = 'oboruds'
|
||||||
@@ -45,6 +62,9 @@ class Oboruds(Base):
|
|||||||
type_id = Column(Integer, ForeignKey("equipment_types.id"))
|
type_id = Column(Integer, ForeignKey("equipment_types.id"))
|
||||||
type = relationship("EquipmentType", back_populates="oboruds")
|
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")
|
components = relationship("Component", back_populates="oborud")
|
||||||
consumables = relationship("Consumable", back_populates="oborud")
|
consumables = relationship("Consumable", back_populates="oborud")
|
||||||
|
|
||||||
@@ -78,3 +98,47 @@ class Zametki(Base):
|
|||||||
txtzam = Column(String(10000))
|
txtzam = Column(String(10000))
|
||||||
created_date = Column(DateTime, default=datetime.datetime.utcnow)
|
created_date = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
rmdt = Column(DateTime)
|
rmdt = Column(DateTime)
|
||||||
|
|
||||||
|
|
||||||
|
class InspectionSession(Base):
|
||||||
|
__tablename__ = 'inspection_sessions'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
started_at = Column(DateTime, nullable=False)
|
||||||
|
completed_at = Column(DateTime, nullable=True)
|
||||||
|
aud_id = Column(Integer, ForeignKey("auditories.id"), nullable=True) # null = всё подряд
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user = relationship("User")
|
||||||
|
auditory = relationship("Auditory")
|
||||||
|
records = relationship("InspectionRecord", back_populates="session")
|
||||||
|
unknown_barcodes = relationship("UnknownBarcode", back_populates="session")
|
||||||
|
|
||||||
|
|
||||||
|
class InspectionRecord(Base):
|
||||||
|
__tablename__ = 'inspection_records'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
session_id = Column(Integer, ForeignKey("inspection_sessions.id"), nullable=False)
|
||||||
|
oborud_id = Column(Integer, ForeignKey("oboruds.id"), nullable=False)
|
||||||
|
checked_at = Column(DateTime, nullable=False) # обновляется при повторном сканировании
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
session = relationship("InspectionSession", back_populates="records")
|
||||||
|
oborud = relationship("Oboruds")
|
||||||
|
|
||||||
|
# Уникальное ограничение: одна запись на оборудование в рамках сессии
|
||||||
|
__table_args__ = (UniqueConstraint('session_id', 'oborud_id', name='_session_oborud_uc'),)
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownBarcode(Base):
|
||||||
|
__tablename__ = 'unknown_barcodes'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
session_id = Column(Integer, ForeignKey("inspection_sessions.id"), nullable=False)
|
||||||
|
barcode = Column(String(100), nullable=False)
|
||||||
|
scanned_at = Column(DateTime, nullable=False)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
session = relationship("InspectionSession", back_populates="unknown_barcodes")
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from .. import models, schemas, database
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
auditories = APIRouter(prefix="/auditories", tags=["auditories"])
|
auditories = APIRouter(prefix="/auditories", tags=["auditories"])
|
||||||
|
|
||||||
@auditories.post("/", response_model=schemas.AuditoryRead)
|
@auditories.post("/", response_model=schemas.AuditoryRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
def create_auditory(item: schemas.AuditoryCreate, db: Session = Depends(database.get_db)):
|
def create_auditory(item: schemas.AuditoryCreate, db: Session = Depends(database.get_db)):
|
||||||
obj = models.Auditory(**item.dict())
|
obj = models.Auditory(**item.dict())
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
|
|||||||
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
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from .. import models, schemas, database
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
|
|
||||||
components = APIRouter(prefix="/components", tags=["components"])
|
components = APIRouter(prefix="/components", tags=["components"])
|
||||||
|
|
||||||
@components.post("/", response_model=schemas.ComponentRead)
|
@components.post("/", response_model=schemas.ComponentRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
def create_component(item: schemas.ComponentCreate, db: Session = Depends(database.get_db)):
|
def create_component(item: schemas.ComponentCreate, db: Session = Depends(database.get_db)):
|
||||||
obj = models.Component(**item.dict())
|
obj = models.Component(**item.dict())
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from .. import models, schemas, database
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
equipment_types = APIRouter(prefix="/equipment-types", tags=["equipment_types"])
|
equipment_types = APIRouter(prefix="/equipment-types", tags=["equipment_types"])
|
||||||
|
|
||||||
@equipment_types.post("/", response_model=schemas.EquipmentTypeRead)
|
@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)):
|
def create_equipment_type(item: schemas.EquipmentTypeCreate, db: Session = Depends(database.get_db)):
|
||||||
obj = models.EquipmentType(**item.dict())
|
obj = models.EquipmentType(**item.dict())
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
|
|||||||
214
backend/routers/inspections.py
Normal file
214
backend/routers/inspections.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
from .. import schemas, database, models
|
||||||
|
from ..security import get_current_user
|
||||||
|
|
||||||
|
|
||||||
|
inspections = APIRouter(prefix="/inspections", tags=["inspections"])
|
||||||
|
|
||||||
|
|
||||||
|
@inspections.post("/sessions", response_model=schemas.InspectionSessionRead)
|
||||||
|
async def create_inspection_session(
|
||||||
|
payload: schemas.InspectionSessionCreate,
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Начать новую сессию проверки"""
|
||||||
|
session = models.InspectionSession(
|
||||||
|
user_id=current_user.id,
|
||||||
|
started_at=datetime.now(timezone.utc),
|
||||||
|
aud_id=payload.aud_id
|
||||||
|
)
|
||||||
|
db.add(session)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(session)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
class CheckBarcodeRequest(schemas.BaseModel):
|
||||||
|
inv_number: str
|
||||||
|
|
||||||
|
|
||||||
|
@inspections.post("/sessions/{session_id}/check", response_model=schemas.CheckBarcodeResponse)
|
||||||
|
async def check_barcode(
|
||||||
|
session_id: int,
|
||||||
|
payload: CheckBarcodeRequest,
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Отсканировать штрихкод"""
|
||||||
|
inv_number = payload.inv_number
|
||||||
|
# Проверка существования сессии
|
||||||
|
session = db.query(models.InspectionSession).filter(models.InspectionSession.id == session_id).first()
|
||||||
|
if not session:
|
||||||
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
|
# Проверка, что сессия не завершена
|
||||||
|
if session.completed_at is not None:
|
||||||
|
raise HTTPException(status_code=400, detail="Session already completed")
|
||||||
|
|
||||||
|
# Поиск оборудования по invNumber (точное совпадение)
|
||||||
|
# invNumber может быть строкой или числом, преобразуем для поиска
|
||||||
|
try:
|
||||||
|
inv_num_int = int(inv_number)
|
||||||
|
oborud = db.query(models.Oboruds).filter(models.Oboruds.invNumber == inv_num_int).first()
|
||||||
|
except ValueError:
|
||||||
|
oborud = None
|
||||||
|
|
||||||
|
if oborud:
|
||||||
|
# Оборудование найдено - создаём/обновляем запись (UPSERT)
|
||||||
|
record = db.query(models.InspectionRecord).filter_by(
|
||||||
|
session_id=session_id,
|
||||||
|
oborud_id=oborud.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if record:
|
||||||
|
# Обновить время проверки
|
||||||
|
record.checked_at = datetime.now(timezone.utc)
|
||||||
|
else:
|
||||||
|
# Создать новую запись
|
||||||
|
record = models.InspectionRecord(
|
||||||
|
session_id=session_id,
|
||||||
|
oborud_id=oborud.id,
|
||||||
|
checked_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
db.add(record)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(oborud)
|
||||||
|
|
||||||
|
return schemas.CheckBarcodeResponse(
|
||||||
|
status="found",
|
||||||
|
equipment=schemas.OborudRead.model_validate(oborud),
|
||||||
|
message=f"Оборудование найдено: {oborud.nazvanie}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Оборудование не найдено - сохранить в неизвестные штрихкоды
|
||||||
|
unknown = models.UnknownBarcode(
|
||||||
|
session_id=session_id,
|
||||||
|
barcode=inv_number,
|
||||||
|
scanned_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
db.add(unknown)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return schemas.CheckBarcodeResponse(
|
||||||
|
status="not_found",
|
||||||
|
equipment=None,
|
||||||
|
message=f"Оборудование с номером {inv_number} не найдено"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inspections.get("/sessions/{session_id}", response_model=schemas.InspectionSessionStats)
|
||||||
|
async def get_inspection_session_stats(
|
||||||
|
session_id: int,
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Получить статистику сессии проверки"""
|
||||||
|
session = db.query(models.InspectionSession).filter(models.InspectionSession.id == session_id).first()
|
||||||
|
if not session:
|
||||||
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
|
# Подсчёт статистики
|
||||||
|
total_checked = db.query(models.InspectionRecord).filter(
|
||||||
|
models.InspectionRecord.session_id == session_id
|
||||||
|
).count()
|
||||||
|
|
||||||
|
total_unknown = db.query(models.UnknownBarcode).filter(
|
||||||
|
models.UnknownBarcode.session_id == session_id
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Подсчёт ожидаемого количества оборудования
|
||||||
|
if session.aud_id:
|
||||||
|
# Проверка по аудитории
|
||||||
|
total_expected = db.query(models.Oboruds).filter(
|
||||||
|
models.Oboruds.aud_id == session.aud_id
|
||||||
|
).count()
|
||||||
|
else:
|
||||||
|
# Проверка всего оборудования
|
||||||
|
total_expected = db.query(models.Oboruds).count()
|
||||||
|
|
||||||
|
# Расчёт прогресса
|
||||||
|
progress_percent = round((total_checked / total_expected * 100), 2) if total_expected > 0 else 0.0
|
||||||
|
|
||||||
|
return schemas.InspectionSessionStats(
|
||||||
|
session=schemas.InspectionSessionRead.model_validate(session),
|
||||||
|
total_expected=total_expected,
|
||||||
|
total_checked=total_checked,
|
||||||
|
total_unknown=total_unknown,
|
||||||
|
progress_percent=progress_percent
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inspections.post("/sessions/{session_id}/complete", response_model=schemas.InspectionSessionRead)
|
||||||
|
async def complete_inspection_session(
|
||||||
|
session_id: int,
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Завершить сессию проверки"""
|
||||||
|
session = db.query(models.InspectionSession).filter(models.InspectionSession.id == session_id).first()
|
||||||
|
if not session:
|
||||||
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
|
if session.completed_at is not None:
|
||||||
|
raise HTTPException(status_code=400, detail="Session already completed")
|
||||||
|
|
||||||
|
session.completed_at = datetime.now(timezone.utc)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(session)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
@inspections.get("/sessions", response_model=list[schemas.InspectionSessionRead])
|
||||||
|
async def list_inspection_sessions(
|
||||||
|
user_id: Optional[int] = None,
|
||||||
|
aud_id: Optional[int] = None,
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Получить список всех сессий проверок (история)"""
|
||||||
|
query = db.query(models.InspectionSession)
|
||||||
|
|
||||||
|
if user_id is not None:
|
||||||
|
query = query.filter(models.InspectionSession.user_id == user_id)
|
||||||
|
|
||||||
|
if aud_id is not None:
|
||||||
|
query = query.filter(models.InspectionSession.aud_id == aud_id)
|
||||||
|
|
||||||
|
# Сортировка по дате (новые сверху)
|
||||||
|
sessions = query.order_by(models.InspectionSession.started_at.desc()).all()
|
||||||
|
return sessions
|
||||||
|
|
||||||
|
|
||||||
|
@inspections.get("/sessions/{session_id}/records", response_model=schemas.InspectionDetailReport)
|
||||||
|
async def get_inspection_session_records(
|
||||||
|
session_id: int,
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Получить детальный отчёт по сессии проверки"""
|
||||||
|
session = db.query(models.InspectionSession).filter(models.InspectionSession.id == session_id).first()
|
||||||
|
if not session:
|
||||||
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
|
# Получить все записи проверок с информацией об оборудовании
|
||||||
|
records = db.query(models.InspectionRecord).filter(
|
||||||
|
models.InspectionRecord.session_id == session_id
|
||||||
|
).order_by(models.InspectionRecord.checked_at.desc()).all()
|
||||||
|
|
||||||
|
# Получить неизвестные штрихкоды
|
||||||
|
unknown_barcodes = db.query(models.UnknownBarcode).filter(
|
||||||
|
models.UnknownBarcode.session_id == session_id
|
||||||
|
).order_by(models.UnknownBarcode.scanned_at.desc()).all()
|
||||||
|
|
||||||
|
return schemas.InspectionDetailReport(
|
||||||
|
records=[schemas.InspectionRecordRead.model_validate(r) for r in records],
|
||||||
|
unknown_barcodes=[schemas.UnknownBarcodeRead.model_validate(ub) for ub in unknown_barcodes]
|
||||||
|
)
|
||||||
@@ -2,10 +2,11 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from .. import models, schemas, database
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
oboruds = APIRouter(prefix="/oboruds", tags=["oboruds"])
|
oboruds = APIRouter(prefix="/oboruds", tags=["oboruds"])
|
||||||
|
|
||||||
@oboruds.post("/", response_model=schemas.OborudRead)
|
@oboruds.post("/", response_model=schemas.OborudRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
def create_oborud(item: schemas.OborudCreate, db: Session = Depends(database.get_db)):
|
def create_oborud(item: schemas.OborudCreate, db: Session = Depends(database.get_db)):
|
||||||
obj = models.Oboruds(**item.dict())
|
obj = models.Oboruds(**item.dict())
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
@@ -14,15 +15,40 @@ def create_oborud(item: schemas.OborudCreate, db: Session = Depends(database.get
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@oboruds.get("/", response_model=list[schemas.OborudRead])
|
@oboruds.get("/", response_model=list[schemas.OborudRead])
|
||||||
def list_oboruds(aud_id: Optional[int] = None, db: Session = Depends(database.get_db)):
|
def list_oboruds(aud_id: Optional[int] = None, sort_by_inv: bool = False, unassigned: bool = False, inv_number: Optional[int] = None, db: Session = Depends(database.get_db)):
|
||||||
query = db.query(models.Oboruds)
|
query = db.query(models.Oboruds)
|
||||||
if aud_id is not None:
|
if unassigned:
|
||||||
|
query = query.filter(models.Oboruds.aud_id == None)
|
||||||
|
elif aud_id is not None:
|
||||||
query = query.filter(models.Oboruds.aud_id == aud_id)
|
query = query.filter(models.Oboruds.aud_id == aud_id)
|
||||||
|
if inv_number is not None:
|
||||||
|
query = query.filter(models.Oboruds.invNumber == inv_number)
|
||||||
|
if sort_by_inv:
|
||||||
|
query = query.order_by(models.Oboruds.invNumber.asc())
|
||||||
return query.all()
|
return query.all()
|
||||||
|
|
||||||
|
@oboruds.get("/stats")
|
||||||
|
def oboruds_stats(db: Session = Depends(database.get_db)):
|
||||||
|
total = db.query(models.Oboruds).count()
|
||||||
|
unassigned = db.query(models.Oboruds).filter(models.Oboruds.aud_id == None).count()
|
||||||
|
return {"total": total, "unassigned": unassigned}
|
||||||
|
|
||||||
@oboruds.get("/{oborud_id}", response_model=schemas.OborudRead)
|
@oboruds.get("/{oborud_id}", response_model=schemas.OborudRead)
|
||||||
def get_oborud(oborud_id: int, db: Session = Depends(database.get_db)):
|
def get_oborud(oborud_id: int, db: Session = Depends(database.get_db)):
|
||||||
obj = db.query(models.Oboruds).filter(models.Oboruds.id == oborud_id).first()
|
obj = db.query(models.Oboruds).filter(models.Oboruds.id == oborud_id).first()
|
||||||
if not obj:
|
if not obj:
|
||||||
raise HTTPException(status_code=404, detail="Oborud not found")
|
raise HTTPException(status_code=404, detail="Oborud not found")
|
||||||
return obj
|
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()
|
||||||
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from .. import models, schemas, database
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
consumables = APIRouter(prefix="/consumables", tags=["consumables"])
|
consumables = APIRouter(prefix="/consumables", tags=["consumables"])
|
||||||
|
|
||||||
@consumables.post("/", response_model=schemas.ConsumableRead)
|
@consumables.post("/", response_model=schemas.ConsumableRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
def create_consumable(item: schemas.ConsumableCreate, db: Session = Depends(database.get_db)):
|
def create_consumable(item: schemas.ConsumableCreate, db: Session = Depends(database.get_db)):
|
||||||
obj = models.Consumable(**item.dict())
|
obj = models.Consumable(**item.dict())
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from datetime import datetime, timezone
|
||||||
from .. import models, schemas, database
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
zametki = APIRouter(prefix="/zametki", tags=["zametki"])
|
zametki = APIRouter(prefix="/zametki", tags=["zametki"])
|
||||||
|
|
||||||
@zametki.post("/", response_model=schemas.ZametkaRead)
|
@zametki.post("/", response_model=schemas.ZametkaRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
def create_zametka(item: schemas.ZametkaCreate, db: Session = Depends(database.get_db)):
|
def create_zametka(item: schemas.ZametkaCreate, db: Session = Depends(database.get_db)):
|
||||||
obj = models.Zametki(**item.dict())
|
obj = models.Zametki(txtzam=item.txtzam, created_date=datetime.now(timezone.utc))
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(obj)
|
db.refresh(obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@zametki.get("/", response_model=list[schemas.ZametkaRead])
|
@zametki.get("/", response_model=list[schemas.ZametkaRead])
|
||||||
def list_zametki(db: Session = Depends(database.get_db)):
|
def list_zametki(active_only: bool = True, db: Session = Depends(database.get_db)):
|
||||||
return db.query(models.Zametki).all()
|
query = db.query(models.Zametki)
|
||||||
|
if active_only:
|
||||||
|
query = query.filter(models.Zametki.rmdt == None)
|
||||||
|
return query.order_by(models.Zametki.created_date.desc()).all()
|
||||||
|
|
||||||
|
@zametki.patch("/{zametka_id}/resolve", response_model=schemas.ZametkaRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
|
def resolve_zametka(zametka_id: int, db: Session = Depends(database.get_db)):
|
||||||
|
obj = db.query(models.Zametki).filter(models.Zametki.id == zametka_id).first()
|
||||||
|
if not obj:
|
||||||
|
raise HTTPException(status_code=404, detail="Zametka not found")
|
||||||
|
obj.rmdt = datetime.now(timezone.utc)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# backend/schemas.py
|
# backend/schemas.py
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional, List
|
from typing import Optional, List, Literal
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ class EquipmentTypeRead(EquipmentTypeBase):
|
|||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
# === Component ===
|
# === Component ===
|
||||||
@@ -31,7 +31,7 @@ class ComponentRead(ComponentBase):
|
|||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
# === Consumable ===
|
# === Consumable ===
|
||||||
@@ -46,30 +46,56 @@ class ConsumableRead(ConsumableBase):
|
|||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# === Owner ===
|
||||||
|
class OwnerBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class OwnerCreate(OwnerBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class OwnerRead(OwnerBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
# === Oborud ===
|
# === Oborud ===
|
||||||
class OborudBase(BaseModel):
|
class OborudBase(BaseModel):
|
||||||
invNumber: Optional[int]
|
invNumber: Optional[int] = None
|
||||||
nazvanie: str
|
nazvanie: str
|
||||||
raspologenie: Optional[str] = None
|
raspologenie: Optional[str] = None
|
||||||
numberppasu: Optional[str] = None
|
numberppasu: Optional[str] = None
|
||||||
kolichestvo: Optional[int] = None
|
kolichestvo: Optional[int] = None
|
||||||
aud_id: int
|
aud_id: Optional[int] = None
|
||||||
type_id: int
|
type_id: Optional[int] = None
|
||||||
|
owner_id: Optional[int] = None
|
||||||
|
|
||||||
class OborudCreate(OborudBase):
|
class OborudCreate(OborudBase):
|
||||||
pass
|
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):
|
class OborudRead(OborudBase):
|
||||||
id: int
|
id: int
|
||||||
type: EquipmentTypeRead
|
type: Optional[EquipmentTypeRead] = None
|
||||||
|
owner: Optional[OwnerRead] = None
|
||||||
components: List[ComponentRead] = []
|
components: List[ComponentRead] = []
|
||||||
consumables: List[ConsumableRead] = []
|
consumables: List[ConsumableRead] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
# === Auditory ===
|
# === Auditory ===
|
||||||
@@ -83,7 +109,7 @@ class AuditoryRead(AuditoryBase):
|
|||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
# === Zametka ===
|
# === Zametka ===
|
||||||
@@ -96,7 +122,96 @@ class ZametkaCreate(ZametkaBase):
|
|||||||
|
|
||||||
class ZametkaRead(ZametkaBase):
|
class ZametkaRead(ZametkaBase):
|
||||||
id: int
|
id: int
|
||||||
created_date: datetime
|
created_date: Optional[datetime] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = 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:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class UserRoleUpdate(BaseModel):
|
||||||
|
role: Role
|
||||||
|
|
||||||
|
|
||||||
|
# === Inspection System ===
|
||||||
|
|
||||||
|
# InspectionSession
|
||||||
|
class InspectionSessionBase(BaseModel):
|
||||||
|
aud_id: Optional[int] = None
|
||||||
|
|
||||||
|
class InspectionSessionCreate(InspectionSessionBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class InspectionSessionRead(InspectionSessionBase):
|
||||||
|
id: int
|
||||||
|
user_id: int
|
||||||
|
started_at: datetime
|
||||||
|
completed_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# InspectionRecord
|
||||||
|
class InspectionRecordRead(BaseModel):
|
||||||
|
id: int
|
||||||
|
session_id: int
|
||||||
|
oborud_id: int
|
||||||
|
checked_at: datetime
|
||||||
|
oborud: Optional[OborudRead] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# UnknownBarcode
|
||||||
|
class UnknownBarcodeRead(BaseModel):
|
||||||
|
id: int
|
||||||
|
session_id: int
|
||||||
|
barcode: str
|
||||||
|
scanned_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# Ответ на сканирование
|
||||||
|
class CheckBarcodeResponse(BaseModel):
|
||||||
|
status: Literal["found", "not_found"]
|
||||||
|
equipment: Optional[OborudRead] = None
|
||||||
|
message: str
|
||||||
|
|
||||||
|
# Статистика сессии
|
||||||
|
class InspectionSessionStats(BaseModel):
|
||||||
|
session: InspectionSessionRead
|
||||||
|
total_expected: int # всего оборудования (в аудитории или всего)
|
||||||
|
total_checked: int # проверено уникальных позиций
|
||||||
|
total_unknown: int # неизвестных штрихкодов
|
||||||
|
progress_percent: float
|
||||||
|
|
||||||
|
# Детальный отчёт
|
||||||
|
class InspectionDetailReport(BaseModel):
|
||||||
|
records: List[InspectionRecordRead]
|
||||||
|
unknown_barcodes: List[UnknownBarcodeRead]
|
||||||
|
|||||||
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
|
||||||
668
frontend/app.js
Normal file
668
frontend/app.js
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
const { createApp } = Vue;
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
auds: "/auditories/",
|
||||||
|
oboruds: (audId) => `/oboruds/?aud_id=${encodeURIComponent(audId)}`,
|
||||||
|
allOboruds: "/oboruds/?sort_by_inv=true",
|
||||||
|
owners: "/owners/",
|
||||||
|
zametki: "/zametki/",
|
||||||
|
};
|
||||||
|
|
||||||
|
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: 'home',
|
||||||
|
auditories: [],
|
||||||
|
selectedAudId: '',
|
||||||
|
oboruds: [],
|
||||||
|
allOboruds: [],
|
||||||
|
unassignedOboruds: [],
|
||||||
|
totalOboruds: 0,
|
||||||
|
status: '',
|
||||||
|
error: '',
|
||||||
|
printTitle: '',
|
||||||
|
// home view
|
||||||
|
homeSearch: '',
|
||||||
|
homeUnassigned: [],
|
||||||
|
homeSearchResults: [],
|
||||||
|
homeSearchDone: false,
|
||||||
|
// auth/user management
|
||||||
|
token: '',
|
||||||
|
role: '',
|
||||||
|
users: [],
|
||||||
|
newAdminUsername: '',
|
||||||
|
newAdminPassword: '',
|
||||||
|
newAudName: '',
|
||||||
|
owners: [],
|
||||||
|
newOwnerName: '',
|
||||||
|
zametki: [],
|
||||||
|
newZametkaText: '',
|
||||||
|
equipmentTypes: [],
|
||||||
|
newTypeName: '',
|
||||||
|
newEquipment: {
|
||||||
|
invNumber: null,
|
||||||
|
nazvanie: '',
|
||||||
|
raspologenie: '',
|
||||||
|
kolichestvo: 1,
|
||||||
|
aud_id: '',
|
||||||
|
type_id: '',
|
||||||
|
owner_id: ''
|
||||||
|
},
|
||||||
|
// Inspection
|
||||||
|
activeInspection: null,
|
||||||
|
inspectionAudId: '',
|
||||||
|
scannedBarcode: '',
|
||||||
|
lastScanResult: null,
|
||||||
|
inspectionStats: {
|
||||||
|
total_checked: 0,
|
||||||
|
total_expected: 0,
|
||||||
|
total_unknown: 0,
|
||||||
|
progress_percent: 0
|
||||||
|
},
|
||||||
|
checkedEquipment: [],
|
||||||
|
unknownBarcodes: [],
|
||||||
|
inspectionHistory: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isAuth() { return !!this.token; },
|
||||||
|
isAdmin() { return this.role === 'admin'; },
|
||||||
|
isEditor() { return this.role === 'editor'; },
|
||||||
|
canEdit() { return this.isAdmin || this.isEditor; },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async showHome() {
|
||||||
|
this.view = 'home';
|
||||||
|
this.status = '';
|
||||||
|
this.error = '';
|
||||||
|
this.homeSearch = '';
|
||||||
|
this.homeSearchResults = [];
|
||||||
|
this.homeSearchDone = false;
|
||||||
|
await this.loadHomeUnassigned();
|
||||||
|
},
|
||||||
|
async loadHomeUnassigned() {
|
||||||
|
this.status = 'Загрузка нераспределённого оборудования…';
|
||||||
|
this.error = '';
|
||||||
|
try {
|
||||||
|
const data = await fetchJSON('/oboruds/?unassigned=true&sort_by_inv=true');
|
||||||
|
this.homeUnassigned = data.map(it => ({ ...it, selectedAudId: '' }));
|
||||||
|
this.status = '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось загрузить данные';
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async doHomeSearch() {
|
||||||
|
const q = this.homeSearch.trim();
|
||||||
|
if (!q) {
|
||||||
|
this.status = 'Введите инвентарный номер';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.status = 'Поиск…';
|
||||||
|
this.error = '';
|
||||||
|
this.homeSearchDone = false;
|
||||||
|
try {
|
||||||
|
const data = await fetchJSON(`/oboruds/?inv_number=${encodeURIComponent(q)}`);
|
||||||
|
this.homeSearchResults = data.filter(it => it.aud_id).map(it => ({ ...it, selectedAudId: '' }));
|
||||||
|
this.homeSearchDone = true;
|
||||||
|
this.status = '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось выполнить поиск';
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async assignToAuditory(item) {
|
||||||
|
if (!item.selectedAudId) {
|
||||||
|
this.status = 'Выберите аудиторию';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.status = 'Сохранение…';
|
||||||
|
this.error = '';
|
||||||
|
await this.fetchAuth(`/oboruds/${item.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ aud_id: item.selectedAudId }),
|
||||||
|
});
|
||||||
|
await this.loadHomeUnassigned();
|
||||||
|
this.status = 'Сохранено';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось назначить аудиторию';
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 loadAllOboruds() {
|
||||||
|
this.status = 'Загрузка всего оборудования…';
|
||||||
|
this.error = '';
|
||||||
|
try {
|
||||||
|
this.allOboruds = await fetchJSON(api.allOboruds);
|
||||||
|
this.status = `Загружено ${this.allOboruds.length} записей`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось загрузить оборудование';
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async showAllEquipment() {
|
||||||
|
this.view = 'allEquipment';
|
||||||
|
await this.loadAllOboruds();
|
||||||
|
},
|
||||||
|
async loadUnassigned() {
|
||||||
|
this.status = 'Загрузка нераспределённого оборудования…';
|
||||||
|
this.error = '';
|
||||||
|
try {
|
||||||
|
const [oboruds, stats] = await Promise.all([
|
||||||
|
fetchJSON('/oboruds/?unassigned=true&sort_by_inv=true'),
|
||||||
|
fetchJSON('/oboruds/stats')
|
||||||
|
]);
|
||||||
|
this.unassignedOboruds = oboruds;
|
||||||
|
this.totalOboruds = stats.total;
|
||||||
|
this.status = '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось загрузить данные';
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async showUnassigned() {
|
||||||
|
this.view = 'unassigned';
|
||||||
|
await this.loadUnassigned();
|
||||||
|
},
|
||||||
|
getAuditoryName(audId) {
|
||||||
|
if (!audId) return '';
|
||||||
|
const aud = this.auditories.find(a => a.id === audId);
|
||||||
|
return aud ? aud.audnazvanie : '';
|
||||||
|
},
|
||||||
|
printPage() {
|
||||||
|
const aud = this.auditories.find(a => a.id === this.selectedAudId);
|
||||||
|
this.printTitle = 'Аудитория № ' + (aud ? aud.audnazvanie : '');
|
||||||
|
this.$nextTick(() => {
|
||||||
|
window.print();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
printAllEquipment() {
|
||||||
|
window.print();
|
||||||
|
},
|
||||||
|
async loadZametki() {
|
||||||
|
this.status = 'Загрузка заметок…';
|
||||||
|
this.error = '';
|
||||||
|
try {
|
||||||
|
this.zametki = await fetchJSON(api.zametki);
|
||||||
|
this.status = '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось загрузить заметки';
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async showZametki() {
|
||||||
|
this.view = 'zametki';
|
||||||
|
await this.loadZametki();
|
||||||
|
},
|
||||||
|
async createZametka() {
|
||||||
|
if (!this.newZametkaText.trim()) {
|
||||||
|
this.status = 'Введите текст заметки';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.status = 'Добавление заметки…';
|
||||||
|
await this.fetchAuth(api.zametki, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ txtzam: this.newZametkaText }),
|
||||||
|
});
|
||||||
|
this.newZametkaText = '';
|
||||||
|
await this.loadZametki();
|
||||||
|
this.status = 'Заметка добавлена';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось добавить заметку';
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async resolveZametka(id) {
|
||||||
|
try {
|
||||||
|
this.status = 'Отметка заметки как решённой…';
|
||||||
|
await this.fetchAuth(`/zametki/${id}/resolve`, { method: 'PATCH' });
|
||||||
|
await this.loadZametki();
|
||||||
|
this.status = 'Заметка отмечена как решённая';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось отметить заметку';
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
return d.toLocaleString('ru-RU');
|
||||||
|
},
|
||||||
|
async loadEquipmentTypes() {
|
||||||
|
try {
|
||||||
|
this.equipmentTypes = await fetchJSON('/equipment-types/');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createEquipmentType() {
|
||||||
|
if (!this.newTypeName.trim()) {
|
||||||
|
this.status = 'Введите название типа';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.status = 'Добавление типа…';
|
||||||
|
await this.fetchAuth('/equipment-types/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: this.newTypeName }),
|
||||||
|
});
|
||||||
|
this.newTypeName = '';
|
||||||
|
await this.loadEquipmentTypes();
|
||||||
|
this.status = 'Тип добавлен';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось добавить тип';
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createEquipment() {
|
||||||
|
if (!this.newEquipment.nazvanie.trim()) {
|
||||||
|
this.status = 'Введите название оборудования';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.status = 'Добавление оборудования…';
|
||||||
|
const data = {
|
||||||
|
nazvanie: this.newEquipment.nazvanie,
|
||||||
|
invNumber: this.newEquipment.invNumber || null,
|
||||||
|
raspologenie: this.newEquipment.raspologenie || null,
|
||||||
|
kolichestvo: this.newEquipment.kolichestvo || null,
|
||||||
|
aud_id: this.newEquipment.aud_id || null,
|
||||||
|
type_id: this.newEquipment.type_id || null,
|
||||||
|
owner_id: this.newEquipment.owner_id || null,
|
||||||
|
};
|
||||||
|
await this.fetchAuth('/oboruds/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
this.newEquipment = { invNumber: null, nazvanie: '', raspologenie: '', kolichestvo: 1, aud_id: '', type_id: '', 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 = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Inspection methods
|
||||||
|
async showInspection() {
|
||||||
|
this.view = 'inspection';
|
||||||
|
this.status = '';
|
||||||
|
this.error = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
async startInspection() {
|
||||||
|
try {
|
||||||
|
this.status = 'Начало проверки…';
|
||||||
|
this.error = '';
|
||||||
|
const response = await this.fetchAuth('/inspections/sessions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
aud_id: this.inspectionAudId || null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeInspection = response;
|
||||||
|
await this.loadInspectionStats();
|
||||||
|
this.status = 'Проверка начата';
|
||||||
|
|
||||||
|
// Фокус на поле ввода
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.barcodeInput) {
|
||||||
|
this.$refs.barcodeInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось начать проверку: ' + e.message;
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkBarcode() {
|
||||||
|
if (!this.scannedBarcode.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.status = 'Проверка штрихкода…';
|
||||||
|
const response = await this.fetchAuth(`/inspections/sessions/${this.activeInspection.id}/check`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
inv_number: this.scannedBarcode
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastScanResult = response;
|
||||||
|
|
||||||
|
// Очистить поле и обновить данные
|
||||||
|
this.scannedBarcode = '';
|
||||||
|
await this.loadInspectionStats();
|
||||||
|
await this.loadInspectionDetails();
|
||||||
|
this.status = '';
|
||||||
|
|
||||||
|
// Автоматически скрыть уведомление через 3 секунды
|
||||||
|
setTimeout(() => {
|
||||||
|
this.lastScanResult = null;
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Вернуть фокус на поле ввода
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.barcodeInput) {
|
||||||
|
this.$refs.barcodeInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось проверить штрихкод: ' + e.message;
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadInspectionStats() {
|
||||||
|
try {
|
||||||
|
const response = await this.fetchAuth(`/inspections/sessions/${this.activeInspection.id}`);
|
||||||
|
this.inspectionStats = response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось загрузить статистику';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadInspectionDetails() {
|
||||||
|
try {
|
||||||
|
const response = await this.fetchAuth(`/inspections/sessions/${this.activeInspection.id}/records`);
|
||||||
|
this.checkedEquipment = response.records;
|
||||||
|
this.unknownBarcodes = response.unknown_barcodes;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось загрузить детали проверки';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshInspectionData() {
|
||||||
|
this.status = 'Обновление данных…';
|
||||||
|
await this.loadInspectionStats();
|
||||||
|
await this.loadInspectionDetails();
|
||||||
|
this.status = 'Данные обновлены';
|
||||||
|
},
|
||||||
|
|
||||||
|
async completeInspection() {
|
||||||
|
if (!confirm('Завершить проверку?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.status = 'Завершение проверки…';
|
||||||
|
await this.fetchAuth(`/inspections/sessions/${this.activeInspection.id}/complete`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeInspection = null;
|
||||||
|
this.inspectionStats = {
|
||||||
|
total_checked: 0,
|
||||||
|
total_expected: 0,
|
||||||
|
total_unknown: 0,
|
||||||
|
progress_percent: 0
|
||||||
|
};
|
||||||
|
this.checkedEquipment = [];
|
||||||
|
this.unknownBarcodes = [];
|
||||||
|
this.lastScanResult = null;
|
||||||
|
this.status = 'Проверка завершена';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось завершить проверку: ' + e.message;
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelInspection() {
|
||||||
|
if (confirm('Прервать проверку без сохранения?')) {
|
||||||
|
this.activeInspection = null;
|
||||||
|
this.inspectionStats = {
|
||||||
|
total_checked: 0,
|
||||||
|
total_expected: 0,
|
||||||
|
total_unknown: 0,
|
||||||
|
progress_percent: 0
|
||||||
|
};
|
||||||
|
this.checkedEquipment = [];
|
||||||
|
this.unknownBarcodes = [];
|
||||||
|
this.lastScanResult = null;
|
||||||
|
this.status = 'Проверка отменена';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadInspectionHistory() {
|
||||||
|
try {
|
||||||
|
this.status = 'Загрузка истории проверок…';
|
||||||
|
this.error = '';
|
||||||
|
this.inspectionHistory = await this.fetchAuth('/inspections/sessions');
|
||||||
|
this.status = `Загружено ${this.inspectionHistory.length} проверок`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось загрузить историю проверок: ' + e.message;
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async viewHistoryDetails(sessionId) {
|
||||||
|
try {
|
||||||
|
this.status = 'Загрузка деталей проверки…';
|
||||||
|
const [stats, details] = await Promise.all([
|
||||||
|
this.fetchAuth(`/inspections/sessions/${sessionId}`),
|
||||||
|
this.fetchAuth(`/inspections/sessions/${sessionId}/records`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Показать активную проверку с данными истории
|
||||||
|
this.activeInspection = stats.session;
|
||||||
|
this.inspectionStats = stats;
|
||||||
|
this.checkedEquipment = details.records;
|
||||||
|
this.unknownBarcodes = details.unknown_barcodes;
|
||||||
|
this.status = '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = 'Не удалось загрузить детали: ' + e.message;
|
||||||
|
this.status = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// read auth from localStorage
|
||||||
|
try {
|
||||||
|
this.token = localStorage.getItem('access_token') || '';
|
||||||
|
this.role = localStorage.getItem('role') || '';
|
||||||
|
} catch {}
|
||||||
|
this.loadAuditories();
|
||||||
|
this.loadOwners();
|
||||||
|
this.loadEquipmentTypes();
|
||||||
|
this.loadHomeUnassigned();
|
||||||
|
if (this.isAdmin) {
|
||||||
|
this.loadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#app');
|
||||||
7
frontend/bootstrap.bundle.min.js
vendored
Normal file
7
frontend/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
738
frontend/index.html
Normal file
738
frontend/index.html
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
<!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="/app/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="/app/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="no-print">
|
||||||
|
<h1>
|
||||||
|
<a href="/app/">АСУ Инвентаризация</a>
|
||||||
|
</h1>
|
||||||
|
</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="#" @click.prevent="showHome">Главная</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="showHome">Главная</a></li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Оборудование</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" @click.prevent="view='byAud'">По аудитории</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click.prevent="showAllEquipment">Всё оборудование</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click.prevent="showUnassigned">Не распределено</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" v-if="isAuth"><a class="nav-link" href="#" @click.prevent="showInspection">Проверка</a></li>
|
||||||
|
<li class="nav-item dropdown" v-if="isAdmin">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Просмотр</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" @click.prevent="view='users'">Пользователи</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click.prevent="view='audManage'">Аудитории</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="#" @click.prevent="showZametki">Заметки</a></li>
|
||||||
|
<li class="nav-item dropdown" v-if="canEdit">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Добавить</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" @click.prevent="view='addEquipment'">Оборудование</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click.prevent="view='addAuditory'">Аудиторию</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click.prevent="view='addType'">Тип оборудования</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click.prevent="view='addOwner'">Владельца</a></li>
|
||||||
|
<li v-if="isAdmin"><hr class="dropdown-divider"></li>
|
||||||
|
<li v-if="isAdmin"><a class="dropdown-item" href="#" @click.prevent="view='addUser'">Пользователя</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" v-if="isAuth"><a class="nav-link" href="/docs" target="_blank">API Docs</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-fluid px-4">
|
||||||
|
|
||||||
|
<!-- Главная страница -->
|
||||||
|
<div v-if="view==='home'">
|
||||||
|
<div class="row">
|
||||||
|
<div class="card col-md-6 col-10">
|
||||||
|
<div class="card-body">
|
||||||
|
<form @submit.prevent="doHomeSearch">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<input type="text" class="form-control w-auto" v-model="homeSearch" placeholder="инвентарный номер" />
|
||||||
|
<button class="btn btn-primary" type="submit">Найти</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="card col-md-10 col-12">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Нераспределённые</h3>
|
||||||
|
<div class="status" :class="{error: !!error}">{{ status }}</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table datatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Инв. номер</th>
|
||||||
|
<th scope="col">Название</th>
|
||||||
|
<th scope="col">Аудитория</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="it in homeUnassigned" :key="it.id">
|
||||||
|
<td>{{ it.invNumber ?? '' }}</td>
|
||||||
|
<td>{{ it.nazvanie ?? '' }}</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="canEdit">
|
||||||
|
<select class="form-select form-select-sm d-inline w-auto" v-model="it.selectedAudId">
|
||||||
|
<option value="">— выберите —</option>
|
||||||
|
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm btn-primary ms-2" @click="assignToAuditory(it)">Назначить</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>—</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="homeUnassigned.length === 0">
|
||||||
|
<td colspan="3" class="text-muted text-center">Нераспределённого оборудования нет</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3" v-if="homeSearchResults.length > 0">
|
||||||
|
<div class="card col-md-10 col-12">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Распределённые</h3>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table datatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Инв. номер</th>
|
||||||
|
<th scope="col">Название</th>
|
||||||
|
<th scope="col">Аудитория исходная</th>
|
||||||
|
<th scope="col" v-if="canEdit">Аудитория переноса</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="it in homeSearchResults" :key="it.id">
|
||||||
|
<td>{{ it.invNumber ?? '' }}</td>
|
||||||
|
<td>{{ it.nazvanie ?? '' }}</td>
|
||||||
|
<td>{{ getAuditoryName(it.aud_id) }}</td>
|
||||||
|
<td v-if="canEdit">
|
||||||
|
<select class="form-select form-select-sm d-inline w-auto" v-model="it.selectedAudId">
|
||||||
|
<option value="">— не менять —</option>
|
||||||
|
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm btn-outline-primary ms-2" :disabled="!it.selectedAudId" @click="assignToAuditory(it)">Перенести</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2" v-if="homeSearchDone && homeSearchResults.length === 0">
|
||||||
|
<div class="col-12 text-center text-muted">Ничего не найдено по запросу «{{ homeSearch }}»</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="view==='byAud'" class="row">
|
||||||
|
<div class="card col-12">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title no-print">Оборудование по аудитории</h3>
|
||||||
|
<h2 class="print-only print-title">{{ printTitle }}</h2>
|
||||||
|
<div class="mb-2 d-flex align-items-center justify-content-center gap-2 no-print">
|
||||||
|
<label for="aud-select" class="mb-0">Аудитория:</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>
|
||||||
|
<button class="btn btn-secondary" @click="printPage" :disabled="!oboruds.length">Печать</button>
|
||||||
|
</div>
|
||||||
|
<div class="status no-print" :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" class="no-print">Кол-во</th>
|
||||||
|
<th scope="col" class="no-print">Тип</th>
|
||||||
|
<th scope="col" class="no-print">Владелец</th>
|
||||||
|
<th scope="col" class="print-only">Проверено</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 class="no-print">{{ it.kolichestvo ?? '' }}</td>
|
||||||
|
<td class="no-print">{{ it.type?.name ?? '' }}</td>
|
||||||
|
<td class="no-print">
|
||||||
|
<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>
|
||||||
|
<td class="print-only"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="view==='allEquipment'" class="row">
|
||||||
|
<div class="card col-12">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title no-print">Всё оборудование (по инв. номеру)</h3>
|
||||||
|
<h2 class="print-only print-title">Всё оборудование</h2>
|
||||||
|
<div class="mb-2 no-print">
|
||||||
|
<button class="btn btn-secondary" @click="printAllEquipment" :disabled="!allOboruds.length">Печать</button>
|
||||||
|
</div>
|
||||||
|
<div class="status no-print" :class="{error: !!error}">{{ status }}</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table datatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="num-col">№</th>
|
||||||
|
<th scope="col" class="inv-col">Инв. номер</th>
|
||||||
|
<th scope="col">Название</th>
|
||||||
|
<th scope="col" class="aud-col">Аудитория</th>
|
||||||
|
<th scope="col">Расположение</th>
|
||||||
|
<th scope="col" class="no-print">Кол-во</th>
|
||||||
|
<th scope="col" class="no-print">Владелец</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(it, index) in allOboruds" :key="it.id">
|
||||||
|
<td class="num-col">{{ index + 1 }}</td>
|
||||||
|
<td class="inv-col">{{ it.invNumber ?? '' }}</td>
|
||||||
|
<td>{{ it.nazvanie ?? '' }}</td>
|
||||||
|
<td class="aud-col">{{ getAuditoryName(it.aud_id) }}</td>
|
||||||
|
<td class="rasp">{{ it.raspologenie ?? '' }}</td>
|
||||||
|
<td class="no-print">{{ it.kolichestvo ?? '' }}</td>
|
||||||
|
<td class="no-print">{{ it.owner?.name ?? '' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="view==='users'" class="row">
|
||||||
|
<div class="card col-12">
|
||||||
|
<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-12">
|
||||||
|
<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-12">
|
||||||
|
<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 v-if="view==='unassigned'" class="row">
|
||||||
|
<div class="card col-12">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Не распределено: {{ unassignedOboruds.length }} из {{ totalOboruds }}</h3>
|
||||||
|
<div class="status" :class="{error: !!error}">{{ status }}</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table datatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">№</th>
|
||||||
|
<th scope="col">Инв. номер</th>
|
||||||
|
<th scope="col">Название</th>
|
||||||
|
<th scope="col">Владелец</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(it, index) in unassignedOboruds" :key="it.id">
|
||||||
|
<td>{{ index + 1 }}</td>
|
||||||
|
<td class="inv">{{ it.invNumber ?? '' }}</td>
|
||||||
|
<td>{{ it.nazvanie ?? '' }}</td>
|
||||||
|
<td>{{ it.owner?.name ?? '' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="view==='zametki'" class="row">
|
||||||
|
<div class="card col-12">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Заметки</h3>
|
||||||
|
|
||||||
|
<div v-if="canEdit" class="mb-4">
|
||||||
|
<h5>Добавить заметку</h5>
|
||||||
|
<div class="row g-2 align-items-end">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<textarea class="form-control" v-model="newZametkaText" rows="3" placeholder="Текст заметки..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button class="btn btn-success" @click="createZametka">Добавить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status" :class="{error: !!error}">{{ status }}</div>
|
||||||
|
|
||||||
|
<h5>Активные заметки</h5>
|
||||||
|
<div v-if="zametki.length === 0" class="text-muted">Нет активных заметок</div>
|
||||||
|
<div v-for="z in zametki" :key="z.id" class="card mb-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text" style="white-space: pre-wrap;">{{ z.txtzam }}</p>
|
||||||
|
<small class="text-muted">{{ formatDate(z.created_date) }}</small>
|
||||||
|
<button v-if="canEdit" class="btn btn-sm btn-outline-success float-end" @click="resolveZametka(z.id)">Решено</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Добавить оборудование -->
|
||||||
|
<div v-if="view==='addEquipment'" class="row">
|
||||||
|
<div class="card col-12 col-md-8">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Добавить оборудование</h3>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Инв. номер</label>
|
||||||
|
<input type="number" class="form-control" v-model="newEquipment.invNumber" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">Название</label>
|
||||||
|
<input type="text" class="form-control" v-model="newEquipment.nazvanie" required />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Аудитория</label>
|
||||||
|
<select class="form-select" v-model="newEquipment.aud_id">
|
||||||
|
<option value="">— не выбрано —</option>
|
||||||
|
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Тип</label>
|
||||||
|
<select class="form-select" v-model="newEquipment.type_id">
|
||||||
|
<option value="">— не выбрано —</option>
|
||||||
|
<option v-for="t in equipmentTypes" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Владелец</label>
|
||||||
|
<select class="form-select" v-model="newEquipment.owner_id">
|
||||||
|
<option value="">— не выбрано —</option>
|
||||||
|
<option v-for="o in owners" :key="o.id" :value="o.id">{{ o.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">Расположение</label>
|
||||||
|
<input type="text" class="form-control" v-model="newEquipment.raspologenie" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Количество</label>
|
||||||
|
<input type="number" class="form-control" v-model="newEquipment.kolichestvo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status mt-3" :class="{error: !!error}">{{ status }}</div>
|
||||||
|
<button class="btn btn-success mt-3" @click="createEquipment">Добавить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Добавить аудиторию -->
|
||||||
|
<div v-if="view==='addAuditory'" class="row">
|
||||||
|
<div class="card col-12 col-md-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Добавить аудиторию</h3>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Название аудитории</label>
|
||||||
|
<input type="text" class="form-control" v-model="newAudName" placeholder="например, 519" />
|
||||||
|
</div>
|
||||||
|
<div class="status" :class="{error: !!error}">{{ status }}</div>
|
||||||
|
<button class="btn btn-success" @click="createAuditory">Добавить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Добавить тип оборудования -->
|
||||||
|
<div v-if="view==='addType'" class="row">
|
||||||
|
<div class="card col-12 col-md-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Добавить тип оборудования</h3>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Название типа</label>
|
||||||
|
<input type="text" class="form-control" v-model="newTypeName" placeholder="например, Компьютер" />
|
||||||
|
</div>
|
||||||
|
<div class="status" :class="{error: !!error}">{{ status }}</div>
|
||||||
|
<button class="btn btn-success" @click="createEquipmentType">Добавить</button>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Существующие типы</h5>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" v-for="t in equipmentTypes" :key="t.id">{{ t.name }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Добавить владельца -->
|
||||||
|
<div v-if="view==='addOwner'" class="row">
|
||||||
|
<div class="card col-12 col-md-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Добавить владельца</h3>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Имя владельца</label>
|
||||||
|
<input type="text" class="form-control" v-model="newOwnerName" placeholder="например, Иванов И.И." />
|
||||||
|
</div>
|
||||||
|
<div class="status" :class="{error: !!error}">{{ status }}</div>
|
||||||
|
<button class="btn btn-success" @click="createOwner">Добавить</button>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Существующие владельцы</h5>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" v-for="o in owners" :key="o.id">{{ o.name }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Добавить пользователя -->
|
||||||
|
<div v-if="view==='addUser'" class="row">
|
||||||
|
<div class="card col-12 col-md-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Добавить пользователя</h3>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Логин</label>
|
||||||
|
<input type="text" class="form-control" v-model="newAdminUsername" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Пароль</label>
|
||||||
|
<input type="password" class="form-control" v-model="newAdminPassword" />
|
||||||
|
</div>
|
||||||
|
<div class="status" :class="{error: !!error}">{{ status }}</div>
|
||||||
|
<button class="btn btn-success" @click="createAdmin">Создать администратора</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Проверка оборудования -->
|
||||||
|
<div v-if="view==='inspection'" class="row">
|
||||||
|
<div class="card col-12">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Проверка оборудования</h3>
|
||||||
|
|
||||||
|
<!-- Блок 1: Начать проверку (если нет активной сессии) -->
|
||||||
|
<div v-if="!activeInspection">
|
||||||
|
<h5>Начать новую проверку</h5>
|
||||||
|
<div class="row g-2 align-items-end mb-3">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label">Аудитория (необязательно)</label>
|
||||||
|
<select class="form-select" v-model="inspectionAudId">
|
||||||
|
<option value="">— Всё оборудование —</option>
|
||||||
|
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button class="btn btn-success" @click="startInspection">Начать проверку</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">История проверок</h5>
|
||||||
|
<button class="btn btn-secondary mb-3" @click="loadInspectionHistory">Загрузить историю</button>
|
||||||
|
|
||||||
|
<div v-if="inspectionHistory.length > 0" class="table-responsive">
|
||||||
|
<table class="table datatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Начата</th>
|
||||||
|
<th>Завершена</th>
|
||||||
|
<th>Аудитория</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="sess in inspectionHistory" :key="sess.id">
|
||||||
|
<td>{{ sess.id }}</td>
|
||||||
|
<td>{{ formatDate(sess.started_at) }}</td>
|
||||||
|
<td>{{ sess.completed_at ? formatDate(sess.completed_at) : '—' }}</td>
|
||||||
|
<td>{{ sess.aud_id ? getAuditoryName(sess.aud_id) : 'Всё' }}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-primary" @click="viewHistoryDetails(sess.id)">Детали</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Блок 2: Активная проверка -->
|
||||||
|
<div v-else>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>Активная проверка</strong><br>
|
||||||
|
<span v-if="activeInspection.aud_id">Аудитория: {{ getAuditoryName(activeInspection.aud_id) }}</span>
|
||||||
|
<span v-else>Всё оборудование</span><br>
|
||||||
|
Начата: {{ formatDate(activeInspection.started_at) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Статистика прогресса -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ inspectionStats.total_checked }}</h5>
|
||||||
|
<p class="card-text">Проверено</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ inspectionStats.total_expected }}</h5>
|
||||||
|
<p class="card-text">Всего</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ inspectionStats.total_unknown }}</h5>
|
||||||
|
<p class="card-text">Не найдено</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center bg-success text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ inspectionStats.progress_percent }}%</h5>
|
||||||
|
<p class="card-text">Прогресс</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Поле для сканирования -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Сканируйте штрихкод или введите инв. номер</label>
|
||||||
|
<input
|
||||||
|
ref="barcodeInput"
|
||||||
|
type="text"
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
v-model="scannedBarcode"
|
||||||
|
@keyup.enter="checkBarcode"
|
||||||
|
placeholder="Отсканируйте или введите номер..."
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Последний результат сканирования -->
|
||||||
|
<div v-if="lastScanResult" class="alert" :class="lastScanResult.status === 'found' ? 'alert-success' : 'alert-danger'">
|
||||||
|
{{ lastScanResult.message }}
|
||||||
|
<div v-if="lastScanResult.equipment">
|
||||||
|
<strong>{{ lastScanResult.equipment.nazvanie }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопки действий -->
|
||||||
|
<div class="mt-3 mb-4">
|
||||||
|
<button class="btn btn-primary me-2" @click="refreshInspectionData">Обновить данные</button>
|
||||||
|
<button class="btn btn-success me-2" @click="completeInspection">Завершить проверку</button>
|
||||||
|
<button class="btn btn-secondary" @click="cancelInspection">Отменить</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Таблица проверенного оборудования (в реальном времени) -->
|
||||||
|
<h5 class="mt-4">Проверенное оборудование</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table datatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Инв. номер</th>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Аудитория</th>
|
||||||
|
<th>Проверено</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="rec in checkedEquipment" :key="rec.id">
|
||||||
|
<td>{{ rec.oborud?.invNumber }}</td>
|
||||||
|
<td>{{ rec.oborud?.nazvanie }}</td>
|
||||||
|
<td>{{ getAuditoryName(rec.oborud?.aud_id) }}</td>
|
||||||
|
<td>{{ formatDate(rec.checked_at) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Неизвестные штрихкоды -->
|
||||||
|
<div v-if="unknownBarcodes.length > 0">
|
||||||
|
<h5 class="mt-4 text-danger">Неизвестные штрихкоды</h5>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" v-for="ub in unknownBarcodes" :key="ub.id">
|
||||||
|
{{ ub.barcode }} — {{ formatDate(ub.scanned_at) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/app/vue.global.prod.js"></script>
|
||||||
|
<script src="/app/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="/app/app.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
46
frontend/login.html
Normal file
46
frontend/login.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<!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="/app/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="/app/styles.css" />
|
||||||
|
<style>
|
||||||
|
body { display: flex; flex-direction: column; min-height: 100vh; }
|
||||||
|
main { flex: 1; display: flex; align-items: center; justify-content: center; width: 100%; }
|
||||||
|
.login-card { max-width: 420px; width: 100%; margin: 0; }
|
||||||
|
.muted { color: #6c757d; font-size: 0.9rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1><a href="/app/">АСУ Инвентаризация</a></h1>
|
||||||
|
<h2>Авторизация</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -9,40 +9,28 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
|
||||||
border: 1px;
|
border: 1px;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
background-color: #6A90B6;
|
background-color: #6A90B6;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #041322;
|
color: #041322;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
||||||
color: #041322;
|
color: #041322;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,28 +41,21 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|
||||||
/*width: 200px; */
|
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
border-color: #E07D54;
|
border-color: #E07D54;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.hidden-column {
|
.hidden-column {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@@ -94,18 +75,37 @@ table {
|
|||||||
width: 110px;
|
width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td.aud-col, th.aud-col {
|
||||||
|
width: 70px;
|
||||||
|
min-width: 70px;
|
||||||
|
max-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.num-col, th.num-col {
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
max-width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.inv-col, th.inv-col {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.inv {
|
.inv {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rasp {
|
.rasp {
|
||||||
width: 200px;
|
width: 250px;
|
||||||
|
max-width: 250px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
white-space: nowrap;
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
white-space: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#modal_matcenn {
|
#modal_matcenn {
|
||||||
|
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,15 +113,20 @@ table {
|
|||||||
background-color: whitesmoke;
|
background-color: whitesmoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.print-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.datatable th:nth-child(7) {
|
.datatable th:nth-child(7) {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
|
|
||||||
* {
|
* {
|
||||||
font-family: "Times New Roman", Times, serif;
|
font-family: "Times New Roman", Times, serif;
|
||||||
}
|
}
|
||||||
@@ -130,8 +135,6 @@ table {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -140,26 +143,65 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@page {
|
@page {
|
||||||
size: A4; /* или letter, legal, tabloid, etc. */
|
size: A4;
|
||||||
margin: 1cm; /* Устанавливаем поля */
|
margin: 10mm;
|
||||||
align-items: center;
|
}
|
||||||
|
|
||||||
|
@page :first {
|
||||||
|
margin-top: 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
title {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-print {
|
.no-print {
|
||||||
display: none;
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-only {
|
||||||
|
display: table-cell !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.print-only {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th, .table td {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 12pt;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.inv-col, th.inv-col {
|
||||||
|
width: 100px;
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.rs-table-bordered {
|
table.rs-table-bordered {
|
||||||
border: 1px solid #000000;
|
border: 1px solid #000000;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.rs-table-bordered > thead > tr > th {
|
table.rs-table-bordered > thead > tr > th {
|
||||||
@@ -172,8 +214,5 @@ table {
|
|||||||
border: 1px solid #000000;
|
border: 1px solid #000000;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
13
frontend/vue.global.prod.js
Normal file
13
frontend/vue.global.prod.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
Single-database configuration for Flask.
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# A generic, single database configuration.
|
|
||||||
|
|
||||||
[alembic]
|
|
||||||
# template used to generate migration files
|
|
||||||
# file_template = %%(rev)s_%%(slug)s
|
|
||||||
|
|
||||||
# set to 'true' to run the environment during
|
|
||||||
# the 'revision' command, regardless of autogenerate
|
|
||||||
# revision_environment = false
|
|
||||||
|
|
||||||
script_location = .
|
|
||||||
|
|
||||||
|
|
||||||
# Logging configuration
|
|
||||||
[loggers]
|
|
||||||
keys = root,sqlalchemy,alembic,flask_migrate
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys = console
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys = generic
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level = WARN
|
|
||||||
handlers = console
|
|
||||||
qualname =
|
|
||||||
|
|
||||||
[logger_sqlalchemy]
|
|
||||||
level = WARN
|
|
||||||
handlers =
|
|
||||||
qualname = sqlalchemy.engine
|
|
||||||
|
|
||||||
[logger_alembic]
|
|
||||||
level = INFO
|
|
||||||
handlers =
|
|
||||||
qualname = alembic
|
|
||||||
|
|
||||||
[logger_flask_migrate]
|
|
||||||
level = INFO
|
|
||||||
handlers =
|
|
||||||
qualname = flask_migrate
|
|
||||||
|
|
||||||
[handler_console]
|
|
||||||
class = StreamHandler
|
|
||||||
args = (sys.stderr,)
|
|
||||||
level = NOTSET
|
|
||||||
formatter = generic
|
|
||||||
|
|
||||||
[formatter_generic]
|
|
||||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
||||||
datefmt = %H:%M:%S
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import logging
|
|
||||||
from logging.config import fileConfig
|
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
from alembic import context
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
|
||||||
# access to the values within the .ini file in use.
|
|
||||||
config = context.config
|
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
|
||||||
# This line sets up loggers basically.
|
|
||||||
fileConfig(config.config_file_name)
|
|
||||||
logger = logging.getLogger('alembic.env')
|
|
||||||
|
|
||||||
|
|
||||||
def get_engine():
|
|
||||||
try:
|
|
||||||
# this works with Flask-SQLAlchemy<3 and Alchemical
|
|
||||||
return current_app.extensions['migrate'].db.get_engine()
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
# this works with Flask-SQLAlchemy>=3
|
|
||||||
return current_app.extensions['migrate'].db.engine
|
|
||||||
|
|
||||||
|
|
||||||
def get_engine_url():
|
|
||||||
try:
|
|
||||||
return get_engine().url.render_as_string(hide_password=False).replace(
|
|
||||||
'%', '%%')
|
|
||||||
except AttributeError:
|
|
||||||
return str(get_engine().url).replace('%', '%%')
|
|
||||||
|
|
||||||
|
|
||||||
# add your model's MetaData object here
|
|
||||||
# for 'autogenerate' support
|
|
||||||
# from myapp import mymodel
|
|
||||||
# target_metadata = mymodel.Base.metadata
|
|
||||||
config.set_main_option('sqlalchemy.url', get_engine_url())
|
|
||||||
target_db = current_app.extensions['migrate'].db
|
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
|
||||||
# can be acquired:
|
|
||||||
# my_important_option = config.get_main_option("my_important_option")
|
|
||||||
# ... etc.
|
|
||||||
|
|
||||||
|
|
||||||
def get_metadata():
|
|
||||||
if hasattr(target_db, 'metadatas'):
|
|
||||||
return target_db.metadatas[None]
|
|
||||||
return target_db.metadata
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline():
|
|
||||||
"""Run migrations in 'offline' mode.
|
|
||||||
|
|
||||||
This configures the context with just a URL
|
|
||||||
and not an Engine, though an Engine is acceptable
|
|
||||||
here as well. By skipping the Engine creation
|
|
||||||
we don't even need a DBAPI to be available.
|
|
||||||
|
|
||||||
Calls to context.execute() here emit the given string to the
|
|
||||||
script output.
|
|
||||||
|
|
||||||
"""
|
|
||||||
url = config.get_main_option("sqlalchemy.url")
|
|
||||||
context.configure(
|
|
||||||
url=url, target_metadata=get_metadata(), literal_binds=True
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online():
|
|
||||||
"""Run migrations in 'online' mode.
|
|
||||||
|
|
||||||
In this scenario we need to create an Engine
|
|
||||||
and associate a connection with the context.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# this callback is used to prevent an auto-migration from being generated
|
|
||||||
# when there are no changes to the schema
|
|
||||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
|
||||||
def process_revision_directives(context, revision, directives):
|
|
||||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
|
||||||
script = directives[0]
|
|
||||||
if script.upgrade_ops.is_empty():
|
|
||||||
directives[:] = []
|
|
||||||
logger.info('No changes in schema detected.')
|
|
||||||
|
|
||||||
conf_args = current_app.extensions['migrate'].configure_args
|
|
||||||
if conf_args.get("process_revision_directives") is None:
|
|
||||||
conf_args["process_revision_directives"] = process_revision_directives
|
|
||||||
|
|
||||||
connectable = get_engine()
|
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
|
||||||
context.configure(
|
|
||||||
connection=connection,
|
|
||||||
target_metadata=get_metadata(),
|
|
||||||
**conf_args
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
|
||||||
run_migrations_offline()
|
|
||||||
else:
|
|
||||||
run_migrations_online()
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""${message}
|
|
||||||
|
|
||||||
Revision ID: ${up_revision}
|
|
||||||
Revises: ${down_revision | comma,n}
|
|
||||||
Create Date: ${create_date}
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
${imports if imports else ""}
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = ${repr(up_revision)}
|
|
||||||
down_revision = ${repr(down_revision)}
|
|
||||||
branch_labels = ${repr(branch_labels)}
|
|
||||||
depends_on = ${repr(depends_on)}
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
${upgrades if upgrades else "pass"}
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
${downgrades if downgrades else "pass"}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""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 ###
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 256c3a3e91a2
|
|
||||||
Revises: 4f95d12a8352
|
|
||||||
Create Date: 2024-03-13 16:01:01.559719
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '256c3a3e91a2'
|
|
||||||
down_revision = '4f95d12a8352'
|
|
||||||
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.drop_column('somepl')
|
|
||||||
|
|
||||||
# ### 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('somepl', sa.VARCHAR(length=10), nullable=True))
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""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 ###
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 4f95d12a8352
|
|
||||||
Revises: 50f85881169e
|
|
||||||
Create Date: 2024-03-13 15:57:33.119683
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '4f95d12a8352'
|
|
||||||
down_revision = '50f85881169e'
|
|
||||||
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('somepl', sa.String(length=10), 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('somepl')
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 50f85881169e
|
|
||||||
Revises: b24baa0d98e6
|
|
||||||
Create Date: 2024-03-13 15:55:03.770084
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '50f85881169e'
|
|
||||||
down_revision = 'b24baa0d98e6'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
pass
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
"""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 ###
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 873defe09f22
|
|
||||||
Revises: ec6bbcd361bd
|
|
||||||
Create Date: 2024-03-13 22:34:36.718676
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '873defe09f22'
|
|
||||||
down_revision = 'ec6bbcd361bd'
|
|
||||||
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('raspologenie', sa.String(length=200), 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('raspologenie')
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 885bdd7b5161
|
|
||||||
Revises: be7c94c549e5
|
|
||||||
Create Date: 2024-04-02 23:03:59.401369
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '885bdd7b5161'
|
|
||||||
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('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 ###
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""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 ###
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 8e838956713f
|
|
||||||
Revises: 256c3a3e91a2
|
|
||||||
Create Date: 2024-03-20 19:03:51.112016
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '8e838956713f'
|
|
||||||
down_revision = '256c3a3e91a2'
|
|
||||||
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))
|
|
||||||
|
|
||||||
# ### 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('numberved')
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: b24baa0d98e6
|
|
||||||
Revises: 873defe09f22, b2a61aef79e9
|
|
||||||
Create Date: 2024-03-13 15:42:47.733687
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'b24baa0d98e6'
|
|
||||||
down_revision = ('873defe09f22', 'b2a61aef79e9')
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
pass
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: b2a61aef79e9
|
|
||||||
Revises: ec6bbcd361bd
|
|
||||||
Create Date: 2024-03-13 01:48:30.093937
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'b2a61aef79e9'
|
|
||||||
down_revision = 'ec6bbcd361bd'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
pass
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: be7c94c549e5
|
|
||||||
Revises: 8e838956713f
|
|
||||||
Create Date: 2024-04-01 15:09:52.082987
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'be7c94c549e5'
|
|
||||||
down_revision = '8e838956713f'
|
|
||||||
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('balancenumber', sa.String(length=30), nullable=True))
|
|
||||||
batch_op.drop_column('typeBalanse')
|
|
||||||
|
|
||||||
# ### 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('typeBalanse', sa.VARCHAR(length=30), nullable=True))
|
|
||||||
batch_op.drop_column('balancenumber')
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: ec6bbcd361bd
|
|
||||||
Revises:
|
|
||||||
Create Date: 2024-03-13 08:22:23.761783
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'ec6bbcd361bd'
|
|
||||||
down_revision = None
|
|
||||||
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.drop_column('duplicate')
|
|
||||||
|
|
||||||
with op.batch_alter_table('zametki', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('txtzam',
|
|
||||||
existing_type=sa.TEXT(length=10000),
|
|
||||||
type_=sa.String(length=10000),
|
|
||||||
existing_nullable=True)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('zametki', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('txtzam',
|
|
||||||
existing_type=sa.String(length=10000),
|
|
||||||
type_=sa.TEXT(length=10000),
|
|
||||||
existing_nullable=True)
|
|
||||||
|
|
||||||
with op.batch_alter_table('oboruds', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('duplicate', sa.BOOLEAN(), nullable=True))
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
76
models.py
76
models.py
@@ -1,76 +0,0 @@
|
|||||||
# backend/models.py
|
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
|
||||||
from sqlalchemy.orm import relationship, declarative_base
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
Base = declarative_base()
|
|
||||||
|
|
||||||
|
|
||||||
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 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'
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -22,3 +22,8 @@ Flask-Migrate==4.0.7
|
|||||||
Flask-SQLAlchemy==3.1.1
|
Flask-SQLAlchemy==3.1.1
|
||||||
waitress==3.0.2
|
waitress==3.0.2
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
python-jose==3.5.0
|
||||||
|
passlib==1.7.4
|
||||||
|
bcrypt==5.0.0
|
||||||
|
python-multipart==0.0.28
|
||||||
|
|||||||
6
serve.py
6
serve.py
@@ -1,6 +0,0 @@
|
|||||||
from waitress import serve
|
|
||||||
from app import app
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
serve(app, port='3800')
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,282 +0,0 @@
|
|||||||
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() {
|
|
||||||
|
|
||||||
clearTable();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
$('#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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
function getData(){
|
|
||||||
|
|
||||||
const audid = document.getElementById('auditory')
|
|
||||||
$.ajax({
|
|
||||||
|
|
||||||
url: "/getall",
|
|
||||||
type: "get",
|
|
||||||
contentType: 'application/json',
|
|
||||||
dataType: 'json',
|
|
||||||
|
|
||||||
success: function(response){
|
|
||||||
|
|
||||||
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 =""
|
|
||||||
|
|
||||||
|
|
||||||
data.forEach(element => {
|
|
||||||
tr += '<tr onclick="tableclick(this)">'
|
|
||||||
tr += '<td>' + element.num_ved + '</td>'
|
|
||||||
tr += '<td clas="inv">' + element.inv_number + '</td>'
|
|
||||||
tr += '<td>' + element.oboruds_id + '</td>'
|
|
||||||
tr += '<td class="no-print">' + element.auditory_name + '</td>'
|
|
||||||
tr += '<td class="rasp">' +element.raspolog + '</td>'
|
|
||||||
tr += '<td>' + '\n' + '</td>'
|
|
||||||
|
|
||||||
tr += '</tr>'
|
|
||||||
});
|
|
||||||
table.innerHTML += tr
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
$( document ).ready(function() {
|
|
||||||
|
|
||||||
|
|
||||||
$(".updatebtn").click(function(){
|
|
||||||
|
|
||||||
|
|
||||||
var audid = document.getElementById('auditory_dubl').value;
|
|
||||||
var invnum = document.getElementById('invnomer').textContent;
|
|
||||||
|
|
||||||
console.log('start perenos')
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: "/perenos",
|
|
||||||
type: 'get',
|
|
||||||
contentType: 'application/json',
|
|
||||||
dataType: 'json',
|
|
||||||
data: {'audid':audid,
|
|
||||||
'invnum':invnum
|
|
||||||
},
|
|
||||||
error: function(error){
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
invnomer.textContent=''
|
|
||||||
nazvanie.textContent=''
|
|
||||||
aud.textContent=''
|
|
||||||
auditory_dubl.selectedIndex = 0;
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
$("#printbutton").click(function(){
|
|
||||||
|
|
||||||
|
|
||||||
let aud = document.getElementById('auditory')
|
|
||||||
let audtext = aud.options[aud.selectedIndex].text;
|
|
||||||
const h2 = document.querySelector('h2');
|
|
||||||
h2.textContent = "Аудитория № " + audtext
|
|
||||||
|
|
||||||
document.getElementById("datatable").className="rs-table-bordered px-3"
|
|
||||||
let column = document.getElementById("proverka")
|
|
||||||
column.classList.remove("hidden-column")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
window.print()
|
|
||||||
document.getElementById("datatable").className="table"
|
|
||||||
h2.textContent='распределение мат. ценностей'
|
|
||||||
column.classList.add("hidden-column")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
$("#printallbutton").click(function(){
|
|
||||||
|
|
||||||
console.log("aaaaaaa")
|
|
||||||
|
|
||||||
})
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
|
|
||||||
function getData(){
|
|
||||||
|
|
||||||
const audid = document.getElementById('auditory')
|
|
||||||
$.ajax({
|
|
||||||
|
|
||||||
url: "/searchonaud",
|
|
||||||
type: "get",
|
|
||||||
contentType: 'application/json',
|
|
||||||
dataType: 'json',
|
|
||||||
data: {
|
|
||||||
auditory: audid.value
|
|
||||||
},
|
|
||||||
|
|
||||||
success: function(response){
|
|
||||||
|
|
||||||
var data = response;
|
|
||||||
const table = document.getElementById('datatable')
|
|
||||||
|
|
||||||
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>'
|
|
||||||
|
|
||||||
var tr =""
|
|
||||||
|
|
||||||
|
|
||||||
data.forEach(element => {
|
|
||||||
tr += '<tr onclick="tableclick(this)">'
|
|
||||||
tr += '<td>' + element.num_ved + '</td>'
|
|
||||||
tr += '<td clas="inv">' + element.inv_number + '</td>'
|
|
||||||
tr += '<td>' + element.oboruds_id + '</td>'
|
|
||||||
tr += '<td class="no-print">' + element.auditory_name + '</td>'
|
|
||||||
tr += '<td class="rasp">' +element.raspolog + '</td>'
|
|
||||||
tr += '<td>' + '\n' + '</td>'
|
|
||||||
|
|
||||||
tr += '</tr>'
|
|
||||||
});
|
|
||||||
table.innerHTML = headTable + tr
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#searchbutton").click(function(){
|
|
||||||
getData();
|
|
||||||
})
|
|
||||||
|
|
||||||
$('#modalsavetodb').click(function(){
|
|
||||||
|
|
||||||
let rasp = document.getElementById('modal_rapolog')
|
|
||||||
let vedomost = document.getElementById('modal_vednumber')
|
|
||||||
let invnom = document.getElementById('modal_invnom')
|
|
||||||
let matcen = document.getElementById('modal_matcenn')
|
|
||||||
|
|
||||||
let changeddata = new Array()
|
|
||||||
|
|
||||||
changeddata[0] = invnom.text
|
|
||||||
changeddata[1] = vedomost.value
|
|
||||||
changeddata[2] = rasp.value
|
|
||||||
|
|
||||||
let sendData = changeddata.join(',')
|
|
||||||
|
|
||||||
changeddata = []
|
|
||||||
$.ajax({
|
|
||||||
|
|
||||||
url: "/addraspved",
|
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json;charset=utf-8",
|
|
||||||
dataType: "json",
|
|
||||||
data: sendData,
|
|
||||||
|
|
||||||
success: function(){
|
|
||||||
/*
|
|
||||||
rasp='',
|
|
||||||
vedomost = '',
|
|
||||||
invnom = '',
|
|
||||||
matcen = '',
|
|
||||||
|
|
||||||
changeddata = []
|
|
||||||
*/
|
|
||||||
$('#getmodal').modal('hide').data( 'bs.modal', null );
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
function tableclick(tableRow){
|
|
||||||
|
|
||||||
let nomved = tableRow.childNodes[0].innerHTML;
|
|
||||||
let invnomer = tableRow.childNodes[1].innerHTML;
|
|
||||||
let nazvanie = tableRow.childNodes[2].innerHTML;
|
|
||||||
let raspolog = tableRow.childNodes[4].innerHTML;
|
|
||||||
|
|
||||||
$('#getmodal').modal('show')
|
|
||||||
|
|
||||||
let rasp = document.getElementById('modal_rapolog')
|
|
||||||
let vedomost = document.getElementById('modal_vednumber')
|
|
||||||
let invnom = document.getElementById('modal_invnom')
|
|
||||||
let matcen = document.getElementById('modal_matcenn')
|
|
||||||
|
|
||||||
invnom.innerText = invnomer
|
|
||||||
matcen.innerText = nazvanie.substring(0,15)
|
|
||||||
|
|
||||||
if (nomved.length >0){
|
|
||||||
|
|
||||||
vedomost.value = nomved
|
|
||||||
}
|
|
||||||
|
|
||||||
if(raspolog.length>0){
|
|
||||||
rasp.value = raspolog
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#mimodal").on('hidden.bs.modal', function () {
|
|
||||||
$(this).data('bs.modal', null);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
$('#modalclose').click(function(){
|
|
||||||
|
|
||||||
$('#getmodal').modal('hide').data( 'bs.modal', null );
|
|
||||||
|
|
||||||
} )
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
function tableclick(x){
|
|
||||||
// let data = document.getElementById(x.rowIndex)
|
|
||||||
|
|
||||||
let datas = x.innerText.split('\t')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
invnom.innerText=datas[1]+"\t"
|
|
||||||
matcen.innerText=datas[2]
|
|
||||||
|
|
||||||
|
|
||||||
if (datas[4].length>0){
|
|
||||||
rasp.value=datas[4];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (datas[0].length>0){
|
|
||||||
vedomost.value=datas[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#modalclose').click(function(){
|
|
||||||
|
|
||||||
$('#getmodal').modal('hide');
|
|
||||||
|
|
||||||
} )
|
|
||||||
|
|
||||||
$('#modalsavetodb').click(function(){
|
|
||||||
$.ajax({
|
|
||||||
|
|
||||||
url: "/addraspved",
|
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json;charset=utf-8",
|
|
||||||
dataType: "json",
|
|
||||||
data: {
|
|
||||||
rasp: rasp.value,
|
|
||||||
ved: vedomost.value,
|
|
||||||
inv: invnomer
|
|
||||||
},
|
|
||||||
success:function() {
|
|
||||||
rasp.value = '';
|
|
||||||
vedomost.value= '';
|
|
||||||
data=[];
|
|
||||||
rasp
|
|
||||||
$('#getmodal').modal('hide');
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
$( document ).ready(function() {
|
|
||||||
$(".reshbtn").click(function(){
|
|
||||||
var zmid = this.id
|
|
||||||
$.ajax({
|
|
||||||
url: "/zamrm",
|
|
||||||
type: 'get',
|
|
||||||
contentType: 'application/json',
|
|
||||||
dataType: 'json',
|
|
||||||
data: {'zmid':zmid},
|
|
||||||
})
|
|
||||||
var parent = document.getElementById('zambody')
|
|
||||||
var child = document.getElementById(zmid)
|
|
||||||
parent.removeChild(child)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
|
|
||||||
<div class="card col-md-10 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Добавить аудиторию</h5>
|
|
||||||
|
|
||||||
<form method='POST' action="/addaudtodb" class="card" >
|
|
||||||
<input type="text" class="form-control" name="auditory">
|
|
||||||
<button> Добавить</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{%endblock%}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static',filename='css/bootstrap.min.css')}}">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static',filename='css/index.css')}}">
|
|
||||||
|
|
||||||
<title class="no-print">Инвентаризация</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
{% include 'head.html' %}
|
|
||||||
|
|
||||||
<div class="row no-print">
|
|
||||||
{% include 'navbar.html' %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{% block content %} {% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% include 'js.html' %}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<header >
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a href="/">Инвентаризация кафедры <br>
|
|
||||||
"Автоматизированные системы управления"</br> </a>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<h2>
|
|
||||||
распределение мат. ценностей
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
|
|
||||||
</header>
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-6 col-10" >
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="POST" action="/">
|
|
||||||
<input type="text" name="srch" placeholder="инвентарный номер">
|
|
||||||
<button> Найти </button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-10 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title"> Нераспределённые </h3>
|
|
||||||
<form method="POST" action="/addoborudtodb">
|
|
||||||
<table class="table" name="table" col-md-10>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Инв. номер</th>
|
|
||||||
<th scope="col">Название</th>
|
|
||||||
<th scope="col">Аудитория</th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{% for item in results: %}
|
|
||||||
<tr>
|
|
||||||
<td> <input type="hidden" name="invnomer" value="{{ item[0] }}"> {{ item[0] }} </td>
|
|
||||||
<td> {{ item[1] }} </td>
|
|
||||||
|
|
||||||
<td> <select name="auditory" id="auditory">
|
|
||||||
|
|
||||||
{% for item in aud: %}
|
|
||||||
<option name="optauditory" value="{{item.id}}">{{ item.audnazvanie }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
<button> Обновить</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-10 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title"> Распределённые </h3>
|
|
||||||
<table class="table" col-md-10>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Инв. номер</th>
|
|
||||||
<th scope="col">Название</th>
|
|
||||||
<th scope="col">Аудитория исходная</th>
|
|
||||||
<th scope="col">Аудитория переноса</th>
|
|
||||||
</tr>
|
|
||||||
<td id="invnomer"> {{res1[0]}} </td>
|
|
||||||
<td id="nazvanie"> {{res1[1]}} </td>
|
|
||||||
<td id="aud"> {{res1[2]}} </td>
|
|
||||||
<td>
|
|
||||||
<select name="auditory" id="auditory_dubl">
|
|
||||||
{% for item in aud: %}
|
|
||||||
<option name="optauditory" value="{{item.id}}">{{ item.audnazvanie }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</table>
|
|
||||||
<button class="updatebtn" > Перенести </button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{%endblock%}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
<script src="{{url_for('static', filename='js/index.js') }}"></script>
|
|
||||||
<script src="{{url_for('static', filename='js/printall.js') }}"></script>
|
|
||||||
<script src="{{url_for('static', filename='js/zametki.js') }}"></script>
|
|
||||||
<script src="{{url_for('static', filename='js/searchonaud.js') }}"></script>
|
|
||||||
<script src="{{url_for('static', filename='js/print.js') }}"></script>
|
|
||||||
<script src="{{url_for('static', filename='js/modal.js') }}"></script>
|
|
||||||
<script src="{{url_for('static', filename='js/allmatc.js') }}"></script>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<div class="row no-print">
|
|
||||||
<nav class="no-print navbar navbar-expand-lg navbar-light">
|
|
||||||
<div class="no-print container-fluid">
|
|
||||||
<button class="no-print navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
|
||||||
aria-controls="no-print navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="no-printnavbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="no-print ollapse navbar-collapse" id="navbarSupportedContent">
|
|
||||||
<ul class="no-print navbar-nav me-auto mb-2 mb-lg-0">
|
|
||||||
<li class="nav-item no-print">
|
|
||||||
<a class="nav-link no-print" aria-current="page" href="{{ url_for('index') }}">Главная</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item no-print ">
|
|
||||||
<a class="nav-link no-print " aria-current="page" href="{{ url_for('alloborud') }}">Вся таблица</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item no-print">
|
|
||||||
<a class="nav-link no-print" href="{{url_for('searchonaud')}}">Поаудиторно</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item no-print">
|
|
||||||
<a class="nav-link no-print" aria-current="page" href="{{url_for('addAud')}}">Добавить аудиторию</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item no-print">
|
|
||||||
<a class="nav-link no-print" aria-current="page" href="{{url_for('vneaud')}}">Не распределено</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item no-print">
|
|
||||||
<a class="nav-link no-print" aria-current="page" href="{{url_for('zametki')}}">Заметки</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-10 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="POST" action="/search">
|
|
||||||
<input type="text" name="srch" placeholder="инвентарный номер">
|
|
||||||
<button> Найти </button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-10 col-10">
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title"> Распределённые </h3>
|
|
||||||
<form method="POST" action="/updateduplicate">
|
|
||||||
<table class="table" col-md-10>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Инв. номер</th>
|
|
||||||
<th scope="col">Название</th>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
<td> {{res1[0]}} </td>
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{%endblock%}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static',filename='css/bootstrap.min.css')}}">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static',filename='css/index.css')}}">
|
|
||||||
<title>Инвентаризация</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
Инвентаризация каф АСУ
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<h2>
|
|
||||||
Аудитория
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-10 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title"> Поаудиторно </h3>
|
|
||||||
<form method="POST" action="/addoborudtodb">
|
|
||||||
<table class="table" id="datatable" col-md-10>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Инв. номер</th>
|
|
||||||
<th scope="col">Название</th>
|
|
||||||
<th scope="col">Аудитория</th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{% for item in res: %}
|
|
||||||
<tr>
|
|
||||||
<td> <input type="hidden" name="invnomer" value="{{ item[0] }}"> {{ item[0] }} </td>
|
|
||||||
<td> {{ item[1] }} </td>
|
|
||||||
<td>
|
|
||||||
{{item[2]}}
|
|
||||||
</td>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
{% 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">
|
|
||||||
|
|
||||||
<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-primary" id="modalsavetodb">Сохранить изменения</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row no-print">
|
|
||||||
<div class="card col-md-10 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
|
|
||||||
|
|
||||||
<select name="auditory" id="auditory">
|
|
||||||
|
|
||||||
{% for item in aud: %}
|
|
||||||
<option name="optauditory" value="{{ item.id }}">{{ item.audnazvanie }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button id="searchbutton"> Найти</button>
|
|
||||||
<button id="printbutton"> Печать</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-10 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title no-print"> Поаудиторно </h3>
|
|
||||||
|
|
||||||
<table class="table " id="datatable">
|
|
||||||
|
|
||||||
<th>Номер в Инв. вед</th>
|
|
||||||
<th>Инв. номер</th>
|
|
||||||
<th>Название</th>
|
|
||||||
<th class="no-print">Аудитория</th>
|
|
||||||
<th>Расположение</th>
|
|
||||||
|
|
||||||
|
|
||||||
{% for item in res %}
|
|
||||||
|
|
||||||
<td><input type="hidden" name="invnomer" value="{{ item[0] }}"> {{ item[0] }} </td>
|
|
||||||
<td> {{ item[1] }} </td>
|
|
||||||
<td class="no-print"> {{ item[2] }} </td>
|
|
||||||
<td id="proverka"> Проверено</td>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-10 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
<form>
|
|
||||||
<h3 class="card-title"> Не распределено {{ kolvo }} штук из {{ all_kol }} </h3>
|
|
||||||
<table class="table" col-md-10>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Инв. номер</th>
|
|
||||||
<th scope="col">Название</th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
|
|
||||||
{% for item in res: %}
|
|
||||||
<tr>
|
|
||||||
<td> <input type="hidden" name="invnomer" value="{{ item[0] }}"> {{ item[0] }} </td>
|
|
||||||
<td> {{ item[1] }} </td>
|
|
||||||
<td> {{ item[2] }} </td>
|
|
||||||
<td> {{ item[3] }} </td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-8 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title"> Добавление заметки </h3>
|
|
||||||
<form method="POST" action="/zametki">
|
|
||||||
<textarea id="textzam" name="textzam" class="col-6"></textarea>
|
|
||||||
<div class="row">
|
|
||||||
<button> Добавить </button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="zambody" class="container col-12">
|
|
||||||
|
|
||||||
{% for item in zam: %}
|
|
||||||
<div class="row" id="{{ item.id }}">
|
|
||||||
<div class="card col-md-6 col-10">
|
|
||||||
<div class="card-body">
|
|
||||||
{{ item.txtzam }}
|
|
||||||
</div>
|
|
||||||
{{ item.created_date }}
|
|
||||||
<div class="row">
|
|
||||||
<button id="{{ item.id }}" class="reshbtn col-4"> Решено </button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{%endblock%}
|
|
||||||
Reference in New Issue
Block a user