mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 15:44:47 +00:00
feat: детальная карточка сборки + цена замера везде
- assembly_detail.js: экран #/c/assembly/:id — статус, адрес, фото, подпись, gcal - orders.js: сборки кликабельны → #/c/assembly/:id - app.js: маршрут #/c/assembly/ - selfmeasure.js: цена 2500₽ + 40₽/км за КАД на шаге 1 и шаге 5 - cabinet.js: цена под кнопкой самозамера Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b75f24e4d7
commit
042dc1a5d3
@ -1722,6 +1722,10 @@ function routeByHash() {
|
|||||||
} else if (location.hash === "#/c/orders") {
|
} else if (location.hash === "#/c/orders") {
|
||||||
if (typeof OrdersScreen !== "undefined") OrdersScreen.mount(app);
|
if (typeof OrdersScreen !== "undefined") OrdersScreen.mount(app);
|
||||||
else init();
|
else init();
|
||||||
|
} else if (location.hash.startsWith("#/c/assembly/")) {
|
||||||
|
const assemblyId = decodeURIComponent(location.hash.replace("#/c/assembly/", ""));
|
||||||
|
if (typeof AssemblyDetailScreen !== "undefined") AssemblyDetailScreen.mount(app, assemblyId);
|
||||||
|
else init();
|
||||||
} else if (location.hash === "#/c/selfmeasure") {
|
} else if (location.hash === "#/c/selfmeasure") {
|
||||||
if (typeof SelfMeasureScreen !== "undefined") SelfMeasureScreen.mount(app);
|
if (typeof SelfMeasureScreen !== "undefined") SelfMeasureScreen.mount(app);
|
||||||
else init();
|
else init();
|
||||||
|
|||||||
174
miniapp/assets/assembly_detail.js
Normal file
174
miniapp/assets/assembly_detail.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/* ============================================================
|
||||||
|
Детальная карточка сборки — #/c/assembly/:id
|
||||||
|
Доступна клиенту, менеджеру, мастеру.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
const AssemblyDetailScreen = (function () {
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s == null ? "" : s)
|
||||||
|
.replace(/&/g, "&").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",
|
||||||
|
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 = {
|
||||||
|
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 row(label, value, opts = {}) {
|
||||||
|
if (!value) return "";
|
||||||
|
return `
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;
|
||||||
|
padding:10px 0;border-bottom:1px solid var(--border);">
|
||||||
|
<div style="font-size:12px;color:var(--muted);flex-shrink:0;margin-right:12px;">${escHtml(label)}</div>
|
||||||
|
<div style="font-size:13px;font-weight:500;color:${opts.color || "var(--ink)"};text-align:right;">${opts.html ? value : escHtml(value)}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mount(container, assemblyId) {
|
||||||
|
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 = `
|
||||||
|
<button class="podbor-back" aria-label="Назад">${(window.ICONS || {}).arrow_left || "‹"}</button>
|
||||||
|
<div class="podbor-title">Сборка кухни</div>
|
||||||
|
<div style="width:36px"></div>
|
||||||
|
`;
|
||||||
|
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 = `<div class="loader-inline"><div class="spinner"></div></div>`;
|
||||||
|
container.appendChild(screen);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await _api("assembly_detail", { assembly_id: assemblyId });
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
screen.innerHTML = `<div class="error" style="margin:16px;">${escHtml(data.error)}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sl = STATUS[data.status] || { icon: "🔧", text: data.status, color: "#8e8e8e" };
|
||||||
|
|
||||||
|
// Статус-баннер
|
||||||
|
const statusBanner = `
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;padding:16px;
|
||||||
|
background:var(--surface);border-bottom:1px solid var(--border);">
|
||||||
|
<div style="font-size:32px;">${sl.icon}</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:16px;font-weight:700;color:${sl.color};">${escHtml(sl.text)}</div>
|
||||||
|
<div style="font-size:12px;color:var(--muted);margin-top:2px;">ID: ${escHtml(data.id)}</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Основные данные
|
||||||
|
const mainBlock = `
|
||||||
|
<div style="margin:12px 16px 0;border:1px solid var(--border);border-radius:12px;
|
||||||
|
padding:0 12px;background:var(--surface);">
|
||||||
|
${row("Адрес", data.address)}
|
||||||
|
${row("Объём работ", data.scope_of_work)}
|
||||||
|
${row("Дата сборки", fmtDate(data.scheduled_at))}
|
||||||
|
${row("Начало", fmtDate(data.started_at))}
|
||||||
|
${row("Завершение", fmtDate(data.completed_at))}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Заметка менеджера
|
||||||
|
const noteBlock = data.manager_note ? `
|
||||||
|
<div style="margin:12px 16px 0;padding:12px;background:var(--surface-2,var(--surface));
|
||||||
|
border:1px solid var(--border);border-radius:12px;">
|
||||||
|
<div style="font-size:11px;font-weight:700;text-transform:uppercase;
|
||||||
|
letter-spacing:.06em;color:var(--muted);margin-bottom:6px;">Заметка</div>
|
||||||
|
<div style="font-size:13px;color:var(--ink);line-height:1.5;">${escHtml(data.manager_note)}</div>
|
||||||
|
</div>` : "";
|
||||||
|
|
||||||
|
// Фото результата
|
||||||
|
const photosAfter = (data.photos_after || []).filter(Boolean);
|
||||||
|
const photosBlock = photosAfter.length ? `
|
||||||
|
<div style="margin:12px 16px 0;">
|
||||||
|
<div style="font-size:11px;font-weight:700;text-transform:uppercase;
|
||||||
|
letter-spacing:.06em;color:var(--muted);margin-bottom:8px;">Фото результата</div>
|
||||||
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||||
|
${photosAfter.map(u => `
|
||||||
|
<a href="${escHtml(u)}" target="_blank">
|
||||||
|
<img src="${escHtml(u)}" alt="фото"
|
||||||
|
style="width:80px;height:80px;object-fit:cover;border-radius:8px;
|
||||||
|
border:1px solid var(--border);">
|
||||||
|
</a>`).join("")}
|
||||||
|
</div>
|
||||||
|
</div>` : "";
|
||||||
|
|
||||||
|
// Подпись
|
||||||
|
const signBlock = data.signed_by_name ? `
|
||||||
|
<div style="margin:12px 16px 0;padding:10px 12px;background:var(--surface);
|
||||||
|
border:1px solid var(--border);border-radius:12px;
|
||||||
|
display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:12px;color:var(--muted);">Принято клиентом</div>
|
||||||
|
<div style="font-size:13px;font-weight:600;color:var(--ink);">${escHtml(data.signed_by_name)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:var(--muted);">${escHtml(fmtDate(data.signed_at) || "")}</div>
|
||||||
|
</div>` : "";
|
||||||
|
|
||||||
|
// Кнопка Google Calendar
|
||||||
|
const calBtn = data.gcal_event_url ? `
|
||||||
|
<div style="margin:12px 16px 0;">
|
||||||
|
<a href="${escHtml(data.gcal_event_url)}" target="_blank"
|
||||||
|
style="display:flex;align-items:center;justify-content:center;gap:8px;
|
||||||
|
padding:12px;border:1px solid var(--border);border-radius:10px;
|
||||||
|
background:var(--surface);text-decoration:none;
|
||||||
|
font-size:13px;font-weight:600;color:var(--accent);">
|
||||||
|
📅 Посмотреть в Google Календаре
|
||||||
|
</a>
|
||||||
|
</div>` : "";
|
||||||
|
|
||||||
|
screen.innerHTML = statusBanner + mainBlock + noteBlock + photosBlock + signBlock + calBtn +
|
||||||
|
`<div style="height:32px;"></div>`;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
screen.innerHTML = `<div class="error" style="margin:16px;">Ошибка: ${escHtml(e.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mount };
|
||||||
|
})();
|
||||||
@ -187,6 +187,10 @@ const CabinetScreen = (function () {
|
|||||||
<div class="block" style="margin:12px 16px 0;display:flex;flex-direction:column;gap:8px;">
|
<div class="block" style="margin:12px 16px 0;display:flex;flex-direction:column;gap:8px;">
|
||||||
<button class="btn-secondary" data-href="#/c/orders" style="width:100%;">📋 История заказов</button>
|
<button class="btn-secondary" data-href="#/c/orders" style="width:100%;">📋 История заказов</button>
|
||||||
<button class="btn-secondary" data-href="#/c/selfmeasure" style="width:100%;">📐 Самозамер кухни</button>
|
<button class="btn-secondary" data-href="#/c/selfmeasure" style="width:100%;">📐 Самозамер кухни</button>
|
||||||
|
<div style="font-size:12px;color:var(--muted);text-align:center;line-height:1.4;">
|
||||||
|
Выезд специалиста: <strong style="color:var(--ink);">2 500 ₽</strong>
|
||||||
|
· за КАД СПб +40 ₽/км
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="height:32px;"></div>
|
<div style="height:32px;"></div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -119,7 +119,7 @@ const OrdersScreen = (function () {
|
|||||||
subtitle: a.scope_of_work || null,
|
subtitle: a.scope_of_work || null,
|
||||||
statusText: sl.text,
|
statusText: sl.text,
|
||||||
statusColor: sl.color,
|
statusColor: sl.color,
|
||||||
href: null,
|
href: a.id ? `#/c/assembly/${encodeURIComponent(a.id)}` : null,
|
||||||
calUrl: a.gcal_event_url || null,
|
calUrl: a.gcal_event_url || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,11 +134,23 @@ const SelfMeasureScreen = (function () {
|
|||||||
return ["А"];
|
return ["А"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Price info block ---- */
|
||||||
|
const PRICE_INFO_HTML = `
|
||||||
|
<div style="margin:12px 16px 0;padding:12px 14px;border-radius:10px;
|
||||||
|
background:#FFF8E7;border:1px solid #F5A623;">
|
||||||
|
<div style="font-size:12px;font-weight:700;color:#B7770A;margin-bottom:4px;">💰 Стоимость выезда специалиста</div>
|
||||||
|
<div style="font-size:13px;color:#5C4800;line-height:1.5;">
|
||||||
|
В черте КАД Санкт-Петербурга — <strong>2 500 ₽</strong><br>
|
||||||
|
За пределами КАД — <strong>2 500 ₽ + 40 ₽/км</strong> от кольцевой до адреса
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
/* ---- Step 1: Kitchen type ---- */
|
/* ---- Step 1: Kitchen type ---- */
|
||||||
function renderStep1(state) {
|
function renderStep1(state) {
|
||||||
const wrap = document.createElement("div");
|
const wrap = document.createElement("div");
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<div class="block" style="margin:16px 16px 0;">
|
${PRICE_INFO_HTML}
|
||||||
|
<div class="block" style="margin:12px 16px 0;">
|
||||||
<div class="block-head">Выберите тип кухни</div>
|
<div class="block-head">Выберите тип кухни</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -526,7 +538,8 @@ const SelfMeasureScreen = (function () {
|
|||||||
const wrap = document.createElement("div");
|
const wrap = document.createElement("div");
|
||||||
|
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<div class="block" style="margin:16px 16px 0;">
|
${PRICE_INFO_HTML}
|
||||||
|
<div class="block" style="margin:12px 16px 0;">
|
||||||
<div class="block-head">Контактные данные</div>
|
<div class="block-head">Контактные данные</div>
|
||||||
<div style="display:flex;flex-direction:column;gap:10px;margin-top:4px;">
|
<div style="display:flex;flex-direction:column;gap:10px;margin-top:4px;">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -50,6 +50,7 @@
|
|||||||
<script src="assets/cabinet.js?v=20260518j"></script>
|
<script src="assets/cabinet.js?v=20260518j"></script>
|
||||||
<script src="assets/selfmeasure.js?v=20260518k"></script>
|
<script src="assets/selfmeasure.js?v=20260518k"></script>
|
||||||
<script src="assets/orders.js?v=20260518l"></script>
|
<script src="assets/orders.js?v=20260518l"></script>
|
||||||
|
<script src="assets/assembly_detail.js?v=20260518l"></script>
|
||||||
<script src="assets/app.js?v=20260518l"></script>
|
<script src="assets/app.js?v=20260518l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user