Compare commits
2 Commits
1950bd4d45
...
61d12f4972
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61d12f4972 | ||
|
|
2ae18dea27 |
@@ -15,12 +15,14 @@ def create_oborud(item: schemas.OborudCreate, db: Session = Depends(database.get
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@oboruds.get("/", response_model=list[schemas.OborudRead])
|
@oboruds.get("/", response_model=list[schemas.OborudRead])
|
||||||
def list_oboruds(aud_id: Optional[int] = None, 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)
|
query = db.query(models.Oboruds)
|
||||||
if unassigned:
|
if unassigned:
|
||||||
query = query.filter(models.Oboruds.aud_id == None)
|
query = query.filter(models.Oboruds.aud_id == None)
|
||||||
elif aud_id is not None:
|
elif aud_id is not None:
|
||||||
query = query.filter(models.Oboruds.aud_id == aud_id)
|
query = query.filter(models.Oboruds.aud_id == aud_id)
|
||||||
|
if inv_number is not None:
|
||||||
|
query = query.filter(models.Oboruds.invNumber == inv_number)
|
||||||
if sort_by_inv:
|
if sort_by_inv:
|
||||||
query = query.order_by(models.Oboruds.invNumber.asc())
|
query = query.order_by(models.Oboruds.invNumber.asc())
|
||||||
return query.all()
|
return query.all()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ async function fetchJSON(url) {
|
|||||||
createApp({
|
createApp({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
view: 'byAud',
|
view: 'home',
|
||||||
auditories: [],
|
auditories: [],
|
||||||
selectedAudId: '',
|
selectedAudId: '',
|
||||||
oboruds: [],
|
oboruds: [],
|
||||||
@@ -27,6 +27,11 @@ createApp({
|
|||||||
status: '',
|
status: '',
|
||||||
error: '',
|
error: '',
|
||||||
printTitle: '',
|
printTitle: '',
|
||||||
|
// home view
|
||||||
|
homeSearch: '',
|
||||||
|
homeUnassigned: [],
|
||||||
|
homeSearchResults: [],
|
||||||
|
homeSearchDone: false,
|
||||||
// auth/user management
|
// auth/user management
|
||||||
token: '',
|
token: '',
|
||||||
role: '',
|
role: '',
|
||||||
@@ -72,6 +77,69 @@ createApp({
|
|||||||
canEdit() { return this.isAdmin || this.isEditor; },
|
canEdit() { return this.isAdmin || this.isEditor; },
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
logout() {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem('access_token');
|
localStorage.removeItem('access_token');
|
||||||
@@ -592,6 +660,7 @@ createApp({
|
|||||||
this.loadAuditories();
|
this.loadAuditories();
|
||||||
this.loadOwners();
|
this.loadOwners();
|
||||||
this.loadEquipmentTypes();
|
this.loadEquipmentTypes();
|
||||||
|
this.loadHomeUnassigned();
|
||||||
if (this.isAdmin) {
|
if (this.isAdmin) {
|
||||||
this.loadUsers();
|
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
@@ -18,12 +18,13 @@
|
|||||||
<div class="row no-print">
|
<div class="row no-print">
|
||||||
<nav class="no-print navbar navbar-expand-lg navbar-light">
|
<nav class="no-print navbar navbar-expand-lg navbar-light">
|
||||||
<div class="container-fluid">
|
<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">
|
<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>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
<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">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Оборудование</a>
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Оборудование</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
@@ -63,6 +64,100 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container-fluid px-4">
|
<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 v-if="view==='byAud'" class="row">
|
||||||
<div class="card col-12">
|
<div class="card col-12">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -636,8 +731,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="/app/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/bootstrap.bundle.min.js"></script>
|
||||||
<script src="/app/app.js" defer></script>
|
<script src="/app/app.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
<link rel="stylesheet" href="/app/bootstrap.min.css" />
|
<link rel="stylesheet" href="/app/bootstrap.min.css" />
|
||||||
<link rel="stylesheet" href="/app/styles.css" />
|
<link rel="stylesheet" href="/app/styles.css" />
|
||||||
<style>
|
<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; }
|
.muted { color: #6c757d; font-size: 0.9rem; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -17,7 +19,7 @@
|
|||||||
<h2>Авторизация</h2>
|
<h2>Авторизация</h2>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main>
|
||||||
<div class="card login-card">
|
<div class="card login-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title mb-3">Вход</h5>
|
<h5 class="card-title mb-3">Вход</h5>
|
||||||
|
|||||||
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