Files
asuinventory/frontend/app.js
Your Name 2ae18dea27 feat: add home page with search/unassigned tables; center login card
- Add home view as default: search by inv number, unassigned equipment
  table with auditory assignment, search results (assigned) table
- Add inv_number query param to GET /oboruds/ for backend search
- Center login card vertically and horizontally via flexbox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:50:52 +03:00

669 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { createApp } = Vue;
const api = {
auds: "/auditories/",
oboruds: (audId) => `/oboruds/?aud_id=${encodeURIComponent(audId)}`,
allOboruds: "/oboruds/?sort_by_inv=true",
owners: "/owners/",
zametki: "/zametki/",
};
async function fetchJSON(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
createApp({
data() {
return {
view: 'home',
auditories: [],
selectedAudId: '',
oboruds: [],
allOboruds: [],
unassignedOboruds: [],
totalOboruds: 0,
status: '',
error: '',
printTitle: '',
// home view
homeSearch: '',
homeUnassigned: [],
homeSearchResults: [],
homeSearchDone: false,
// auth/user management
token: '',
role: '',
users: [],
newAdminUsername: '',
newAdminPassword: '',
newAudName: '',
owners: [],
newOwnerName: '',
zametki: [],
newZametkaText: '',
equipmentTypes: [],
newTypeName: '',
newEquipment: {
invNumber: null,
nazvanie: '',
raspologenie: '',
kolichestvo: 1,
aud_id: '',
type_id: '',
owner_id: ''
},
// Inspection
activeInspection: null,
inspectionAudId: '',
scannedBarcode: '',
lastScanResult: null,
inspectionStats: {
total_checked: 0,
total_expected: 0,
total_unknown: 0,
progress_percent: 0
},
checkedEquipment: [],
unknownBarcodes: [],
inspectionHistory: []
};
},
computed: {
isAuth() { return !!this.token; },
isAdmin() { return this.role === 'admin'; },
isEditor() { return this.role === 'editor'; },
canEdit() { return this.isAdmin || this.isEditor; },
},
methods: {
async showHome() {
this.view = 'home';
this.status = '';
this.error = '';
this.homeSearch = '';
this.homeSearchResults = [];
this.homeSearchDone = false;
await this.loadHomeUnassigned();
},
async loadHomeUnassigned() {
this.status = 'Загрузка нераспределённого оборудования…';
this.error = '';
try {
const data = await fetchJSON('/oboruds/?unassigned=true&sort_by_inv=true');
this.homeUnassigned = data.map(it => ({ ...it, selectedAudId: '' }));
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить данные';
this.status = '';
}
},
async doHomeSearch() {
const q = this.homeSearch.trim();
if (!q) {
this.status = 'Введите инвентарный номер';
return;
}
this.status = 'Поиск…';
this.error = '';
this.homeSearchDone = false;
try {
const data = await fetchJSON(`/oboruds/?inv_number=${encodeURIComponent(q)}`);
this.homeSearchResults = data.filter(it => it.aud_id).map(it => ({ ...it, selectedAudId: '' }));
this.homeSearchDone = true;
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось выполнить поиск';
this.status = '';
}
},
async assignToAuditory(item) {
if (!item.selectedAudId) {
this.status = 'Выберите аудиторию';
return;
}
try {
this.status = 'Сохранение…';
this.error = '';
await this.fetchAuth(`/oboruds/${item.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aud_id: item.selectedAudId }),
});
await this.loadHomeUnassigned();
this.status = 'Сохранено';
} catch (e) {
console.error(e);
this.error = 'Не удалось назначить аудиторию';
this.status = '';
}
},
logout() {
try {
localStorage.removeItem('access_token');
localStorage.removeItem('role');
} catch {}
this.token = '';
this.role = '';
this.users = [];
this.status = '';
this.error = '';
this.view = 'byAud';
// опционально: редирект на страницу логина
// window.location.href = '/login';
},
authHeaders() {
const h = {};
if (this.token) h['Authorization'] = `Bearer ${this.token}`;
return h;
},
async fetchAuth(url, options = {}) {
const opt = { ...options, headers: { ...(options.headers||{}), ...this.authHeaders() } };
const res = await fetch(url, opt);
if (!res.ok) {
const text = await res.text();
throw new Error(`HTTP ${res.status}: ${text}`);
}
return res.json();
},
async loadAuditories() {
this.status = 'Загрузка аудиторий…';
this.error = '';
try {
this.auditories = await fetchJSON(api.auds);
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить аудитории';
this.status = '';
}
},
async loadOboruds() {
if (!this.selectedAudId) {
this.error = '';
this.status = 'Выберите аудиторию';
return;
}
this.status = 'Загрузка оборудования…';
this.error = '';
try {
this.oboruds = await fetchJSON(api.oboruds(this.selectedAudId));
// init selected owner helper field
this.oboruds.forEach(o => { o.selectedOwnerId = o.owner?.id || ''; });
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить оборудование';
this.status = '';
}
},
async loadAllOboruds() {
this.status = 'Загрузка всего оборудования…';
this.error = '';
try {
this.allOboruds = await fetchJSON(api.allOboruds);
this.status = `Загружено ${this.allOboruds.length} записей`;
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить оборудование';
this.status = '';
}
},
async showAllEquipment() {
this.view = 'allEquipment';
await this.loadAllOboruds();
},
async loadUnassigned() {
this.status = 'Загрузка нераспределённого оборудования…';
this.error = '';
try {
const [oboruds, stats] = await Promise.all([
fetchJSON('/oboruds/?unassigned=true&sort_by_inv=true'),
fetchJSON('/oboruds/stats')
]);
this.unassignedOboruds = oboruds;
this.totalOboruds = stats.total;
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить данные';
this.status = '';
}
},
async showUnassigned() {
this.view = 'unassigned';
await this.loadUnassigned();
},
getAuditoryName(audId) {
if (!audId) return '';
const aud = this.auditories.find(a => a.id === audId);
return aud ? aud.audnazvanie : '';
},
printPage() {
const aud = this.auditories.find(a => a.id === this.selectedAudId);
this.printTitle = 'Аудитория № ' + (aud ? aud.audnazvanie : '');
this.$nextTick(() => {
window.print();
});
},
printAllEquipment() {
window.print();
},
async loadZametki() {
this.status = 'Загрузка заметок…';
this.error = '';
try {
this.zametki = await fetchJSON(api.zametki);
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить заметки';
this.status = '';
}
},
async showZametki() {
this.view = 'zametki';
await this.loadZametki();
},
async createZametka() {
if (!this.newZametkaText.trim()) {
this.status = 'Введите текст заметки';
return;
}
try {
this.status = 'Добавление заметки…';
await this.fetchAuth(api.zametki, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ txtzam: this.newZametkaText }),
});
this.newZametkaText = '';
await this.loadZametki();
this.status = 'Заметка добавлена';
} catch (e) {
console.error(e);
this.error = 'Не удалось добавить заметку';
this.status = '';
}
},
async resolveZametka(id) {
try {
this.status = 'Отметка заметки как решённой…';
await this.fetchAuth(`/zametki/${id}/resolve`, { method: 'PATCH' });
await this.loadZametki();
this.status = 'Заметка отмечена как решённая';
} catch (e) {
console.error(e);
this.error = 'Не удалось отметить заметку';
this.status = '';
}
},
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleString('ru-RU');
},
async loadEquipmentTypes() {
try {
this.equipmentTypes = await fetchJSON('/equipment-types/');
} catch (e) {
console.error(e);
}
},
async createEquipmentType() {
if (!this.newTypeName.trim()) {
this.status = 'Введите название типа';
return;
}
try {
this.status = 'Добавление типа…';
await this.fetchAuth('/equipment-types/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: this.newTypeName }),
});
this.newTypeName = '';
await this.loadEquipmentTypes();
this.status = 'Тип добавлен';
} catch (e) {
console.error(e);
this.error = 'Не удалось добавить тип';
this.status = '';
}
},
async createEquipment() {
if (!this.newEquipment.nazvanie.trim()) {
this.status = 'Введите название оборудования';
return;
}
try {
this.status = 'Добавление оборудования…';
const data = {
nazvanie: this.newEquipment.nazvanie,
invNumber: this.newEquipment.invNumber || null,
raspologenie: this.newEquipment.raspologenie || null,
kolichestvo: this.newEquipment.kolichestvo || null,
aud_id: this.newEquipment.aud_id || null,
type_id: this.newEquipment.type_id || null,
owner_id: this.newEquipment.owner_id || null,
};
await this.fetchAuth('/oboruds/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
this.newEquipment = { invNumber: null, nazvanie: '', raspologenie: '', kolichestvo: 1, aud_id: '', type_id: '', owner_id: '' };
this.status = 'Оборудование добавлено';
} catch (e) {
console.error(e);
this.error = 'Не удалось добавить оборудование';
this.status = '';
}
},
async saveOwner(item) {
try {
this.status = 'Сохранение владельца…';
await this.fetchAuth(`/oboruds/${item.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ owner_id: item.selectedOwnerId || null }),
});
this.status = 'Сохранено';
} catch (e) {
console.error(e);
this.error = 'Не удалось сохранить владельца';
this.status = '';
}
},
async loadOwners() {
try {
this.status = this.status || 'Загрузка владельцев…';
const data = await fetchJSON(api.owners);
this.owners = data;
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить владельцев';
this.status = '';
}
},
async createOwner() {
if (!this.newOwnerName) {
this.status = 'Укажите имя владельца';
return;
}
try {
this.status = 'Добавление владельца…';
await this.fetchAuth(api.owners, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: this.newOwnerName }),
});
this.newOwnerName = '';
await this.loadOwners();
// refresh table owner names if visible
if (this.selectedAudId) await this.loadOboruds();
this.status = 'Владелец добавлен';
} catch (e) {
console.error(e);
this.error = 'Не удалось добавить владельца';
this.status = '';
}
},
async loadUsers() {
try {
this.status = 'Загрузка пользователей…';
this.error = '';
this.users = await this.fetchAuth('/auth/users');
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить пользователей';
this.status = '';
}
},
async createAdmin() {
if (!this.newAdminUsername || !this.newAdminPassword) {
this.status = 'Укажите логин и пароль';
return;
}
try {
this.status = 'Создание администратора…';
this.error = '';
await this.fetchAuth('/auth/users/admin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: this.newAdminUsername, password: this.newAdminPassword }),
});
this.newAdminUsername = '';
this.newAdminPassword = '';
await this.loadUsers();
this.status = 'Администратор создан';
} catch (e) {
console.error(e);
this.error = 'Не удалось создать администратора';
this.status = '';
}
},
async createAuditory() {
if (!this.newAudName) {
this.status = 'Укажите название аудитории';
return;
}
try {
this.status = 'Добавление аудитории…';
this.error = '';
await this.fetchAuth('/auditories/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ audnazvanie: this.newAudName }),
});
this.newAudName = '';
await this.loadAuditories();
this.status = 'Аудитория добавлена';
} catch (e) {
console.error(e);
this.error = 'Не удалось добавить аудиторию';
this.status = '';
}
},
// Inspection methods
async showInspection() {
this.view = 'inspection';
this.status = '';
this.error = '';
},
async startInspection() {
try {
this.status = 'Начало проверки…';
this.error = '';
const response = await this.fetchAuth('/inspections/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
aud_id: this.inspectionAudId || null
})
});
this.activeInspection = response;
await this.loadInspectionStats();
this.status = 'Проверка начата';
// Фокус на поле ввода
this.$nextTick(() => {
if (this.$refs.barcodeInput) {
this.$refs.barcodeInput.focus();
}
});
} catch (e) {
console.error(e);
this.error = 'Не удалось начать проверку: ' + e.message;
this.status = '';
}
},
async checkBarcode() {
if (!this.scannedBarcode.trim()) return;
try {
this.status = 'Проверка штрихкода…';
const response = await this.fetchAuth(`/inspections/sessions/${this.activeInspection.id}/check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
inv_number: this.scannedBarcode
})
});
this.lastScanResult = response;
// Очистить поле и обновить данные
this.scannedBarcode = '';
await this.loadInspectionStats();
await this.loadInspectionDetails();
this.status = '';
// Автоматически скрыть уведомление через 3 секунды
setTimeout(() => {
this.lastScanResult = null;
}, 3000);
// Вернуть фокус на поле ввода
this.$nextTick(() => {
if (this.$refs.barcodeInput) {
this.$refs.barcodeInput.focus();
}
});
} catch (e) {
console.error(e);
this.error = 'Не удалось проверить штрихкод: ' + e.message;
this.status = '';
}
},
async loadInspectionStats() {
try {
const response = await this.fetchAuth(`/inspections/sessions/${this.activeInspection.id}`);
this.inspectionStats = response;
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить статистику';
}
},
async loadInspectionDetails() {
try {
const response = await this.fetchAuth(`/inspections/sessions/${this.activeInspection.id}/records`);
this.checkedEquipment = response.records;
this.unknownBarcodes = response.unknown_barcodes;
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить детали проверки';
}
},
async refreshInspectionData() {
this.status = 'Обновление данных…';
await this.loadInspectionStats();
await this.loadInspectionDetails();
this.status = 'Данные обновлены';
},
async completeInspection() {
if (!confirm('Завершить проверку?')) return;
try {
this.status = 'Завершение проверки…';
await this.fetchAuth(`/inspections/sessions/${this.activeInspection.id}/complete`, {
method: 'POST'
});
this.activeInspection = null;
this.inspectionStats = {
total_checked: 0,
total_expected: 0,
total_unknown: 0,
progress_percent: 0
};
this.checkedEquipment = [];
this.unknownBarcodes = [];
this.lastScanResult = null;
this.status = 'Проверка завершена';
} catch (e) {
console.error(e);
this.error = 'Не удалось завершить проверку: ' + e.message;
this.status = '';
}
},
cancelInspection() {
if (confirm('Прервать проверку без сохранения?')) {
this.activeInspection = null;
this.inspectionStats = {
total_checked: 0,
total_expected: 0,
total_unknown: 0,
progress_percent: 0
};
this.checkedEquipment = [];
this.unknownBarcodes = [];
this.lastScanResult = null;
this.status = 'Проверка отменена';
}
},
async loadInspectionHistory() {
try {
this.status = 'Загрузка истории проверок…';
this.error = '';
this.inspectionHistory = await this.fetchAuth('/inspections/sessions');
this.status = `Загружено ${this.inspectionHistory.length} проверок`;
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить историю проверок: ' + e.message;
this.status = '';
}
},
async viewHistoryDetails(sessionId) {
try {
this.status = 'Загрузка деталей проверки…';
const [stats, details] = await Promise.all([
this.fetchAuth(`/inspections/sessions/${sessionId}`),
this.fetchAuth(`/inspections/sessions/${sessionId}/records`)
]);
// Показать активную проверку с данными истории
this.activeInspection = stats.session;
this.inspectionStats = stats;
this.checkedEquipment = details.records;
this.unknownBarcodes = details.unknown_barcodes;
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить детали: ' + e.message;
this.status = '';
}
}
},
mounted() {
// read auth from localStorage
try {
this.token = localStorage.getItem('access_token') || '';
this.role = localStorage.getItem('role') || '';
} catch {}
this.loadAuditories();
this.loadOwners();
this.loadEquipmentTypes();
this.loadHomeUnassigned();
if (this.isAdmin) {
this.loadUsers();
}
}
}).mount('#app');