Compare commits
67 Commits
c2f3f10c9b
...
feature/fr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31f1fcecc4 | ||
|
|
67e7ab6f94 | ||
|
|
913e23b966 | ||
|
|
7219fbdada | ||
|
|
92fc020d94 | ||
|
|
dca8a0dbcf | ||
|
|
61d12f4972 | ||
|
|
2ae18dea27 | ||
|
|
1950bd4d45 | ||
|
|
9491909f24 | ||
|
|
54534ee490 | ||
|
|
bef4af4644 | ||
|
|
35bd29c223 | ||
|
|
e2ff0f9a05 | ||
|
|
e428e7f762 | ||
|
|
3f91dc91ec | ||
|
|
779c256e7b | ||
|
|
d686b26465 | ||
|
|
08e979ecb2 | ||
|
|
72f1d53051 | ||
|
|
b1e0693131 | ||
|
|
86713fc75f | ||
|
|
c24a1fa8c6 | ||
|
|
f108e013c2 | ||
| 1c33775f92 | |||
| 43ab114e1a | |||
| 946ad5c31f | |||
|
|
7b956d89bf | ||
| 0891abc0e1 | |||
| 08393f6685 | |||
| 9cde4e2c7d | |||
|
|
0a60a16344 | ||
|
|
496ef3fa9d | ||
|
|
3494e5d17c | ||
|
|
29b0070260 | ||
|
|
2e9cc918d5 | ||
|
|
d943894ae8 | ||
|
|
1c901ddb00 | ||
|
|
f9a188c927 | ||
|
|
1fce4b41c0 | ||
|
|
7ccbba06c1 | ||
|
|
63115f0328 | ||
| cb862c63a6 | |||
| 927d8d75a9 | |||
| 509a3ee913 | |||
| 86f0f9d977 | |||
| 0c4bf4b9fd | |||
| d71945ecb2 | |||
| 3cd5fe63b5 | |||
| 0c6001b297 | |||
| 682b6c4ffd | |||
| 9c859d4660 | |||
|
|
f6b720e4eb | ||
|
|
2fda39a449 | ||
| 291e5d0e25 | |||
| 45d8dfd870 | |||
|
|
4c6166c907 | ||
|
|
2c51bdc695 | ||
| 87c4ebe33e | |||
|
|
bf93bf0fdb | ||
|
|
027b1dc855 | ||
| 4a47746e9d | |||
| 12a63278f7 | |||
|
|
965695a693 | ||
|
|
b47e9391ba | ||
|
|
5259dc5292 | ||
|
|
a6eea28071 |
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:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
venv/
|
||||||
|
backend/venv/
|
||||||
|
**/__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.db
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
data/
|
||||||
|
*.xls
|
||||||
|
*.xlsx
|
||||||
25
.gitignore
vendored
25
.gitignore
vendored
@@ -1,16 +1,21 @@
|
|||||||
*.csv
|
|
||||||
.vscode
|
.vscode
|
||||||
instance
|
.idea
|
||||||
venv/
|
venv/
|
||||||
|
instance/
|
||||||
|
|
||||||
__pychache__/
|
# Ignore Python bytecode caches everywhere
|
||||||
123
|
__pycache__/
|
||||||
|
**/__pycache__/
|
||||||
|
|
||||||
zabalans.csv
|
|
||||||
project.db
|
|
||||||
|
|
||||||
|
# Migrations and DB files
|
||||||
|
migrations/
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Data and temp files
|
||||||
|
*.csv
|
||||||
c*.txt
|
c*.txt
|
||||||
__pycache__/app.cpython-312.pyc
|
|
||||||
__pycache__/models.cpython-312.pyc
|
# Legacy specific ignores (if present)
|
||||||
|
backend/venv
|
||||||
|
backend/__pycache__
|
||||||
|
backend/routeres/__pycache__
|
||||||
|
|||||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
COPY requirements.txt /app_src/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r /app_src/requirements.txt
|
||||||
|
|
||||||
|
COPY backend/ /app_src/backend/
|
||||||
|
COPY frontend/ /app_src/frontend/
|
||||||
|
COPY docker-entrypoint.sh /app_src/docker-entrypoint.sh
|
||||||
|
RUN chmod +x /app_src/docker-entrypoint.sh
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app_src/docker-entrypoint.sh"]
|
||||||
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`.
|
||||||
BIN
VedomostMOL.xls
Normal file
BIN
VedomostMOL.xls
Normal file
Binary file not shown.
BIN
VedomostMOL.xlsx
Normal file
BIN
VedomostMOL.xlsx
Normal file
Binary file not shown.
274
app.py
274
app.py
@@ -1,274 +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("/", 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():
|
|
||||||
oboruds = Oboruds.query.all()
|
|
||||||
oboruds_json = [{
|
|
||||||
'id': oborud.id,
|
|
||||||
'invNumber': oborud.invNumber,
|
|
||||||
'nazvanie': oborud.nazvanie,
|
|
||||||
'raspologenie': oborud.raspologenie,
|
|
||||||
'numberved': oborud.numberved,
|
|
||||||
'balancenumber': oborud.balancenumber,
|
|
||||||
'kolichestvo': oborud.kolichestvo,
|
|
||||||
'balancenumber': oborud.balancenumber,
|
|
||||||
'aud_id': oborud.aud_id
|
|
||||||
} for oborud in oboruds]
|
|
||||||
|
|
||||||
|
|
||||||
return jsonify(oboruds_json)
|
|
||||||
|
|
||||||
@app.route('/updateduplicate', methods=['GET', 'POST'])
|
|
||||||
def updateduplicate():
|
|
||||||
if request.method == 'POST':
|
|
||||||
aud = request.form.get('auditory_dubl')
|
|
||||||
|
|
||||||
|
|
||||||
@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, dt.typeBalanse])
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
print(query_string)
|
|
||||||
un_query_string = unquote(unquote(query_string)).split(',')
|
|
||||||
|
|
||||||
print(un_query_string)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ob = db.session.query(Oboruds).filter_by(invNumber=un_query_string[0]).first()
|
|
||||||
ob.raspologenie=un_query_string[2]
|
|
||||||
ob.numberved = un_query_string[1]
|
|
||||||
db.session.commit()
|
|
||||||
db.session.close()
|
|
||||||
|
|
||||||
return jsonify({'success': True}, 200, {'ContentType': 'application/json'})
|
|
||||||
|
|
||||||
|
|
||||||
# ==================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
def ranomraspr():
|
|
||||||
with app.app_context():
|
|
||||||
while len(db.session.query(Oboruds).filter(Oboruds.aud_id == None).all()) > 0:
|
|
||||||
audid = random.choice(db.session.query(Auditory).all())
|
|
||||||
oborud = random.choice(db.session.query(Oboruds).filter(Oboruds.aud_id == 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()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
#ranomraspr()
|
|
||||||
#createdb()
|
|
||||||
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:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
2
backend/__init__.py
Normal file
2
backend/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Backend package initializer."""
|
||||||
|
|
||||||
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()
|
||||||
26
backend/create_new_db.py
Normal file
26
backend/create_new_db.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from backend.models import Base, User
|
||||||
|
from backend.database import engine
|
||||||
|
from backend.security import get_password_hash
|
||||||
|
|
||||||
|
|
||||||
|
def recreate_db_with_admin():
|
||||||
|
# Drop and recreate all tables
|
||||||
|
Base.metadata.drop_all(bind=engine)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
# Seed default admin user (username: admin, password: admin)
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
db = Session()
|
||||||
|
try:
|
||||||
|
if not db.query(User).filter(User.username == 'admin').first():
|
||||||
|
db.add(User(username='admin', password_hash=get_password_hash('admin'), role='admin'))
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
recreate_db_with_admin()
|
||||||
|
|
||||||
18
backend/database.py
Normal file
18
backend/database.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# backend/database.py
|
||||||
|
import os
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db")
|
||||||
|
|
||||||
|
connect_args = {"check_same_thread": False} if SQLALCHEMY_DATABASE_URL.startswith("sqlite") else {}
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args=connect_args)
|
||||||
|
SessionLocal = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
27
backend/init_db.py
Normal file
27
backend/init_db.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from backend.models import Base, User
|
||||||
|
from backend.database import engine
|
||||||
|
from backend.security import get_password_hash
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
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()
|
||||||
|
print("Created default admin user (login: admin / password: admin)")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_db()
|
||||||
57
backend/main.py
Normal file
57
backend/main.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# backend/main.py
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
from starlette.staticfiles import StaticFiles
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from backend.routers.equipment_types import equipment_types
|
||||||
|
from backend.routers.auditories import auditories
|
||||||
|
from backend.routers.oboruds import oboruds
|
||||||
|
from backend.routers.components import components
|
||||||
|
from backend.routers.rashodniki import consumables
|
||||||
|
from backend.routers.zametki import zametki
|
||||||
|
from backend.routers.auth import auth
|
||||||
|
from backend.routers.owners import owners
|
||||||
|
from backend.routers.inspections import inspections
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Для фронтенда Vue.js
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # заменить на ['http://localhost:5173'] для безопасности
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/ping")
|
||||||
|
def ping():
|
||||||
|
return {"message": "pong"}
|
||||||
|
|
||||||
|
|
||||||
|
# Serve frontend
|
||||||
|
app.mount("/app", StaticFiles(directory="frontend", html=True), name="frontend")
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root():
|
||||||
|
return RedirectResponse(url="/app/")
|
||||||
|
|
||||||
|
@app.get("/login")
|
||||||
|
def login_page():
|
||||||
|
return RedirectResponse(url="/app/login.html")
|
||||||
|
|
||||||
|
|
||||||
|
# Подключение роутов
|
||||||
|
app.include_router(equipment_types)
|
||||||
|
app.include_router(auditories)
|
||||||
|
app.include_router(oboruds)
|
||||||
|
app.include_router(components)
|
||||||
|
app.include_router(consumables)
|
||||||
|
app.include_router(zametki)
|
||||||
|
app.include_router(auth)
|
||||||
|
app.include_router(owners)
|
||||||
|
app.include_router(inspections)
|
||||||
89
backend/migrate_data.py
Normal file
89
backend/migrate_data.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import sys
|
||||||
|
from sqlalchemy import create_engine, text, inspect
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from backend.database import SessionLocal as NewSession, engine as new_engine
|
||||||
|
from backend import models
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
OLD_DB_URL = "sqlite:///./instance/project.-10-11-25.db"
|
||||||
|
old_engine = create_engine(OLD_DB_URL, connect_args={"check_same_thread": False})
|
||||||
|
OldSession = sessionmaker(bind=old_engine)
|
||||||
|
old_db = OldSession()
|
||||||
|
|
||||||
|
new_db = NewSession()
|
||||||
|
|
||||||
|
def log(msg: str):
|
||||||
|
print(f"[INFO] {msg}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_schema():
|
||||||
|
"""Ensure new DB has required tables/columns (owners table and oboruds.owner_id)."""
|
||||||
|
# Create any missing tables defined in models (e.g., owners)
|
||||||
|
models.Base.metadata.create_all(bind=new_engine)
|
||||||
|
|
||||||
|
# If oboruds.owner_id is missing (older DB), add the column (SQLite allows simple ALTER)
|
||||||
|
try:
|
||||||
|
inspector = inspect(new_engine)
|
||||||
|
cols = [c["name"] for c in inspector.get_columns("oboruds")]
|
||||||
|
if "owner_id" not in cols:
|
||||||
|
with new_engine.begin() as conn:
|
||||||
|
conn.execute(text("ALTER TABLE oboruds ADD COLUMN owner_id INTEGER"))
|
||||||
|
log("Добавлен столбец oboruds.owner_id")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Предупреждение: проверка/добавление owner_id не выполнена: {e}")
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
ensure_schema()
|
||||||
|
log("Запуск переноса данных из old_app.db → app.db")
|
||||||
|
|
||||||
|
# Тип оборудования по умолчанию
|
||||||
|
log("Добавление типа оборудования по умолчанию: 'Неизвестно'")
|
||||||
|
default_type = models.EquipmentType(name="Неизвестно")
|
||||||
|
new_db.add(default_type)
|
||||||
|
new_db.commit()
|
||||||
|
|
||||||
|
# Перенос аудиторий
|
||||||
|
log("Перенос аудиторий...")
|
||||||
|
auditory_map = {}
|
||||||
|
aud_rows = old_db.execute(text("SELECT id, audnazvanie FROM auditory")).fetchall()
|
||||||
|
for aud in aud_rows:
|
||||||
|
new_aud = models.Auditory(audnazvanie=aud.audnazvanie)
|
||||||
|
new_db.add(new_aud)
|
||||||
|
new_db.flush()
|
||||||
|
auditory_map[aud.id] = new_aud.id
|
||||||
|
log(f" → перенесено: {len(aud_rows)}")
|
||||||
|
|
||||||
|
# Перенос оборудования
|
||||||
|
log("Перенос оборудования...")
|
||||||
|
ob_rows = old_db.execute(text("SELECT * FROM oboruds")).fetchall()
|
||||||
|
for ob in ob_rows:
|
||||||
|
new_ob = models.Oboruds(
|
||||||
|
invNumber=ob.invNumber,
|
||||||
|
nazvanie=ob.nazvanie,
|
||||||
|
raspologenie=ob.raspologenie,
|
||||||
|
numberppasu=ob.numberppasu,
|
||||||
|
kolichestvo=ob.kolichestvo,
|
||||||
|
aud_id=auditory_map.get(ob.aud_id),
|
||||||
|
type_id=default_type.id
|
||||||
|
)
|
||||||
|
new_db.add(new_ob)
|
||||||
|
log(f" → перенесено: {len(ob_rows)}")
|
||||||
|
|
||||||
|
# Перенос заметок
|
||||||
|
log("Перенос заметок...")
|
||||||
|
z_rows = old_db.execute(text("SELECT * FROM zametki")).fetchall()
|
||||||
|
for z in z_rows:
|
||||||
|
new_z = models.Zametki(
|
||||||
|
txtzam=z.txtzam,
|
||||||
|
created_date=z.created_date,
|
||||||
|
rmdt=z.rmdt
|
||||||
|
)
|
||||||
|
new_db.add(new_z)
|
||||||
|
log(f" → перенесено: {len(z_rows)}")
|
||||||
|
|
||||||
|
new_db.commit()
|
||||||
|
log("✅ Перенос завершён успешно.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate()
|
||||||
144
backend/models.py
Normal file
144
backend/models.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# backend/models.py
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, UniqueConstraint
|
||||||
|
from sqlalchemy.orm import relationship, declarative_base
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
username = Column(String(150), unique=True, nullable=False)
|
||||||
|
password_hash = Column(String(255), nullable=False)
|
||||||
|
role = Column(String(50), nullable=False, default='viewer') # 'admin' | 'editor' | 'viewer'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Auditory(Base):
|
||||||
|
__tablename__ = 'auditories'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
audnazvanie = Column(String)
|
||||||
|
|
||||||
|
oboruds = relationship("Oboruds", back_populates="auditory")
|
||||||
|
|
||||||
|
|
||||||
|
class EquipmentType(Base):
|
||||||
|
__tablename__ = 'equipment_types'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String, unique=True, nullable=False)
|
||||||
|
|
||||||
|
oboruds = relationship("Oboruds", back_populates="type")
|
||||||
|
|
||||||
|
class Owner(Base):
|
||||||
|
__tablename__ = 'owners'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String, unique=True, nullable=False)
|
||||||
|
|
||||||
|
oboruds = relationship("Oboruds", back_populates="owner")
|
||||||
|
|
||||||
|
|
||||||
|
class Oboruds(Base):
|
||||||
|
__tablename__ = 'oboruds'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
invNumber = Column(Integer)
|
||||||
|
nazvanie = Column(String(500))
|
||||||
|
raspologenie = Column(String(200))
|
||||||
|
numberppasu = Column(String(100))
|
||||||
|
kolichestvo = Column(Integer)
|
||||||
|
|
||||||
|
aud_id = Column(Integer, ForeignKey("auditories.id"))
|
||||||
|
auditory = relationship("Auditory", back_populates="oboruds")
|
||||||
|
|
||||||
|
type_id = Column(Integer, ForeignKey("equipment_types.id"))
|
||||||
|
type = relationship("EquipmentType", back_populates="oboruds")
|
||||||
|
|
||||||
|
owner_id = Column(Integer, ForeignKey("owners.id"))
|
||||||
|
owner = relationship("Owner", back_populates="oboruds")
|
||||||
|
|
||||||
|
components = relationship("Component", back_populates="oborud")
|
||||||
|
consumables = relationship("Consumable", back_populates="oborud")
|
||||||
|
|
||||||
|
|
||||||
|
class Component(Base):
|
||||||
|
__tablename__ = 'components'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
description = Column(String)
|
||||||
|
|
||||||
|
oborud_id = Column(Integer, ForeignKey("oboruds.id"))
|
||||||
|
oborud = relationship("Oboruds", back_populates="components")
|
||||||
|
|
||||||
|
|
||||||
|
class Consumable(Base):
|
||||||
|
__tablename__ = 'consumables'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
description = Column(String)
|
||||||
|
|
||||||
|
oborud_id = Column(Integer, ForeignKey("oboruds.id"))
|
||||||
|
oborud = relationship("Oboruds", back_populates="consumables")
|
||||||
|
|
||||||
|
|
||||||
|
class Zametki(Base):
|
||||||
|
__tablename__ = 'zametki'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
txtzam = Column(String(10000))
|
||||||
|
created_date = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
|
rmdt = Column(DateTime)
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
BIN
backend/requirements.txt
Normal file
BIN
backend/requirements.txt
Normal file
Binary file not shown.
2
backend/routers/__init__.py
Normal file
2
backend/routers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Routers package initializer."""
|
||||||
|
|
||||||
19
backend/routers/auditories.py
Normal file
19
backend/routers/auditories.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
|
auditories = APIRouter(prefix="/auditories", tags=["auditories"])
|
||||||
|
|
||||||
|
@auditories.post("/", response_model=schemas.AuditoryRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
|
def create_auditory(item: schemas.AuditoryCreate, db: Session = Depends(database.get_db)):
|
||||||
|
obj = models.Auditory(**item.dict())
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@auditories.get("/", response_model=list[schemas.AuditoryRead])
|
||||||
|
def list_auditories(db: Session = Depends(database.get_db)):
|
||||||
|
return db.query(models.Auditory).all()
|
||||||
|
|
||||||
63
backend/routers/auth.py
Normal file
63
backend/routers/auth.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from .. import schemas, database
|
||||||
|
from ..security import authenticate_user, create_access_token, get_password_hash, require_roles
|
||||||
|
from ..models import User
|
||||||
|
|
||||||
|
|
||||||
|
auth = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
|
|
||||||
|
@auth.post("/token", response_model=schemas.Token)
|
||||||
|
def login_for_access_token(
|
||||||
|
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
):
|
||||||
|
user = authenticate_user(db, form_data.username, form_data.password)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
|
||||||
|
access_token_expires = timedelta(minutes=60)
|
||||||
|
access_token = create_access_token(data={"sub": user.username, "role": user.role}, expires_delta=access_token_expires)
|
||||||
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
|
||||||
|
|
||||||
|
@auth.post("/users", response_model=schemas.UserRead, dependencies=[Depends(require_roles(["admin"]))])
|
||||||
|
def create_user(item: schemas.UserCreate, db: Session = Depends(database.get_db)):
|
||||||
|
if db.query(User).filter(User.username == item.username).first():
|
||||||
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
|
obj = User(username=item.username, password_hash=get_password_hash(item.password), role=item.role)
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@auth.post("/users/admin", response_model=schemas.UserRead, dependencies=[Depends(require_roles(["admin"]))])
|
||||||
|
def create_admin_user(item: schemas.UserCreate, db: Session = Depends(database.get_db)):
|
||||||
|
if db.query(User).filter(User.username == item.username).first():
|
||||||
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
|
obj = User(username=item.username, password_hash=get_password_hash(item.password), role="admin")
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@auth.get("/users", response_model=list[schemas.UserRead], dependencies=[Depends(require_roles(["admin"]))])
|
||||||
|
def list_users(db: Session = Depends(database.get_db)):
|
||||||
|
return db.query(User).all()
|
||||||
|
|
||||||
|
|
||||||
|
@auth.patch("/users/{user_id}/role", response_model=schemas.UserRead, dependencies=[Depends(require_roles(["admin"]))])
|
||||||
|
def update_user_role(user_id: int, payload: schemas.UserRoleUpdate, db: Session = Depends(database.get_db)):
|
||||||
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
user.role = payload.role
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
return user
|
||||||
19
backend/routers/components.py
Normal file
19
backend/routers/components.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
|
|
||||||
|
components = APIRouter(prefix="/components", tags=["components"])
|
||||||
|
|
||||||
|
@components.post("/", response_model=schemas.ComponentRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
|
def create_component(item: schemas.ComponentCreate, db: Session = Depends(database.get_db)):
|
||||||
|
obj = models.Component(**item.dict())
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@components.get("/", response_model=list[schemas.ComponentRead])
|
||||||
|
def list_components(db: Session = Depends(database.get_db)):
|
||||||
|
return db.query(models.Component).all()
|
||||||
18
backend/routers/equipment_types.py
Normal file
18
backend/routers/equipment_types.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
|
equipment_types = APIRouter(prefix="/equipment-types", tags=["equipment_types"])
|
||||||
|
|
||||||
|
@equipment_types.post("/", response_model=schemas.EquipmentTypeRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
|
def create_equipment_type(item: schemas.EquipmentTypeCreate, db: Session = Depends(database.get_db)):
|
||||||
|
obj = models.EquipmentType(**item.dict())
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@equipment_types.get("/", response_model=list[schemas.EquipmentTypeRead])
|
||||||
|
def list_equipment_types(db: Session = Depends(database.get_db)):
|
||||||
|
return db.query(models.EquipmentType).all()
|
||||||
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]
|
||||||
|
)
|
||||||
54
backend/routers/oboruds.py
Normal file
54
backend/routers/oboruds.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
|
oboruds = APIRouter(prefix="/oboruds", tags=["oboruds"])
|
||||||
|
|
||||||
|
@oboruds.post("/", response_model=schemas.OborudRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
|
def create_oborud(item: schemas.OborudCreate, db: Session = Depends(database.get_db)):
|
||||||
|
obj = models.Oboruds(**item.dict())
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@oboruds.get("/", response_model=list[schemas.OborudRead])
|
||||||
|
def list_oboruds(aud_id: Optional[int] = None, sort_by_inv: bool = False, unassigned: bool = False, inv_number: Optional[int] = None, db: Session = Depends(database.get_db)):
|
||||||
|
query = db.query(models.Oboruds)
|
||||||
|
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)
|
||||||
|
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()
|
||||||
|
|
||||||
|
@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)
|
||||||
|
def get_oborud(oborud_id: int, db: Session = Depends(database.get_db)):
|
||||||
|
obj = db.query(models.Oboruds).filter(models.Oboruds.id == oborud_id).first()
|
||||||
|
if not obj:
|
||||||
|
raise HTTPException(status_code=404, detail="Oborud not found")
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@oboruds.patch("/{oborud_id}", response_model=schemas.OborudRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
|
def update_oborud(oborud_id: int, payload: schemas.OborudUpdate, db: Session = Depends(database.get_db)):
|
||||||
|
obj = db.query(models.Oboruds).filter(models.Oboruds.id == oborud_id).first()
|
||||||
|
if not obj:
|
||||||
|
raise HTTPException(status_code=404, detail="Oborud not found")
|
||||||
|
data = payload.dict(exclude_unset=True)
|
||||||
|
for k, v in data.items():
|
||||||
|
setattr(obj, k, v)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
22
backend/routers/owners.py
Normal file
22
backend/routers/owners.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
|
|
||||||
|
owners = APIRouter(prefix="/owners", tags=["owners"])
|
||||||
|
|
||||||
|
|
||||||
|
@owners.post("/", response_model=schemas.OwnerRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
|
def create_owner(item: schemas.OwnerCreate, db: Session = Depends(database.get_db)):
|
||||||
|
obj = models.Owner(**item.dict())
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@owners.get("/", response_model=list[schemas.OwnerRead])
|
||||||
|
def list_owners(db: Session = Depends(database.get_db)):
|
||||||
|
return db.query(models.Owner).all()
|
||||||
|
|
||||||
18
backend/routers/rashodniki.py
Normal file
18
backend/routers/rashodniki.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
|
consumables = APIRouter(prefix="/consumables", tags=["consumables"])
|
||||||
|
|
||||||
|
@consumables.post("/", response_model=schemas.ConsumableRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
|
def create_consumable(item: schemas.ConsumableCreate, db: Session = Depends(database.get_db)):
|
||||||
|
obj = models.Consumable(**item.dict())
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@consumables.get("/", response_model=list[schemas.ConsumableRead])
|
||||||
|
def list_consumables(db: Session = Depends(database.get_db)):
|
||||||
|
return db.query(models.Consumable).all()
|
||||||
32
backend/routers/zametki.py
Normal file
32
backend/routers/zametki.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from .. import models, schemas, database
|
||||||
|
from ..security import require_roles
|
||||||
|
|
||||||
|
zametki = APIRouter(prefix="/zametki", tags=["zametki"])
|
||||||
|
|
||||||
|
@zametki.post("/", response_model=schemas.ZametkaRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
|
||||||
|
def create_zametka(item: schemas.ZametkaCreate, db: Session = Depends(database.get_db)):
|
||||||
|
obj = models.Zametki(txtzam=item.txtzam, created_date=datetime.now(timezone.utc))
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@zametki.get("/", response_model=list[schemas.ZametkaRead])
|
||||||
|
def list_zametki(active_only: bool = True, db: Session = Depends(database.get_db)):
|
||||||
|
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
|
||||||
217
backend/schemas.py
Normal file
217
backend/schemas.py
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# backend/schemas.py
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List, Literal
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# === Equipment Type ===
|
||||||
|
class EquipmentTypeBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class EquipmentTypeCreate(EquipmentTypeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EquipmentTypeRead(EquipmentTypeBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# === Component ===
|
||||||
|
class ComponentBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
class ComponentCreate(ComponentBase):
|
||||||
|
oborud_id: int
|
||||||
|
|
||||||
|
class ComponentRead(ComponentBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# === Consumable ===
|
||||||
|
class ConsumableBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
class ConsumableCreate(ConsumableBase):
|
||||||
|
oborud_id: int
|
||||||
|
|
||||||
|
class ConsumableRead(ConsumableBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# === Owner ===
|
||||||
|
class OwnerBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class OwnerCreate(OwnerBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class OwnerRead(OwnerBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# === Oborud ===
|
||||||
|
class OborudBase(BaseModel):
|
||||||
|
invNumber: Optional[int] = None
|
||||||
|
nazvanie: str
|
||||||
|
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 OborudCreate(OborudBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class OborudUpdate(BaseModel):
|
||||||
|
invNumber: Optional[int] = None
|
||||||
|
nazvanie: Optional[str] = None
|
||||||
|
raspologenie: Optional[str] = None
|
||||||
|
numberppasu: Optional[str] = None
|
||||||
|
kolichestvo: Optional[int] = None
|
||||||
|
aud_id: Optional[int] = None
|
||||||
|
type_id: Optional[int] = None
|
||||||
|
owner_id: Optional[int] = None
|
||||||
|
|
||||||
|
class OborudRead(OborudBase):
|
||||||
|
id: int
|
||||||
|
type: Optional[EquipmentTypeRead] = None
|
||||||
|
owner: Optional[OwnerRead] = None
|
||||||
|
components: List[ComponentRead] = []
|
||||||
|
consumables: List[ConsumableRead] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# === Auditory ===
|
||||||
|
class AuditoryBase(BaseModel):
|
||||||
|
audnazvanie: str
|
||||||
|
|
||||||
|
class AuditoryCreate(AuditoryBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AuditoryRead(AuditoryBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# === Zametka ===
|
||||||
|
class ZametkaBase(BaseModel):
|
||||||
|
txtzam: str
|
||||||
|
rmdt: Optional[datetime] = None
|
||||||
|
|
||||||
|
class ZametkaCreate(ZametkaBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ZametkaRead(ZametkaBase):
|
||||||
|
id: int
|
||||||
|
created_date: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
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
|
||||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./appdata:/app
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=sqlite:////app/data/app.db
|
||||||
|
restart: unless-stopped
|
||||||
11
docker-entrypoint.sh
Normal file
11
docker-entrypoint.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Sync app code from image on every start (keeps code fresh after rebuilds)
|
||||||
|
rm -rf /app/backend /app/frontend
|
||||||
|
cp -r /app_src/backend /app_src/frontend /app/
|
||||||
|
|
||||||
|
mkdir -p /app/data
|
||||||
|
python -m backend.init_db
|
||||||
|
|
||||||
|
exec uvicorn backend.main:app --host 0.0.0.0 --port 8000
|
||||||
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
250
frontend/styles.css
Normal file
250
frontend/styles.css
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #E2F3FD;
|
||||||
|
min-width: 580px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
background-color: #6A90B6;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #041322;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #041322;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #E07D54;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 5px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #E07D54;
|
||||||
|
border-color: #E07D54;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover, .btn-primary:focus, .btn-primary:active {
|
||||||
|
background-color: #c86a3d;
|
||||||
|
border-color: #c86a3d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary {
|
||||||
|
color: #E07D54;
|
||||||
|
border-color: #E07D54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary:hover, .btn-outline-primary:focus, .btn-outline-primary:active {
|
||||||
|
background-color: #E07D54;
|
||||||
|
border-color: #E07D54;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
border-color: #E07D54;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-column {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
word-break: break-all;
|
||||||
|
border-collapse: separate !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 10rem;
|
||||||
|
word-break: break-all;
|
||||||
|
border-collapse: separate !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aud {
|
||||||
|
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 {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rasp {
|
||||||
|
width: 250px;
|
||||||
|
max-width: 250px;
|
||||||
|
word-break: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
white-space: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal_matcenn {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datatable {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datatable th:nth-child(7) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
* {
|
||||||
|
font-family: "Times New Roman", Times, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page :first {
|
||||||
|
margin-top: 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-only {
|
||||||
|
display: table-cell !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.print-only {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
border: 1px solid #E07D54;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th, .table td {
|
||||||
|
border: 1px solid #E07D54;
|
||||||
|
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 {
|
||||||
|
border: 1px solid #E07D54;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.rs-table-bordered > thead > tr > th {
|
||||||
|
border: 1px solid #E07D54;
|
||||||
|
padding: 2px;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.rs-table-bordered > tbody > tr > td {
|
||||||
|
border: 1px solid #E07D54;
|
||||||
|
padding: 10px;
|
||||||
|
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.
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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: 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,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,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,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 ###
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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,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 ###
|
|
||||||
35
models.py
35
models.py
@@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
db = SQLAlchemy()
|
|
||||||
|
|
||||||
|
|
||||||
class Auditory(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
||||||
audnazvanie=db.Column(db.String)
|
|
||||||
oboruds = db.relationship('Oboruds')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Oboruds(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
||||||
invNumber=db.Column(db.Integer)
|
|
||||||
nazvanie=db.Column(db.String(500))
|
|
||||||
balancenumber = db.Column(db.String(30))
|
|
||||||
raspologenie = db.Column(db.String(200))
|
|
||||||
numberved = db.Column(db.String(100))
|
|
||||||
kolichestvo = db.Column(db.Integer)
|
|
||||||
aud_id = db.Column(db.Integer, db.ForeignKey(Auditory.id))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Zametki(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
||||||
txtzam=db.Column(db.String(10000))
|
|
||||||
created_date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
|
||||||
rmdt = db.Column(db.DateTime)
|
|
||||||
|
|
||||||
|
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,167 +0,0 @@
|
|||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #E2F3FD;
|
|
||||||
min-width: 580px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row{
|
|
||||||
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
|
|
||||||
border: 1px;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
background-color: #6A90B6;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
|
|
||||||
text-decoration: none;
|
|
||||||
color: #041322;
|
|
||||||
}
|
|
||||||
|
|
||||||
a{
|
|
||||||
|
|
||||||
color: #041322;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: #E07D54;
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
|
|
||||||
width: 200px;
|
|
||||||
margin: 10px;
|
|
||||||
border-radius: 15px;
|
|
||||||
border-color: #E07D54;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.hidden-column{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav{
|
|
||||||
|
|
||||||
width:100%;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.table{
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aud{
|
|
||||||
width: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv{
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rasp{
|
|
||||||
max-width: 200px;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
#modal_matcenn{
|
|
||||||
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datatable{
|
|
||||||
background-color: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
|
|
||||||
*{
|
|
||||||
font-family: "Times New Roman", Times, serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@page {
|
|
||||||
size: A4; /* или letter, legal, tabloid, etc. */
|
|
||||||
margin: 1cm; /* Устанавливаем поля */
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
border: none;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
}
|
|
||||||
.no-print {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.rs-table-bordered{
|
|
||||||
border:1px solid #000000;
|
|
||||||
margin-top:20px;
|
|
||||||
font-size: 14pt;
|
|
||||||
|
|
||||||
}
|
|
||||||
table.rs-table-bordered > thead > tr > th{
|
|
||||||
border:1px solid #000000;
|
|
||||||
padding: 2px;
|
|
||||||
font-size: 14pt;
|
|
||||||
}
|
|
||||||
table.rs-table-bordered > tbody > tr > td{
|
|
||||||
border:1px solid #000000;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 14pt;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
function getAllData(){
|
|
||||||
$.ajax({
|
|
||||||
|
|
||||||
url: "/getall",
|
|
||||||
type: "get",
|
|
||||||
contentType: 'application/json',
|
|
||||||
dataType: 'json',
|
|
||||||
|
|
||||||
success: function(response){
|
|
||||||
console.log(response)
|
|
||||||
let data = response;
|
|
||||||
$('#datatable tbody').empty();
|
|
||||||
$.each(response, function(index, item) {
|
|
||||||
$('#datatable tbody').append(
|
|
||||||
'<tr>' +
|
|
||||||
'<td>' + item.invNumber + '</td>' +
|
|
||||||
'<td>' + item.nazvanie + '</td>' +
|
|
||||||
'<td>' + item.raspologenie + '</td>' +
|
|
||||||
'<td>' + item.numberved + '</td>' +
|
|
||||||
'<td>' + item.buhnumberpp + '</td>' +
|
|
||||||
'<td>' + item.kolichestvo + '</td>' +
|
|
||||||
'<td>' + item.balancenumber + '</td>' +
|
|
||||||
'</tr>'
|
|
||||||
);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function(){
|
|
||||||
|
|
||||||
getAllData()
|
|
||||||
})
|
|
||||||
@@ -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')
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#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,35 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="row col-12">
|
|
||||||
<div class=" card col-11">
|
|
||||||
<h3 class=" no-print"> Все мат. ценности </h3>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="row col-12">
|
|
||||||
<button class="button" id="printallbutton"> Печать </button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="card col-md-11" >
|
|
||||||
<table id="datable" class="datable table pagebreak" >
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">№ п/п</th>
|
|
||||||
<th scope="col">№ п/п вед</th>
|
|
||||||
<th scope="col">Инв. номер</th>
|
|
||||||
<th scope="col">Название</th>
|
|
||||||
<th scope="col">Кол-во</th>
|
|
||||||
<th scope="col">Счёт</th>
|
|
||||||
<th scope="col">Аудитория</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% 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,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,74 +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>
|
|
||||||
<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 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" col-md-10>
|
|
||||||
|
|
||||||
<td >Номер в Инв. вед</td>
|
|
||||||
<td >Инв. номер</td>
|
|
||||||
<td >Название</td>
|
|
||||||
<td class="no-print">Аудитория</td>
|
|
||||||
<td >Расположение</td>
|
|
||||||
|
|
||||||
|
|
||||||
{% 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