/* ============================================================
История заказов — #/c/orders
Единый таймлайн: подборы + сборки клиента.
============================================================ */
const OrdersScreen = (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", year: "numeric" });
} 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,
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 PROPOSAL_LABELS = {
brief: { icon: "📝", text: "Анкета заполнена", color: "#8e8e8e" },
draft: { icon: "⏳", text: "Готовим подбор", color: "#F39C12" },
sent: { icon: "📨", text: "Подбор отправлен", color: "#2980B9" },
reviewed: { icon: "✅", text: "Вы просмотрели", color: "#27AE60" },
done: { icon: "🎉", text: "Завершён", color: "#16a085" },
archived: { icon: "📦", text: "В архиве", color: "#bdc3c7" },
};
/* ---- Статусы сборок ------------------------------------------ */
const ASSEMBLY_LABELS = {
created: { icon: "🆕", text: "Создана", color: "#8e8e8e" },
scheduled: { icon: "📅", text: "Запланирована", color: "#2980B9" },
in_progress: { icon: "🔨", text: "В процессе", color: "#F39C12" },
done: { icon: "✅", text: "Завершена", color: "#27AE60" },
cancelled: { icon: "❌", text: "Отменена", color: "#C0392B" },
};
/* ---- Один элемент таймлайна ---------------------------------- */
function renderItem(item) {
const statusStyle = `color:${item.statusColor};font-size:12px;font-weight:500;`;
const hasLink = !!item.href;
const dateStr = fmtDate(item.date);
return `
${escHtml(item.typeLabel)}
${item.title ? `
${escHtml(item.title)}
` : ""}
${escHtml(item.statusText)}
${dateStr ? `
${escHtml(dateStr)}
` : ""}
${item.subtitle ? `
${escHtml(item.subtitle)}
` : ""}
${item.calUrl ? `
📅 Посмотреть в календаре` : ""}
`;
}
/* ---- Маппинг подбора в элемент таймлайна -------------------- */
function proposalToItem(p) {
const sl = PROPOSAL_LABELS[p.status] || { icon: "📋", text: p.status, color: "#8e8e8e" };
const partsArr = [];
if (p.n_categories) partsArr.push(`${p.n_categories} категор.`);
if (p.n_variants) partsArr.push(`${p.n_variants} вар.`);
const subtitle = partsArr.join(" · ");
const date = p.sent_at || p.reviewed_at || p.created_at;
return {
type: "proposal",
date,
icon: sl.icon,
typeLabel: "Подбор кухни",
title: null,
subtitle: subtitle || null,
statusText: sl.text,
statusColor: sl.color,
href: p.id ? `#/c/proposal/${encodeURIComponent(p.id)}` : null,
calUrl: null,
};
}
/* ---- Маппинг сборки в элемент таймлайна --------------------- */
function assemblyToItem(a) {
const sl = ASSEMBLY_LABELS[a.status] || { icon: "🔧", text: a.status, color: "#8e8e8e" };
const date = a.scheduled_at || a.ts;
return {
type: "assembly",
date,
icon: sl.icon,
typeLabel: "Сборка кухни",
title: a.address || null,
subtitle: a.scope_of_work || null,
statusText: sl.text,
statusColor: sl.color,
href: a.id ? `#/c/assembly/${encodeURIComponent(a.id)}` : null,
calUrl: a.gcal_event_url || null,
};
}
/* ---- Пустое состояние --------------------------------------- */
function renderEmpty() {
return `
📋
Заказов пока нет
Когда менеджер создаст подбор или запланирует сборку — всё появится здесь.
`;
}
/* ── mount ─────────────────────────────────────────────────── */
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";
screen.innerHTML = ``;
container.appendChild(screen);
try {
const [proposalsData, assembliesData] = await Promise.all([
_api("proposal_list").catch(() => ({ proposals: [] })),
_api("assembly_list").catch(() => ({ assemblies: [] })),
]);
const proposals = (proposalsData.proposals || []).map(proposalToItem);
const assemblies = (assembliesData.assemblies || []).map(assemblyToItem);
const all = [...proposals, ...assemblies].sort((a, b) => {
const da = a.date || "";
const db = b.date || "";
return db.localeCompare(da);
});
screen.innerHTML = "";
if (!all.length) {
screen.innerHTML = renderEmpty();
screen.querySelectorAll("[data-href]").forEach(el => {
el.addEventListener("click", () => {
haptic && haptic("impact");
location.hash = el.dataset.href;
});
});
return;
}
// Счётчик
const countDiv = document.createElement("div");
countDiv.style.cssText = "padding:12px 16px 4px;font-size:13px;color:var(--muted);";
countDiv.textContent = `Всего: ${all.length} ${_declinate(all.length, ["запись", "записи", "записей"])}`;
screen.appendChild(countDiv);
// Таймлайн
const timeline = document.createElement("div");
timeline.className = "orders-timeline";
timeline.innerHTML = all.map(renderItem).join("");
screen.appendChild(timeline);
const spacer = document.createElement("div");
spacer.style.height = "32px";
screen.appendChild(spacer);
// Клики
screen.querySelectorAll("[data-href]").forEach(el => {
el.addEventListener("click", () => {
haptic && haptic("impact");
location.hash = el.dataset.href;
});
});
} catch (e) {
screen.innerHTML = `Ошибка: ${escHtml(e.message)}
`;
}
}
function _declinate(n, forms) {
const abs = Math.abs(n) % 100;
const r = abs % 10;
if (abs > 10 && abs < 20) return forms[2];
if (r > 1 && r < 5) return forms[1];
if (r === 1) return forms[0];
return forms[2];
}
return { mount };
})();