diff --git a/memory/business_rules.md b/memory/business_rules.md
new file mode 100644
index 0000000..3870464
--- /dev/null
+++ b/memory/business_rules.md
@@ -0,0 +1,25 @@
+# Бизнес-правила ЗОВ / ИП Васильев Р.Г.
+
+## Стоимость замера
+
+| Условие | Цена |
+|---|---|
+| Стандартный выезд (в черте КАД СПб) | **2 500 ₽** |
+| За каждый км за пределами КАД СПб | **+ 40 ₽/км** |
+
+> Дистанция считается от КАД до адреса клиента (в одну сторону).
+
+## Сборка
+
+- Стоимость сборки согласовывается отдельно по каждому заказу.
+- Срок: фиксируется в поле `scheduled_at` сборки.
+
+## Активный период клиента
+
+- `ACTIVE_PERIOD_DAYS` = 90 дней
+- `GRACE_PERIOD_DAYS` = 14 дней
+
+## Источники самозамера
+
+- `source=self_measure` в листе Measurements → клиент сам заполнил через мастер #/c/selfmeasure
+- `source=manager` → менеджер ввёл вручную
diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js
index 07668ed..847cbfa 100644
--- a/miniapp/assets/app.js
+++ b/miniapp/assets/app.js
@@ -1,4 +1,4 @@
-// ЗОВ MiniApp — главный скрипт. v20260518k
+// ЗОВ MiniApp — главный скрипт. v20260518l
// На входе: подписанный initData от Telegram.
// Ходим на backend → получаем профиль (роль, статус) → рендерим меню.
@@ -1719,6 +1719,9 @@ function routeByHash() {
} else {
app.innerHTML = `
+
+
diff --git a/miniapp/assets/orders.js b/miniapp/assets/orders.js
new file mode 100644
index 0000000..1b4420c
--- /dev/null
+++ b/miniapp/assets/orders.js
@@ -0,0 +1,233 @@
+/* ============================================================
+ История заказов — #/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: 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 };
+})();
diff --git a/miniapp/assets/styles.css b/miniapp/assets/styles.css
index 3cccd22..f9d59b4 100644
--- a/miniapp/assets/styles.css
+++ b/miniapp/assets/styles.css
@@ -1478,3 +1478,94 @@ html[data-variant="d"] .section-head .label {
text-transform: uppercase;
color: var(--muted);
}
+
+/* ── История заказов — таймлайн ─────────────────────────────── */
+.orders-timeline {
+ padding: 8px 0 0;
+}
+.orders-item {
+ display: flex;
+ gap: 0;
+ padding: 0 16px 0 12px;
+}
+.orders-item--link {
+ cursor: pointer;
+}
+.orders-item--link:active {
+ opacity: 0.7;
+}
+.orders-item-left {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 36px;
+ flex-shrink: 0;
+}
+.orders-item-icon {
+ font-size: 20px;
+ line-height: 1;
+ padding: 4px 0;
+ z-index: 1;
+ background: transparent;
+}
+.orders-item-line {
+ width: 2px;
+ flex: 1;
+ min-height: 16px;
+ background: var(--border, #e8e8e8);
+ margin: 2px 0;
+}
+.orders-item:last-child .orders-item-line {
+ display: none;
+}
+.orders-item-body {
+ flex: 1;
+ padding: 4px 0 20px 4px;
+}
+.orders-item-type {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--muted);
+ margin-bottom: 2px;
+}
+.orders-item-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--ink);
+ margin: 2px 0;
+}
+.orders-item-subtitle {
+ font-size: 12px;
+ color: var(--muted);
+ margin-top: 4px;
+ line-height: 1.4;
+}
+
+/* cabinet / orders shared blocks */
+.block {
+ background: var(--surface, #fff);
+ border: 1px solid var(--border, #e8e8e8);
+ border-radius: 12px;
+ padding: 14px 14px 10px;
+}
+.block-head {
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ color: var(--muted);
+ margin-bottom: 8px;
+}
+.assembly-card {
+ border: 1px solid var(--border, #e8e8e8);
+ border-radius: 10px;
+ padding: 10px 12px;
+ background: var(--surface, #fff);
+}
+.btn-sm {
+ font-size: 13px;
+ padding: 7px 14px;
+}
+/* ─────────────────────────────────────────────────────────────── */
diff --git a/miniapp/index.html b/miniapp/index.html
index 8768361..f17c5aa 100644
--- a/miniapp/index.html
+++ b/miniapp/index.html
@@ -12,7 +12,7 @@
-
+
@@ -49,6 +49,7 @@
-
+
+