fix aud search on center

This commit is contained in:
Your Name
2026-05-11 15:12:34 +03:00
parent 54534ee490
commit 9491909f24
61 changed files with 1049 additions and 2774 deletions

View File

@@ -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();
}

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>АСУ Инвентаризация</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
<link rel="stylesheet" href="/app/bootstrap.min.css" />
<link rel="stylesheet" href="/app/styles.css" />
</head>
<body>
@@ -27,10 +27,23 @@
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="#" @click.prevent="view='byAud'">По аудитории</a></li>
<li class="nav-item"><a class="nav-link" href="#" @click.prevent="showAllEquipment">Всё оборудование</a></li>
<li class="nav-item"><a class="nav-link" href="#" @click.prevent="showUnassigned">Не распределено</a></li>
<li class="nav-item" v-if="isAuth"><a class="nav-link" href="#" @click.prevent="showInspection">Проверка</a></li>
<li class="nav-item" v-if="isAdmin"><a class="nav-link" href="#" @click.prevent="view='users'">Пользователи</a></li>
<li class="nav-item" v-if="isAdmin"><a class="nav-link" href="#" @click.prevent="view='audManage'">Аудитории</a></li>
<li class="nav-item"><a class="nav-link" href="/docs" target="_blank">API Docs</a></li>
<li class="nav-item" v-if="canEdit"><a class="nav-link" href="#" @click.prevent="view='owners'">Владельцы</a></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>
@@ -46,8 +59,8 @@
<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 gap-2 no-print">
<label for="aud-select" class="me-2">Аудитория:</label>
<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>
@@ -102,30 +115,34 @@
<div v-if="view==='allEquipment'" class="row">
<div class="card col-12">
<div class="card-body">
<h3 class="card-title">Всё оборудование (по инв. номеру)</h3>
<div class="status" :class="{error: !!error}">{{ status }}</div>
<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"></th>
<th scope="col">Инв. номер</th>
<th scope="col" class="num-col"></th>
<th scope="col" class="inv-col">Инв. номер</th>
<th scope="col">Название</th>
<th scope="col">Аудитория</th>
<th scope="col" class="aud-col">Аудитория</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>
</tr>
</thead>
<tbody>
<tr v-for="(it, index) in allOboruds" :key="it.id">
<td>{{ index + 1 }}</td>
<td class="inv">{{ it.invNumber ?? '' }}</td>
<td class="num-col">{{ index + 1 }}</td>
<td class="inv-col">{{ it.invNumber ?? '' }}</td>
<td>{{ it.nazvanie ?? '' }}</td>
<td>{{ getAuditoryName(it.aud_id) }}</td>
<td class="aud-col">{{ getAuditoryName(it.aud_id) }}</td>
<td class="rasp">{{ it.raspologenie ?? '' }}</td>
<td>{{ it.kolichestvo ?? '' }}</td>
<td>{{ it.owner?.name ?? '' }}</td>
<td class="no-print">{{ it.kolichestvo ?? '' }}</td>
<td class="no-print">{{ it.owner?.name ?? '' }}</td>
</tr>
</tbody>
</table>
@@ -261,9 +278,357 @@
</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="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>
</body>
</html>

View File

@@ -4,8 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Вход — АСУ Инвентаризация</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/index.css" />
<link rel="stylesheet" href="/app/bootstrap.min.css" />
<link rel="stylesheet" href="/app/styles.css" />
<style>
.login-card { max-width: 420px; margin: 40px auto; }
.muted { color: #6c757d; font-size: 0.9rem; }

View File

@@ -75,6 +75,23 @@ table {
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;
}
@@ -169,6 +186,16 @@ table {
border: 1px solid #000000;
padding: 5px;
font-size: 12pt;
word-break: normal;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
}
td.inv-col, th.inv-col {
width: 100px;
min-width: 100px;
max-width: 100px;
}
table.rs-table-bordered {