Compare commits
24 Commits
1950bd4d45
...
feature/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a30857280 | ||
|
|
f750ef3a01 | ||
|
|
8e3cce0bb0 | ||
|
|
44961e3a9c | ||
|
|
1cadd41d26 | ||
|
|
d53b8b60c9 | ||
|
|
fc82b38c1f | ||
|
|
a315b9033a | ||
|
|
8715d21ea3 | ||
|
|
ec6088904c | ||
|
|
062d6de913 | ||
|
|
73ce68b071 | ||
|
|
68a8865dc6 | ||
|
|
aaf20ac9f3 | ||
|
|
76a9d4c1b3 | ||
|
|
04ff1b012b | ||
|
|
31f1fcecc4 | ||
|
|
67e7ab6f94 | ||
|
|
913e23b966 | ||
|
|
7219fbdada | ||
|
|
92fc020d94 | ||
|
|
dca8a0dbcf | ||
|
|
61d12f4972 | ||
|
|
2ae18dea27 |
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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
|
||||
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
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"]
|
||||
@@ -1,10 +1,13 @@
|
||||
# backend/database.py
|
||||
import os
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./app.db" # или PostgreSQL URL
|
||||
SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db")
|
||||
|
||||
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
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():
|
||||
|
||||
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()
|
||||
@@ -15,12 +15,14 @@ def create_oborud(item: schemas.OborudCreate, db: Session = Depends(database.get
|
||||
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, db: Session = Depends(database.get_db)):
|
||||
def list_oboruds(aud_id: Optional[int] = None, sort_by_inv: bool = False, unassigned: bool = False, inv_number: Optional[int] = None, db: Session = Depends(database.get_db)):
|
||||
query = db.query(models.Oboruds)
|
||||
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()
|
||||
|
||||
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
|
||||
@@ -17,7 +17,7 @@ async function fetchJSON(url) {
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
view: 'byAud',
|
||||
view: 'home',
|
||||
auditories: [],
|
||||
selectedAudId: '',
|
||||
oboruds: [],
|
||||
@@ -27,6 +27,11 @@ createApp({
|
||||
status: '',
|
||||
error: '',
|
||||
printTitle: '',
|
||||
// home view
|
||||
homeSearch: '',
|
||||
homeUnassigned: [],
|
||||
homeSearchResults: [],
|
||||
homeSearchDone: false,
|
||||
// auth/user management
|
||||
token: '',
|
||||
role: '',
|
||||
@@ -72,6 +77,69 @@ createApp({
|
||||
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');
|
||||
@@ -82,9 +150,7 @@ createApp({
|
||||
this.users = [];
|
||||
this.status = '';
|
||||
this.error = '';
|
||||
this.view = 'byAud';
|
||||
// опционально: редирект на страницу логина
|
||||
// window.location.href = '/login';
|
||||
window.location.href = '/login';
|
||||
},
|
||||
authHeaders() {
|
||||
const h = {};
|
||||
@@ -94,6 +160,10 @@ createApp({
|
||||
async fetchAuth(url, options = {}) {
|
||||
const opt = { ...options, headers: { ...(options.headers||{}), ...this.authHeaders() } };
|
||||
const res = await fetch(url, opt);
|
||||
if (res.status === 401) {
|
||||
this.logout();
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`HTTP ${res.status}: ${text}`);
|
||||
@@ -237,6 +307,16 @@ createApp({
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleString('ru-RU');
|
||||
},
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||||
},
|
||||
formatDateOnly(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('ru-RU');
|
||||
},
|
||||
async loadEquipmentTypes() {
|
||||
try {
|
||||
this.equipmentTypes = await fetchJSON('/equipment-types/');
|
||||
@@ -592,6 +672,7 @@ createApp({
|
||||
this.loadAuditories();
|
||||
this.loadOwners();
|
||||
this.loadEquipmentTypes();
|
||||
this.loadHomeUnassigned();
|
||||
if (this.isAdmin) {
|
||||
this.loadUsers();
|
||||
}
|
||||
|
||||
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
@@ -5,7 +5,7 @@
|
||||
<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" />
|
||||
<link rel="stylesheet" href="/app/styles.css?v=11" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="no-print">
|
||||
@@ -18,12 +18,13 @@
|
||||
<div class="row no-print">
|
||||
<nav class="no-print navbar navbar-expand-lg navbar-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/app/">Главная</a>
|
||||
<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">
|
||||
@@ -62,15 +63,109 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid px-4">
|
||||
<div class="container-fluid px-2 px-md-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 flex-grow-1" 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">
|
||||
<div class="mb-2 d-flex align-items-center justify-content-center gap-2 no-print aud-select-bar">
|
||||
<label for="aud-select" class="mb-0">Аудитория:</label>
|
||||
<select id="aud-select" class="form-select w-auto" v-model="selectedAudId">
|
||||
<select id="aud-select" class="form-select" style="max-width:220px;" v-model="selectedAudId">
|
||||
<option value="">— выберите аудиторию —</option>
|
||||
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option>
|
||||
</select>
|
||||
@@ -79,10 +174,10 @@
|
||||
</div>
|
||||
<div class="status no-print" :class="{error: !!error}">{{ status }}</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table datatable">
|
||||
<table class="table datatable byaud-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col" class="no-print">ID</th>
|
||||
<th scope="col">Инв. номер</th>
|
||||
<th scope="col">Название</th>
|
||||
<th scope="col">Расположение</th>
|
||||
@@ -94,19 +189,19 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="it in oboruds" :key="it.id">
|
||||
<td>{{ it.id }}</td>
|
||||
<td class="no-print">{{ 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">
|
||||
<td class="no-print owner-td">
|
||||
<template v-if="canEdit">
|
||||
<select class="form-select form-select-sm d-inline w-auto" v-model="it.selectedOwnerId">
|
||||
<select class="form-select form-select-sm" 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>
|
||||
<button class="btn btn-sm btn-outline-primary" @click="saveOwner(it)">Сохранить</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ it.owner?.name ?? '' }}
|
||||
@@ -131,7 +226,7 @@
|
||||
</div>
|
||||
<div class="status no-print" :class="{error: !!error}">{{ status }}</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table datatable">
|
||||
<table class="table datatable allequip-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="num-col">№</th>
|
||||
@@ -140,6 +235,7 @@
|
||||
<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>
|
||||
<th scope="col" class="no-print">Владелец</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -151,6 +247,7 @@
|
||||
<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.type?.name ?? '' }}</td>
|
||||
<td class="no-print">{{ it.owner?.name ?? '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -169,16 +266,16 @@
|
||||
<div v-else>
|
||||
<h5 class="mt-2">Создать пользователя-админа</h5>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<div class="col-12 col-sm-auto">
|
||||
<label class="form-label">Логин</label>
|
||||
<input class="form-control" v-model="newAdminUsername" placeholder="username" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="col-12 col-sm-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 class="col-12 col-sm-auto">
|
||||
<button class="btn btn-success w-100" @click="createAdmin">Создать админа</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
|
||||
@@ -216,12 +313,12 @@
|
||||
<div v-else>
|
||||
<h5 class="mt-2">Добавить аудиторию</h5>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<div class="col-12 col-sm-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 class="col-12 col-sm-auto">
|
||||
<button class="btn btn-success w-100" @click="createAuditory">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
|
||||
@@ -256,12 +353,12 @@
|
||||
<div v-else>
|
||||
<h5 class="mt-2">Добавить владельца</h5>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<div class="col-12 col-sm-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 class="col-12 col-sm-auto">
|
||||
<button class="btn btn-success w-100" @click="createOwner">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
|
||||
@@ -324,11 +421,11 @@
|
||||
|
||||
<div v-if="canEdit" class="mb-4">
|
||||
<h5>Добавить заметку</h5>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-8">
|
||||
<textarea class="form-control" v-model="newZametkaText" rows="3" placeholder="Текст заметки..."></textarea>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="col-md-8">
|
||||
<button class="btn btn-success" @click="createZametka">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,8 +438,10 @@
|
||||
<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>
|
||||
<small class="text-muted">{{ formatTime(z.created_date) }}</small>
|
||||
</div>
|
||||
<button v-if="canEdit" class="btn btn-sm btn-outline-success mt-1" @click="resolveZametka(z.id)">Решено</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -483,15 +582,15 @@
|
||||
<div v-if="!activeInspection">
|
||||
<h5>Начать новую проверку</h5>
|
||||
<div class="row g-2 align-items-end mb-3">
|
||||
<div class="col-auto">
|
||||
<div class="col-12 col-sm-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 class="col-12 col-sm-auto">
|
||||
<button class="btn btn-success w-100" @click="startInspection">Начать проверку</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -534,41 +633,48 @@
|
||||
</div>
|
||||
|
||||
<!-- Статистика прогресса -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<div class="row mb-3 g-2">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body py-2">
|
||||
<h5 class="card-title">{{ inspectionStats.total_checked }}</h5>
|
||||
<p class="card-text">Проверено</p>
|
||||
<p class="card-text small">Проверено</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body py-2">
|
||||
<h5 class="card-title">{{ inspectionStats.total_expected }}</h5>
|
||||
<p class="card-text">Всего</p>
|
||||
<p class="card-text small">Всего</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body py-2">
|
||||
<h5 class="card-title">{{ inspectionStats.total_unknown }}</h5>
|
||||
<p class="card-text">Не найдено</p>
|
||||
<p class="card-text small">Не найдено</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center h-100 bg-success text-white">
|
||||
<div class="card-body py-2">
|
||||
<h5 class="card-title">{{ inspectionStats.progress_percent }}%</h5>
|
||||
<p class="card-text">Прогресс</p>
|
||||
<p class="card-text small">Прогресс</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button class="btn btn-primary" @click="refreshInspectionData">Обновить данные</button>
|
||||
<button class="btn btn-success" @click="completeInspection">Завершить проверку</button>
|
||||
<button class="btn btn-secondary" @click="cancelInspection">Отменить</button>
|
||||
</div>
|
||||
|
||||
<!-- Поле для сканирования -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Сканируйте штрихкод или введите инв. номер</label>
|
||||
@@ -591,13 +697,6 @@
|
||||
</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">
|
||||
@@ -615,7 +714,7 @@
|
||||
<td>{{ rec.oborud?.invNumber }}</td>
|
||||
<td>{{ rec.oborud?.nazvanie }}</td>
|
||||
<td>{{ getAuditoryName(rec.oborud?.aud_id) }}</td>
|
||||
<td>{{ formatDate(rec.checked_at) }}</td>
|
||||
<td><div>{{ formatDateOnly(rec.checked_at) }}</div><div>{{ formatTime(rec.checked_at) }}</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -636,8 +735,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/app/app.js" defer></script>
|
||||
<script src="/app/vue.global.prod.js"></script>
|
||||
<script src="/app/bootstrap.bundle.min.js"></script>
|
||||
<script src="/app/app.js?v=4" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<link rel="stylesheet" href="/app/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/app/styles.css" />
|
||||
<style>
|
||||
.login-card { max-width: 420px; margin: 40px auto; }
|
||||
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>
|
||||
@@ -17,7 +19,7 @@
|
||||
<h2>Авторизация</h2>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<main>
|
||||
<div class="card login-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">Вход</h5>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
body {
|
||||
background-color: #E2F3FD;
|
||||
min-width: 580px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
@@ -38,6 +38,38 @@ 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 {
|
||||
@@ -93,7 +125,8 @@ td.inv-col, th.inv-col {
|
||||
}
|
||||
|
||||
.inv {
|
||||
width: 400px;
|
||||
width: auto;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.rasp {
|
||||
@@ -126,6 +159,149 @@ td.inv-col, th.inv-col {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
header h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 4px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.rasp {
|
||||
width: auto;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.table td, .table th {
|
||||
font-size: 0.69rem;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.byaud-table th,
|
||||
.allequip-table th {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* byAud table: Название и Расположение — приоритет */
|
||||
.byaud-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
/* ID */
|
||||
.byaud-table th:nth-child(1),
|
||||
.byaud-table td:nth-child(1) { width: 6%; }
|
||||
/* Инв. номер */
|
||||
.byaud-table th:nth-child(2),
|
||||
.byaud-table td:nth-child(2) { width: 12%; }
|
||||
/* Название — auto, gets most remaining space */
|
||||
/* Расположение */
|
||||
.byaud-table th:nth-child(4),
|
||||
.byaud-table td:nth-child(4) { width: 20%; }
|
||||
/* Кол-во */
|
||||
.byaud-table th:nth-child(5),
|
||||
.byaud-table td:nth-child(5) { width: 7%; }
|
||||
/* Тип */
|
||||
.byaud-table th:nth-child(6),
|
||||
.byaud-table td:nth-child(6) { width: 12%; }
|
||||
/* Владелец */
|
||||
.byaud-table th:nth-child(7),
|
||||
.byaud-table td:nth-child(7) { width: 25%; }
|
||||
|
||||
/* allEquipment table: Название и Расположение — приоритет */
|
||||
.allequip-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
/* № */
|
||||
.allequip-table th:nth-child(1),
|
||||
.allequip-table td:nth-child(1) { width: 5%; }
|
||||
/* Инв. номер */
|
||||
.allequip-table th:nth-child(2),
|
||||
.allequip-table td:nth-child(2) { width: 12%; }
|
||||
/* Название — auto, gets most space */
|
||||
/* Аудитория */
|
||||
.allequip-table th:nth-child(4),
|
||||
.allequip-table td:nth-child(4) { width: 9%; }
|
||||
/* Расположение — auto, shares space with Название */
|
||||
/* Кол-во */
|
||||
.allequip-table th:nth-child(6),
|
||||
.allequip-table td:nth-child(6) { width: 7%; }
|
||||
/* Тип */
|
||||
.allequip-table th:nth-child(7),
|
||||
.allequip-table td:nth-child(7) { width: 13%; }
|
||||
/* Владелец */
|
||||
.allequip-table th:nth-child(8),
|
||||
.allequip-table td:nth-child(8) { width: 15%; }
|
||||
|
||||
.owner-td select {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
margin-bottom: 3px;
|
||||
font-size: 0.69rem;
|
||||
}
|
||||
|
||||
.owner-td .btn {
|
||||
width: 100%;
|
||||
padding: 2px 4px;
|
||||
font-size: 0.69rem;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.aud-select-bar {
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.aud-select-bar label {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.aud-select-bar select,
|
||||
.aud-select-bar .btn {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.form-row-mobile {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.form-row-mobile .col-auto,
|
||||
.form-row-mobile [class*="col-"] {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.btn-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-bar .btn {
|
||||
flex: 1 1 auto;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
* {
|
||||
font-family: "Times New Roman", Times, serif;
|
||||
@@ -177,13 +353,13 @@ td.inv-col, th.inv-col {
|
||||
}
|
||||
|
||||
.table {
|
||||
border: 1px solid #000000;
|
||||
border: 1px solid #E07D54;
|
||||
margin-top: 20px;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
border: 1px solid #000000;
|
||||
border: 1px solid #E07D54;
|
||||
padding: 5px;
|
||||
font-size: 12pt;
|
||||
word-break: normal;
|
||||
@@ -199,19 +375,19 @@ td.inv-col, th.inv-col {
|
||||
}
|
||||
|
||||
table.rs-table-bordered {
|
||||
border: 1px solid #000000;
|
||||
border: 1px solid #E07D54;
|
||||
margin-top: 20px;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
table.rs-table-bordered > thead > tr > th {
|
||||
border: 1px solid #000000;
|
||||
border: 1px solid #E07D54;
|
||||
padding: 2px;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
table.rs-table-bordered > tbody > tr > td {
|
||||
border: 1px solid #000000;
|
||||
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
Reference in New Issue
Block a user