diff --git a/backend/main.py b/backend/main.py
index b18280c..3c90fb5 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1,6 +1,7 @@
# backend/main.py
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
+from starlette.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from backend.routers.equipment_types import equipment_types
@@ -29,9 +30,12 @@ def ping():
return {"message": "pong"}
+# Serve static frontend
+app.mount("/app", StaticFiles(directory="frontend", html=True), name="frontend")
+
@app.get("/")
def root():
- return RedirectResponse(url="/docs")
+ return RedirectResponse(url="/app/")
# Подключение роутов
diff --git a/frontend/app.js b/frontend/app.js
new file mode 100644
index 0000000..ab3e6e0
--- /dev/null
+++ b/frontend/app.js
@@ -0,0 +1,76 @@
+const api = {
+ auds: "/auditories/",
+ oboruds: (audId) => `/oboruds/?aud_id=${encodeURIComponent(audId)}`,
+};
+
+async function fetchJSON(url) {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ return res.json();
+}
+
+function setStatus(msg, type = "info") {
+ const el = document.getElementById("status");
+ el.textContent = msg || "";
+ el.className = `status ${type}`;
+}
+
+async function loadAuditories() {
+ setStatus("Загрузка аудиторий…");
+ try {
+ const data = await fetchJSON(api.auds);
+ const select = document.getElementById("aud-select");
+ select.innerHTML = '';
+ data.forEach((a) => {
+ const opt = document.createElement("option");
+ opt.value = a.id;
+ opt.textContent = `${a.id} — ${a.audnazvanie}`;
+ select.appendChild(opt);
+ });
+ setStatus("");
+ } catch (e) {
+ console.error(e);
+ setStatus("Не удалось загрузить аудитории", "error");
+ }
+}
+
+function renderOboruds(items) {
+ const tbody = document.querySelector("#ob-table tbody");
+ tbody.innerHTML = "";
+ items.forEach((it) => {
+ const tr = document.createElement("tr");
+ tr.innerHTML = `
+
${it.id} |
+ ${it.invNumber ?? ""} |
+ ${it.nazvanie ?? ""} |
+ ${it.raspologenie ?? ""} |
+ ${it.kolichestvo ?? ""} |
+ ${it.type?.name ?? ""} |
+ `;
+ tbody.appendChild(tr);
+ });
+}
+
+async function loadOborudsForSelected() {
+ const select = document.getElementById("aud-select");
+ const audId = select.value;
+ if (!audId) {
+ setStatus("Выберите аудиторию", "warn");
+ return;
+ }
+ setStatus("Загрузка оборудования…");
+ try {
+ const data = await fetchJSON(api.oboruds(audId));
+ renderOboruds(data);
+ setStatus("");
+ } catch (e) {
+ console.error(e);
+ setStatus("Не удалось загрузить оборудование", "error");
+ }
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("load-btn").addEventListener("click", loadOborudsForSelected);
+ loadAuditories();
+});
+
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..d290d84
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ ASU Inventory — Фронт
+
+
+
+
+
+
+
+ Оборудование по аудитории
+
+
+
+
+
+
+
+
+
+ | ID |
+ Инв. номер |
+ Название |
+ Расположение |
+ Кол-во |
+ Тип |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/styles.css b/frontend/styles.css
new file mode 100644
index 0000000..f24719b
--- /dev/null
+++ b/frontend/styles.css
@@ -0,0 +1,19 @@
+:root { --bg: #0f172a; --fg: #e2e8f0; --muted: #94a3b8; --accent: #38bdf8; --err: #ef4444; --warn: #f59e0b; }
+* { box-sizing: border-box; }
+body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background: var(--bg); color: var(--fg); }
+header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid #1f2937; }
+header h1 { margin: 0; font-size: 20px; }
+header nav a { color: var(--accent); text-decoration: none; }
+main { padding: 20px; max-width: 1000px; margin: 0 auto; }
+.panel { background: #0b1220; border: 1px solid #1f2937; border-radius: 8px; padding: 16px; }
+.controls { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
+select, button { padding: 8px 10px; border-radius: 6px; border: 1px solid #1f2937; background: #0a0f1a; color: var(--fg); }
+button { cursor: pointer; }
+button:hover { border-color: var(--accent); }
+.status { min-height: 20px; color: var(--muted); margin-bottom: 8px; }
+.status.error { color: var(--err); }
+.status.warn { color: var(--warn); }
+table { width: 100%; border-collapse: collapse; }
+th, td { border-bottom: 1px solid #1f2937; padding: 8px; text-align: left; }
+th { color: var(--muted); font-weight: 600; }
+