From 366625be66e769587f30af01e029e4400e3e2ef4 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Wed, 13 May 2026 18:12:18 +0300 Subject: [PATCH] =?UTF-8?q?flow:=20=D1=83=D0=BF=D1=80=D0=BE=D1=89=D1=91?= =?UTF-8?q?=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B0?= =?UTF-8?q?=20+=203=20=D1=87=D1=91=D1=82=D0=BA=D0=B8=D0=B5=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B4=D0=B8=D0=B8=20=D1=83=20=D0=B7=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D1=89=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По цепочке менеджер→замерщик→замер: Менеджер «Заказать замер»: - ФИО, телефон, адрес, кому назначить - Одно поле «Примечание» (рекомендации по дате + особенности) Убраны radio-buttons specific/this_week/next_week — слишком сложно. Точную дату всё равно согласует замерщик с клиентом. Замерщик в карточке заявки — 3 чёткие стадии: 1. ЕСЛИ статус requested (дата не назначена): - Блок «📞 Согласовать дату с клиентом» - Подсказка «Позвоните клиенту и зафиксируйте» - datetime-local + кнопка «Назначить» 2. ЕСЛИ статус scheduled (дата уже есть): - Блок «📅 Замер назначен» крупно (Newsreader 22pt italic) - Кнопка «Изменить дату» — разворачивает скрытую форму - ОСНОВНАЯ кнопка «📐 Начать замер» (большая, primary, 16pt) - До «Начать замер» чек-листа не видно Чек-лист (📋 в шапке) теперь живёт ТОЛЬКО в мастере замера (когда нажали «Начать замер»). До этого момента не отвлекает. Backend: DM при создании заявки шлёт только примечание (без расшифровки preferred_type). Cache bust v=20260513z. --- backend-py/app/main.py | 9 +- miniapp/assets/app.js | 182 ++++++++++++++++++++++---------------- miniapp/assets/podbor.css | 19 ++++ miniapp/assets/request.js | 83 ++--------------- miniapp/index.html | 22 ++--- 5 files changed, 147 insertions(+), 168 deletions(-) diff --git a/backend-py/app/main.py b/backend-py/app/main.py index f5037b4..9df6a31 100644 --- a/backend-py/app/main.py +++ b/backend-py/app/main.py @@ -1196,18 +1196,15 @@ def _handle_measurement_request(body: dict[str, Any]) -> dict[str, Any]: # Уведомление назначенному замерщику if assigned_to: - timing_line = _format_preferred_human( - preferred_type, preferred_date, preferred_time_of_day, preferred_note - ) + note_line = f"\nПримечание: {preferred_note}" if preferred_note else "" tg.send_message( int(assigned_to), f"📐 Новая заявка на замер\n\n" f"Клиент: {client_name}\n" f"Телефон: {client_phone}\n" f"Адрес: {address or '—'}\n" - f"Когда: {timing_line}\n" - f"От менеджера: {user.get('full_name') or tg_id}\n\n" - f"{notes if notes else ''}\n" + f"От менеджера: {user.get('full_name') or tg_id}" + f"{note_line}\n\n" f"Откройте кабинет — согласуйте точную дату с клиентом." ) diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js index 000fcdf..2db4beb 100644 --- a/miniapp/assets/app.js +++ b/miniapp/assets/app.js @@ -541,6 +541,9 @@ function renderInboxItem(m) { } function formatPreferredHuman(m) { + // Теперь приоритет — текст из preferred_note (свободная форма). + // Старые записи с preferred_type/date/time_of_day выводятся как fallback. + if (m.preferred_note) return m.preferred_note; const todMap = { morning: "утром", day: "днём", evening: "вечером" }; const t = m.preferred_type || "tbd"; const parts = []; @@ -562,9 +565,7 @@ function formatPreferredHuman(m) { } else { parts.push("согласовать с клиентом"); } - let s = parts.join(" "); - if (m.preferred_note) s += " · " + m.preferred_note; - return s; + return parts.join(" "); } function formatDateHuman(iso) { @@ -640,99 +641,126 @@ async function renderInboxDetail(measurementId) { `)); - // Приблизительная дата от менеджера (если точной ещё нет — это подсказка) - if (!m.scheduled_at && (m.preferred_type || m.preferred_note)) { - const prefText = formatPreferredHuman(m); + // Примечание от менеджера (рекомендации по дате, особенности доступа) + if (m.preferred_note) { app.appendChild(el(`
-
⏰ Когда удобно клиенту (от менеджера)
-
${escHtml(prefText)}
-
- Позвоните клиенту и согласуйте точную дату — она появится ниже. -
+
📝 Примечание менеджера
+
${escHtml(m.preferred_note).replace(/\n/g, "
")}
`)); } - // Заметки от менеджера - if (m.notes) { - app.appendChild(el(` -
-
Заметки от менеджера
-
${escHtml(m.notes).replace(/\n/g, "
")}
-
- `)); - } - - // Блок логистики — заполняется замерщиком/сборщиком на месте + // Блок логистики (подъезд, GPS, парковка) — заполняется на месте app.appendChild(renderLogisticsBlock(m)); - // Блок «назначить дату» (если ещё requested) или «изменить дату» (если scheduled) - const isScheduled = m.status === "scheduled"; - const schedSection = el(` -
-
${isScheduled ? "Дата замера" : "Назначить дату"}
-
+ // Блок даты замера — две версии в зависимости от статуса + const isScheduled = m.status === "scheduled" && m.scheduled_at; + if (isScheduled) { + // Дата назначена — показываем её крупно + кнопка «Изменить» + const dateSection = el(` +
+
📅 Замер назначен
+
${escHtml(formatDateHuman(m.scheduled_at))}
+
+ +
+ +
+ `); + app.appendChild(dateSection); + dateSection.querySelector("#changeDate").addEventListener("click", () => { + dateSection.querySelector("#changeDateForm").style.display = ""; + dateSection.querySelector("#changeDate").style.display = "none"; + }); + dateSection.querySelector("#cancelChange").addEventListener("click", () => { + dateSection.querySelector("#changeDateForm").style.display = "none"; + dateSection.querySelector("#changeDate").style.display = ""; + }); + dateSection.querySelector("#saveSched").addEventListener("click", () => saveScheduleDate(measurementId, dateSection)); + + // ОСНОВНАЯ кнопка — начать замер (открывает мастер с чек-листом) + const startSection = el(` +
+ +
+
+ Чек-лист, фото и заметки откроются после нажатия. +
+ `); + app.appendChild(startSection); + startSection.querySelector("#startMeasure").addEventListener("click", () => { + haptic && haptic("impact"); + location.hash = `#/measure?id=${measurementId}`; + }); + } else { + // Дата не назначена — основной шаг: согласовать и назначить + const dateSection = el(` +
+
📞 Согласовать дату с клиентом
+
+ Позвоните клиенту, договоритесь о точной дате и времени, затем зафиксируйте здесь. +
- +
-
-
- `); - app.appendChild(schedSection); + + `); + app.appendChild(dateSection); + dateSection.querySelector("#saveSched").addEventListener("click", () => saveScheduleDate(measurementId, dateSection)); + } +} - schedSection.querySelector("#saveSched").addEventListener("click", async () => { - const input = schedSection.querySelector("#schedInput"); - const errorEl = schedSection.querySelector("#schedError"); - errorEl.textContent = ""; - const val = input.value; - if (!val) { - errorEl.textContent = "Укажите дату и время"; +async function saveScheduleDate(measurementId, section) { + const input = section.querySelector("#schedInput"); + const errorEl = section.querySelector("#schedError"); + if (errorEl) errorEl.textContent = ""; + const val = input.value; + if (!val) { + if (errorEl) errorEl.textContent = "Укажите дату и время"; + return; + } + const iso = new Date(val).toISOString(); + try { + const res = await fetch(`${BACKEND_URL}/api/measurement_schedule`, { + method: "POST", + body: JSON.stringify({ + initData: tg?.initData || "", + initDataUnsafe: tg?.initDataUnsafe || null, + measurement_id: measurementId, + scheduled_at: iso, + }), + }); + const data = await res.json(); + if (data.error) { + if (errorEl) errorEl.textContent = "Ошибка: " + data.error; return; } - const iso = new Date(val).toISOString(); - try { - const res = await fetch(`${BACKEND_URL}/api/measurement_schedule`, { - method: "POST", - body: JSON.stringify({ - initData: tg?.initData || "", - measurement_id: measurementId, - scheduled_at: iso, - }), - }); - const data = await res.json(); - if (data.error) { - errorEl.textContent = "Ошибка: " + data.error; - return; - } - haptic && haptic("success"); - tg?.showAlert?.("Дата назначена — менеджер уведомлён."); - renderInboxDetail(measurementId); // перерисовать - } catch (e) { - errorEl.textContent = "Сеть: " + e.message; - } - }); - - // Кнопка «Сделать замер» (только если назначено или прямо сейчас) - const measureBtn = el(` -
- -
- `); - measureBtn.querySelector("#goMeasure").addEventListener("click", () => { - haptic && haptic("impact"); - // Передаём measurement_id чтобы wizard работал в update-mode - location.hash = `#/measure?id=${measurementId}`; - }); - app.appendChild(measureBtn); + haptic && haptic("success"); + tg?.showAlert?.("Дата сохранена — менеджер уведомлён."); + renderInboxDetail(measurementId); // перерисовать с новым статусом + } catch (e) { + if (errorEl) errorEl.textContent = "Сеть: " + e.message; + } } function renderLogisticsBlock(m) { diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index fbbbab7..2e9c941 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -2183,6 +2183,25 @@ border-left: 3px solid var(--walnut, #6B4A2B); } +/* Блок «дата назначена» */ +.date-set-block { + background: linear-gradient(180deg, rgba(0, 62, 126, 0.04), transparent); + border-left: 3px solid var(--accent-1, #003E7E); +} +.date-set-block .date-set-value { + font-family: var(--font-display, "Newsreader", serif); + font-style: italic; + font-size: 22px; + color: var(--accent-1, #003E7E); + padding: 8px 4px 12px; + font-weight: 500; +} +.date-set-block .date-set-form { + border-top: 1px dashed rgba(0, 62, 126, 0.2); + padding-top: 12px; + margin-top: 12px; +} + /* ===== Логистика (подъезд, GPS, парковка) ===== */ .logistics-block .block-head { display: flex; diff --git a/miniapp/assets/request.js b/miniapp/assets/request.js index 7cf07a8..dfb06a6 100644 --- a/miniapp/assets/request.js +++ b/miniapp/assets/request.js @@ -9,11 +9,8 @@ const MeasurementRequest = (function () { client_phone: "", address: "", assigned_to_tg_id: "", - notes: "", - // Приблизительная дата визита - preferred_type: "tbd", // specific | this_week | next_week | tbd - preferred_date: "", - preferred_time_of_day: "", // morning | day | evening | "" + // Одно поле «Примечание» — рекомендации по дате замера + особенности. + // Замерщик увидит это в карточке заявки и согласует точное время с клиентом. preferred_note: "", }; let measurers = []; @@ -24,8 +21,8 @@ const MeasurementRequest = (function () { const oldNav = document.getElementById("bottom-nav"); if (oldNav) oldNav.remove(); state = { - client_name: "", client_phone: "", address: "", assigned_to_tg_id: "", notes: "", - preferred_type: "tbd", preferred_date: "", preferred_time_of_day: "", preferred_note: "", + client_name: "", client_phone: "", address: "", assigned_to_tg_id: "", + preferred_note: "", }; render(); loadMeasurers(); @@ -75,53 +72,11 @@ const MeasurementRequest = (function () { -
⏰ Когда удобно клиенту
-
- - - - -
- - -
-
- -
-
@@ -147,23 +102,6 @@ const MeasurementRequest = (function () { state[e.target.dataset.bind] = e.target.value; }); }); - // Радио-кнопки + поля приблизительной даты - node.querySelectorAll("[data-pref]").forEach(inp => { - const key = inp.dataset.pref; - const mapKey = "preferred_" + key; - inp.addEventListener("change", e => { - const val = e.target.type === "radio" ? e.target.value : e.target.value; - state[mapKey] = val; - if (key === "type") togglePrefSpecific(node); - }); - }); - togglePrefSpecific(node); - } - - function togglePrefSpecific(node) { - const box = node.querySelector("#prefSpecificBox"); - if (!box) return; - box.style.display = state.preferred_type === "specific" ? "" : "none"; } async function loadMeasurers() { @@ -224,12 +162,9 @@ const MeasurementRequest = (function () { client_phone: phone, address: state.address || "", assigned_to_tg_id: state.assigned_to_tg_id || "", - notes: state.notes || "", - // Приблизительная дата визита - preferred_type: state.preferred_type || "tbd", - preferred_date: state.preferred_date || "", - preferred_time_of_day: state.preferred_time_of_day || "", + // Примечание (рекомендации по дате + особенности) — единое поле preferred_note: state.preferred_note || "", + preferred_type: "tbd", }), }); const data = await res.json(); diff --git a/miniapp/index.html b/miniapp/index.html index a64e686..edde1cf 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -12,8 +12,8 @@ - - + + @@ -31,14 +31,14 @@
Сделано с душой!
- - - - - - - - - + + + + + + + + +