diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..fe81f05 --- /dev/null +++ b/.claude/settings.local.json @@ -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:*)" + ] + } +} diff --git a/INSPECTION_GUIDE.md b/INSPECTION_GUIDE.md new file mode 100644 index 0000000..95896b6 --- /dev/null +++ b/INSPECTION_GUIDE.md @@ -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`. diff --git a/addoborudtodb b/addoborudtodb deleted file mode 100644 index e69de29..0000000 diff --git a/app.py b/app.py deleted file mode 100644 index 17633fb..0000000 --- a/app.py +++ /dev/null @@ -1,344 +0,0 @@ -# -*- coding: utf-8 -*- - -from flask import Flask, render_template, redirect, url_for, request, jsonify -from models import db, Oboruds, Auditory, Zametki -from flask_migrate import Migrate -from datetime import * -import csv -import random -from urllib.parse import unquote - -app = Flask(__name__) - -app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///project.db" -app.jinja_env.auto_reload = True -app.config['TEMPLATES_AUTO_RELOAD'] = True - -app.secret_key = '6523e58bc0eec42c31b9635d5e0dfc23b6d119b73e633bf3a5284c79bb4a1ede' - -db.init_app(app) - -migrate = Migrate(app, db) - - - -@app.route("/login", methods=['GET', 'POST']) -def login(): - return render_template('login.html') - - -@app.route("/", methods=['GET', 'POST']) -def index(): - results = [] - results1 = [] - all_aud = db.session.query(Auditory).all() - auds = [] - for item in all_aud: - auds.append(item.audnazvanie) - - if request.method == 'POST': - p = request.form.get('srch').strip() - all_aud = db.session.query(Auditory).all() - s = db.session.query(Oboruds).filter( - Oboruds.invNumber.contains(p)).first() - if s: - for item in all_aud: - auds.append(item.audnazvanie) - if s.aud_id is None: - results.append([s.invNumber, s.nazvanie]) - else: - results1.append(s.invNumber) - results1.append(s.nazvanie) - aud = db.session.get(Auditory, s.aud_id) - results1.append(aud.audnazvanie) - return render_template('index.html', aud=all_aud, results=results, res1=results1) - return render_template('index.html', aud=all_aud, results=results, res1=results1) - - -@app.route("/perenos", methods=['GET', 'POST']) -def perenos(): - audid = request.args.get('audid') - invnomer = request.args.get('invnum') - ob = db.session.query(Oboruds).filter_by(invNumber=invnomer).first() - ob.aud_id = audid - db.session.commit() - return jsonify({'success': True}, 200, {'ContentType': 'application/json'}) - - -@app.route("/addaud", methods=['GET', 'POST']) -def addAud(): - return render_template('addaud.html', methods=['GET', 'POST']) - - -@app.route('/searchonaud', methods=['GET', 'POST']) -def searchonaud(): - all_aud = db.session.query(Auditory).all() - res = [] - if request.method == 'GET' and request.args.get('auditory'): - audid = request.args.get('auditory') - q = db.session.query(Auditory, Oboruds).filter( - Auditory.id == Oboruds.aud_id - ).filter(Auditory.id == audid - ).order_by(Oboruds.invNumber).all() - results = [] - for auditory, oboruds in q: - results.append({ - 'num_ved': oboruds.numberved, - 'auditory_id': auditory.id, - 'auditory_name': auditory.audnazvanie, - 'inv_number': oboruds.invNumber, - 'oboruds_id': oboruds.nazvanie, - 'raspolog': oboruds.raspologenie, - }) - return jsonify(results) - - else: - return render_template('searchonaud.html', aud=all_aud, res=res) - - -@app.route("/addaudtodb", methods=['GET', 'POST']) -def addaudtodb(): - if request.method == 'POST': - aud = request.form.get('auditory') - db.session.add(Auditory(audnazvanie=aud)) - db.session.commit() - return redirect(url_for('addAud')) - - -@app.route("/addoborudtodb", methods=['GET', 'POST']) -def addoborudtodb(): - res = [] - if request.method == 'POST': - audid = request.form.get('auditory') - invnomer = request.form.get('invnomer') - ob = db.session.query(Oboruds).filter_by(invNumber=invnomer).first() - ob.aud_id = audid - db.session.commit() - return redirect(url_for('index')) - - -@app.route('/all') -def alloborud(): - return render_template('all.html') - - -@app.route('/getall') -def getall(): - oborud = db.session.query(Oboruds).order_by(Oboruds.invNumber).all() - - results = [] - for oboruds in oborud: - if oboruds.aud_id is None: - results.append({ - 'invNumber': oboruds.invNumber, - 'nazvanie': oboruds.nazvanie, - 'raspologenie': oboruds.raspologenie, - 'balancenumber': oboruds.balancenumber, - 'kolichestvo': oboruds.kolichestvo, - 'numberppasu': oboruds.numberppasu, - 'numberved': oboruds.numberved, - 'aud': ""}) - else: - aud = db.session.query(Auditory).filter_by(id=oboruds.aud_id).first() - results.append({ - 'invNumber': oboruds.invNumber, - 'nazvanie': oboruds.nazvanie, - 'raspologenie': oboruds.raspologenie, - 'balancenumber': oboruds.balancenumber, - 'kolichestvo': oboruds.kolichestvo, - 'numberppasu': oboruds.numberppasu, - 'numberved': oboruds.numberved, - 'aud': aud.audnazvanie}) - - return jsonify(results) - - -@app.route('/vneaud', methods=['GET', 'POST']) -def vneaud(): - res = [] - data = db.session.query(Oboruds).filter(Oboruds.aud_id == None).all() - - ak = db.session.query(Oboruds).all() - - for dt in data: - res.append([dt.invNumber, dt.nazvanie]) - - return render_template('vneaud.html', res=res, kolvo=len(data), all_kol=len(ak)) - - -@app.route('/zametki', methods=['GET', 'POST']) -def zametki(): - zam = db.session.query(Zametki).filter(Zametki.rmdt == None).all() - if request.method == 'POST': - textzam = request.form.get('textzam') - timeadd = datetime.now(timezone.utc) - if len(textzam) > 0: - db.session.add(Zametki(txtzam=textzam, created_date=timeadd)) - db.session.commit() - return redirect(url_for('zametki')) - - return render_template('zametki.html', zam=zam) - - -@app.route('/zamrm', methods=['GET', 'POST']) -def js2(): - zmid = request.args.get('zmid') - ob = db.session.query(Zametki).filter_by(id=zmid).first() - ob.rmdt = datetime.now(timezone.utc) - db.session.commit() - return jsonify({'success': True}), 200, {'ContentType': 'application/json'} - - -@app.route('/zamsearch', methods=['GET', 'POST']) -def zamsearch(): - p = request.form.get('srch') - - searchedZam = db.session.query(Zametki).filter( - Zametki.txtzam.contains(p)).ll() - zam = [] - for item in searchedZam: - zam.append([item.txtzam, item.created_date]) - return render_template('zametki.html', zam=zam) - - -@app.route('/addraspved', methods=['GET', 'POST']) -def addraspved(): - if request.method == 'POST': - query_string = request.data.decode() - - un_query_string = unquote(unquote(query_string)).split(',') - - ob = db.session.query(Oboruds).filter_by(invNumber=un_query_string[0]).first() - - ob.numberved = un_query_string[1] - ob.kolichestvo = un_query_string[2] - ob.balancenumber = un_query_string[3] - ob.raspologenie = un_query_string[4] - - db.session.commit() - db.session.close() - - return jsonify({'success': True}, 200, {'ContentType': 'application/json'}) - - -@app.route('/addoborudasu', methods=['GET', 'POST']) -def addoborud(): - if request.method == 'POST': - query_string = request.data.decode() - - un_query_string = unquote(unquote(query_string)).split(',') - db.session.add( - Oboruds(invNumber=un_query_string[0], - nazvanie=un_query_string[5], - raspologenie=un_query_string[4], - numberved=un_query_string[1], - kolichestvo=un_query_string[2], - balancenumber=un_query_string[3] - ) - ) - - db.session.commit() - - return jsonify({'success': True}, 200, {'ContentType': 'application/json'}) - - -# ================================================================================== - - -def ranomraspr(): - with app.app_context(): - while len(db.session.query(Oboruds).filter(Oboruds.aud_id is None).all()) > 0: - audid = random.choice(db.session.query(Auditory).all()) - oborud = random.choice(db.session.query(Oboruds).filter(Oboruds.aud_id is None).all()) - oborud.aud_id = audid.id - db.session.commit() - - -def createdb(): - with app.app_context(): - db.create_all() - auds = ['519', '521', '521a', '522', '523', - '601л', - '602л', - '603л', - '604л', - '605л', - '606л', - '607л', - '608л', - '609л', - '610л', - '611л', - '612л', - '613л', - '616л', - '617л', - '618л', - '619л', - '620л', - '621л', - '622л', - '626л', - '627л', - '703л', - '710л', - '713л'] - for aud in auds: - db.session.add(Auditory(audnazvanie=aud)) - - db.session.commit() - - with open('inventMavrin.csv', encoding='cp1251') as csv_file: - csv_reader = csv.reader(csv_file, delimiter=';') - for row in csv_reader: - db.session.add( - Oboruds(invNumber=row[0], nazvanie=row[1], typeBalanse='баланс')) - - db.session.commit() - - -def write2excell(): - wb = xlrd.open_workbook("VedIsh.xls") - sheet = wb.sheet_by_index(0) - - newFile = copy(wb) - newSheet = newFile.get_sheet(0) - invNomerColum = 6 - - column_index = 1 - for row_idx in range(sheet.nrows): - cell_value = sheet.cell(row_idx, column_index) - - if cell_value: - - tmp_inv_number = str(cell_value).split(':')[1] - - try: - a = tmp_inv_number[1:-1] - - inv_number = int(tmp_inv_number[1:-1]) - - with app.app_context(): - auditory_obj = db.session.query(Auditory).join(Oboruds, Oboruds.aud_id == Auditory.id).filter( - Oboruds.invNumber == inv_number).first() - - print(auditory_obj.audnazvanie) - - # cur.execute("SELECT aud.audnazvanie FROM oboruds AS ob JOIN auditory AS aud ON ob.aud_id = aud.id WHERE ob.invNumber = ?", (inv_number,)) - - - except: - pass - - """ - else: - #newSheet.write(row_idx, invNomerColum, "Нет инв номера") - pass - newFile.save("Ved31.xls") - """ - - -if __name__ == '__main__': - #write2excell() - - app.run(debug=True, host='0.0.0.0', port='3800') diff --git a/backend/.claude/settings.local.json b/backend/.claude/settings.local.json new file mode 100644 index 0000000..0461afa --- /dev/null +++ b/backend/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(.venvScriptsactivate)", + "Bash(python:*)", + "Bash(venv/Scripts/python.exe:*)", + "Bash(timeout 5 tail:*)", + "Bash(curl:*)" + ] + } +} diff --git a/backend/main.py b/backend/main.py index 8a432d8..c9d94e5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,6 +12,7 @@ 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 @@ -32,8 +33,7 @@ def ping(): return {"message": "pong"} -# Serve static assets and frontend -app.mount("/static", StaticFiles(directory="static"), name="static") +# Serve frontend app.mount("/app", StaticFiles(directory="frontend", html=True), name="frontend") @app.get("/") @@ -46,11 +46,12 @@ def login_page(): # Подключение роутов -app.include_router(equipment_types) -app.include_router(auditories) -app.include_router(oboruds) +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) diff --git a/backend/models.py b/backend/models.py index a20c754..d2fdafd 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,6 +1,6 @@ # backend/models.py -from sqlalchemy import Column, Integer, String, ForeignKey, DateTime +from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, UniqueConstraint from sqlalchemy.orm import relationship, declarative_base import datetime @@ -98,3 +98,47 @@ class Zametki(Base): 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") diff --git a/backend/schemas.py b/backend/schemas.py index 51ecce1..6450bd8 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -16,7 +16,7 @@ class EquipmentTypeRead(EquipmentTypeBase): id: int class Config: - orm_mode = True + from_attributes = True # === Component === @@ -31,7 +31,7 @@ class ComponentRead(ComponentBase): id: int class Config: - orm_mode = True + from_attributes = True # === Consumable === @@ -46,7 +46,7 @@ class ConsumableRead(ConsumableBase): id: int class Config: - orm_mode = True + from_attributes = True # === Owner === @@ -60,7 +60,7 @@ class OwnerRead(OwnerBase): id: int class Config: - orm_mode = True + from_attributes = True # === Oborud === @@ -95,7 +95,7 @@ class OborudRead(OborudBase): consumables: List[ConsumableRead] = [] class Config: - orm_mode = True + from_attributes = True # === Auditory === @@ -109,7 +109,7 @@ class AuditoryRead(AuditoryBase): id: int class Config: - orm_mode = True + from_attributes = True # === Zametka === @@ -125,7 +125,7 @@ class ZametkaRead(ZametkaBase): created_date: Optional[datetime] = None class Config: - orm_mode = True + from_attributes = True # === Auth/User === @@ -151,8 +151,67 @@ class UserRead(UserBase): id: int class Config: - orm_mode = True + 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] diff --git a/frontend/app.js b/frontend/app.js index a6734b7..9b25b1f 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -5,6 +5,7 @@ const api = { oboruds: (audId) => `/oboruds/?aud_id=${encodeURIComponent(audId)}`, allOboruds: "/oboruds/?sort_by_inv=true", owners: "/owners/", + zametki: "/zametki/", }; async function fetchJSON(url) { @@ -21,6 +22,8 @@ createApp({ selectedAudId: '', oboruds: [], allOboruds: [], + unassignedOboruds: [], + totalOboruds: 0, status: '', error: '', printTitle: '', @@ -33,6 +36,33 @@ createApp({ 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: { @@ -117,6 +147,27 @@ createApp({ 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); @@ -129,6 +180,120 @@ createApp({ 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 = 'Сохранение владельца…'; @@ -235,6 +400,187 @@ createApp({ 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() { @@ -245,6 +591,7 @@ createApp({ } catch {} this.loadAuditories(); this.loadOwners(); + this.loadEquipmentTypes(); if (this.isAdmin) { this.loadUsers(); } diff --git a/frontend/index.html b/frontend/index.html index 1075bce..a3c2254 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@