diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js
index cb748a2..468115a 100644
--- a/miniapp/assets/app.js
+++ b/miniapp/assets/app.js
@@ -1,4 +1,4 @@
-// ЗОВ MiniApp — главный скрипт.
+// ЗОВ MiniApp — главный скрипт. v20260518i
// На входе: подписанный initData от Telegram.
// Ходим на backend → получаем профиль (роль, статус) → рендерим меню.
@@ -1684,6 +1684,11 @@ async function init() {
hideSplash();
return;
}
+ if (location.hash === "#/inbox") {
+ if (typeof InboxScreen !== "undefined") InboxScreen.mount(app);
+ hideSplash();
+ return;
+ }
if (location.hash.startsWith("#/assembly")) {
Assembly.mount(app);
hideSplash();
@@ -1741,6 +1746,9 @@ function routeByHash() {
MeasurementRequest.mount(app);
} else if (location.hash.startsWith("#/inbox/")) {
renderInboxDetail(location.hash.replace("#/inbox/", ""));
+ } else if (location.hash === "#/inbox") {
+ if (typeof InboxScreen !== "undefined") InboxScreen.mount(app);
+ else init();
} else if (location.hash.startsWith("#/assembly")) {
Assembly.mount(app);
} else if (location.hash.startsWith("#/master")) {
diff --git a/miniapp/assets/inbox.js b/miniapp/assets/inbox.js
new file mode 100644
index 0000000..fb9f48c
--- /dev/null
+++ b/miniapp/assets/inbox.js
@@ -0,0 +1,169 @@
+/* ============================================================
+ Входящие задачи менеджера — #/inbox
+ Замеры завершены, решение по подбору не принято.
+ ============================================================ */
+
+const InboxScreen = (function () {
+
+ function escHtml(s) {
+ return String(s == null ? "" : s)
+ .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+ }
+
+ function fmtDate(iso) {
+ if (!iso) return "—";
+ try {
+ return new Date(iso).toLocaleDateString("ru-RU", { day: "numeric", month: "short" });
+ } catch { return iso.slice(0, 10); }
+ }
+
+ async function _api(path, body) {
+ const ctrl = new AbortController();
+ const t = setTimeout(() => ctrl.abort(), 15000);
+ try {
+ const res = await fetch(`${BACKEND_URL}/api/${path}`, {
+ method: "POST", signal: ctrl.signal,
+ body: JSON.stringify({ initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null, ...body }),
+ });
+ return await res.json();
+ } catch (e) {
+ if (e.name === "AbortError") throw new Error("Сервер не отвечает");
+ throw e;
+ } finally { clearTimeout(t); }
+ }
+
+ function renderCard(item, listEl) {
+ const isLater = item.decision === "later";
+ const card = document.createElement("article");
+ card.className = "assembly-card";
+ card.style.cssText = "position:relative;";
+ card.innerHTML = `
+
+ ${isLater ? "🔁 Отложено" : "📐 Замер завершён"}
+ ${escHtml(fmtDate(item.ts))}
+
+ ${escHtml(item.client_name || "Без имени")}
+ ${item.address ? `${escHtml(item.address)}
` : ""}
+
+
+
+
+
+
+ `;
+
+ const resultEl = card.querySelector(".inbox-card-result");
+
+ async function decide(decision) {
+ const btns = card.querySelectorAll("button");
+ btns.forEach(b => { b.disabled = true; });
+ resultEl.textContent = "Сохраняем…";
+ try {
+ const data = await _api("measurement_decision", { measurement_id: item.id, decision });
+ if (data.error) {
+ resultEl.textContent = "Ошибка: " + data.error;
+ btns.forEach(b => { b.disabled = false; });
+ } else {
+ haptic && haptic("success");
+ card.style.transition = "opacity .3s";
+ card.style.opacity = "0";
+ setTimeout(() => {
+ card.remove();
+ if (!listEl.querySelector("article")) {
+ listEl.innerHTML = `Входящих задач нет 🎉
`;
+ }
+ }, 300);
+ }
+ } catch (e) {
+ resultEl.textContent = e.message;
+ btns.forEach(b => { b.disabled = false; });
+ }
+ }
+
+ card.querySelector("[data-action='podbor']").addEventListener("click", async () => {
+ haptic && haptic("impact");
+ await decide("needed");
+ sessionStorage.setItem("prefillClient", JSON.stringify({
+ name: item.client_name,
+ phone: item.client_phone,
+ measurement_id: item.id,
+ }));
+ location.hash = "#/podbor";
+ });
+
+ card.querySelector("[data-action='later']").addEventListener("click", () => {
+ haptic && haptic("impact");
+ decide("later");
+ });
+
+ card.querySelector("[data-action='skip']").addEventListener("click", () => {
+ haptic && haptic("impact");
+ decide("not_needed");
+ });
+
+ return card;
+ }
+
+ async function mount(container) {
+ container.innerHTML = "";
+ document.body.classList.remove("has-bottom-nav");
+ const oldNav = document.getElementById("bottom-nav");
+ if (oldNav) oldNav.remove();
+
+ // Header
+ const h = document.createElement("header");
+ h.className = "podbor-header";
+ h.innerHTML = `
+
+ Входящие
+
+ `;
+ h.querySelector(".podbor-back").addEventListener("click", () => {
+ haptic && haptic("impact");
+ history.back();
+ });
+ container.appendChild(h);
+
+ const screen = document.createElement("div");
+ screen.className = "podbor-screen";
+ container.appendChild(screen);
+
+ const loading = document.createElement("div");
+ loading.className = "loader-inline";
+ loading.innerHTML = ``;
+ screen.appendChild(loading);
+
+ try {
+ const data = await _api("manager_pending", {});
+ loading.remove();
+
+ if (data.error) {
+ screen.innerHTML = `${escHtml(data.error)}
`;
+ return;
+ }
+
+ const items = data.pending || [];
+ if (!items.length) {
+ screen.innerHTML = `Входящих задач нет 🎉
`;
+ return;
+ }
+
+ const count = document.createElement("div");
+ count.style.cssText = "padding:10px 16px 4px;font-size:13px;color:var(--muted);";
+ count.textContent = `${items.length} ${items.length === 1 ? "задача" : items.length < 5 ? "задачи" : "задач"}`;
+ screen.appendChild(count);
+
+ const list = document.createElement("div");
+ list.style.cssText = "padding:0 16px 24px;display:flex;flex-direction:column;gap:10px;";
+ items.forEach(item => list.appendChild(renderCard(item, list)));
+ screen.appendChild(list);
+
+ } catch (e) {
+ loading.remove();
+ screen.innerHTML = `Ошибка: ${escHtml(e.message)}
`;
+ }
+ }
+
+ return { mount };
+})();
diff --git a/miniapp/index.html b/miniapp/index.html
index 9847874..40250c4 100644
--- a/miniapp/index.html
+++ b/miniapp/index.html
@@ -46,6 +46,7 @@
-
+
+