/* ============================================================
Таймлайн заказа клиента — #/c/assembly/:id/timeline
Доступен клиенту, менеджеру, назначенному сборщику.
============================================================ */
const ClientTimeline = (function () {
function escHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
}
function fmtDate(iso) {
if (!iso) return null;
try {
return new Date(iso).toLocaleDateString("ru-RU", {
day: "numeric", month: "long",
hour: "2-digit", minute: "2-digit",
});
} catch { return iso.slice(0, 16).replace("T", " "); }
}
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,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
initData: tg?.initData || "",
initDataUnsafe: tg?.initDataUnsafe || null,
...body,
}),
});
if (!res.ok) throw new Error(`Ошибка сервера (${res.status})`);
return await res.json();
} catch (e) {
if (e.name === "AbortError") throw new Error("Сервер не отвечает");
throw e;
} finally { clearTimeout(t); }
}
const STATUS_COLORS = {
created: "#8e8e8e",
scheduled: "#2980B9",
in_progress: "#F39C12",
done: "#27AE60",
cancelled: "#C0392B",
};
const STATUS_LABELS = {
created: "Создана",
scheduled: "Запланирована",
in_progress: "В процессе",
done: "Завершена",
cancelled: "Отменена",
};
function mount(container, assemblyId) {
container.innerHTML = "";
document.body.classList.remove("has-bottom-nav");
const oldNav = document.getElementById("bottom-nav");
if (oldNav) oldNav.remove();
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";
screen.style.cssText = "padding:0 0 48px;";
screen.innerHTML = ``;
container.appendChild(screen);
_api("client_order_timeline", { assembly_id: assemblyId })
.then(data => {
if (data.error) {
screen.innerHTML = `${escHtml(data.error)}
`;
return;
}
screen.innerHTML = "";
// Шапка — название + статус
const statusColor = STATUS_COLORS[data.status] || "#8e8e8e";
const statusText = STATUS_LABELS[data.status] || data.status;
const titleEl = document.createElement("div");
titleEl.style.cssText = "padding:16px 16px 12px;border-bottom:1px solid var(--border);";
titleEl.innerHTML = `
${escHtml(data.client_name || "Заказ")}
${data.address ? `
📍 ${escHtml(data.address)}
` : ""}
${escHtml(statusText)}
`;
screen.appendChild(titleEl);
// Подсказка прогресса
const milestones = data.milestones || [];
const doneCount = milestones.filter(m => m.done).length;
const total = milestones.length;
const pct = total ? Math.round((doneCount / total) * 100) : 0;
const progressEl = document.createElement("div");
progressEl.style.cssText = "padding:12px 16px;border-bottom:1px solid var(--border);";
progressEl.innerHTML = `
Выполнено этапов
${doneCount} / ${total}
`;
screen.appendChild(progressEl);
// Таймлайн
const tlWrap = document.createElement("div");
tlWrap.style.cssText = "padding:16px;";
milestones.forEach((ms, idx) => {
const isLast = idx === milestones.length - 1;
const row = document.createElement("div");
row.style.cssText = "display:flex;gap:12px;";
// Левая колонка: точка + линия
const lineCol = document.createElement("div");
lineCol.style.cssText = "display:flex;flex-direction:column;align-items:center;width:36px;flex-shrink:0;";
const dot = document.createElement("div");
dot.style.cssText = `
width:36px;height:36px;border-radius:50%;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
font-size:17px;
background:${ms.done ? "var(--accent)" : "var(--surface)"};
border:2px solid ${ms.done ? "var(--accent)" : "var(--border)"};
`;
dot.textContent = ms.done ? ms.icon : "○";
const connLine = document.createElement("div");
if (!isLast) {
connLine.style.cssText = `
flex:1;width:2px;min-height:20px;margin:4px 0;
background:${ms.done ? "var(--accent)" : "var(--border)"};
opacity:${ms.done ? "1" : "0.4"};
`;
}
lineCol.appendChild(dot);
lineCol.appendChild(connLine);
// Правая колонка: контент
const content = document.createElement("div");
content.style.cssText = `
padding:4px 0 ${isLast ? "0" : "20px"};
flex:1;min-width:0;
`;
content.innerHTML = `
${escHtml(ms.title)}
${ms.ts ? `
${escHtml(fmtDate(ms.ts) || "")}
` : (!ms.done ? `
ожидается
` : "")}
${ms.detail ? `
${escHtml(ms.detail)}
` : ""}
`;
row.appendChild(lineCol);
row.appendChild(content);
tlWrap.appendChild(row);
});
screen.appendChild(tlWrap);
// Кнопка «Назад к карточке сборки»
const backBtn = document.createElement("div");
backBtn.style.cssText = "margin:0 16px;";
backBtn.innerHTML = `
`;
backBtn.querySelector("button").addEventListener("click", () => {
haptic && haptic("impact");
history.back();
});
screen.appendChild(backBtn);
})
.catch(e => {
screen.innerHTML = `Ошибка: ${escHtml(e.message)}
`;
});
}
return { mount };
})();