diff --git a/backend-py/app/main.py b/backend-py/app/main.py index 6351077..5b6ceba 100644 --- a/backend-py/app/main.py +++ b/backend-py/app/main.py @@ -606,6 +606,12 @@ def _measurement_columns() -> list[str]: "address", "client_name", "client_phone", # Поля Commit C (структура замера по чек-листу) "zamer_no", "zamer_date", "floor_base", "photos_meta", + # Поля для приблизительной даты от менеджера (Commit C2) + # preferred_type: specific | this_week | next_week | tbd + # preferred_date: ISO date если specific + # preferred_time_of_day: morning | day | evening + # preferred_note: «после звонка», «не раньше вторника», ... + "preferred_type", "preferred_date", "preferred_time_of_day", "preferred_note", ] @@ -637,6 +643,7 @@ def _row_for_measurement(measurement_id: str, ts: str, **fields) -> list[str]: "assigned_to_tg_id": "", "requested_by_tg_id": "", "scheduled_at": "", "address": "", "client_name": "", "client_phone": "", "zamer_no": "", "zamer_date": "", "floor_base": "", "photos_meta": "", + "preferred_type": "", "preferred_date": "", "preferred_time_of_day": "", "preferred_note": "", } base.update(fields) return [str(base.get(c, "")) for c in cols] @@ -1120,6 +1127,16 @@ def _handle_measurement_request(body: dict[str, Any]) -> dict[str, Any]: assigned_to = str(body.get("assigned_to_tg_id") or "").strip() notes = (body.get("notes") or "").strip() + # Приблизительная дата визита (Commit C2) + preferred_type = (body.get("preferred_type") or "tbd").strip() + preferred_date = (body.get("preferred_date") or "").strip() + preferred_time_of_day = (body.get("preferred_time_of_day") or "").strip() + preferred_note = (body.get("preferred_note") or "").strip() + if preferred_type not in ("specific", "this_week", "next_week", "tbd"): + preferred_type = "tbd" + if preferred_time_of_day not in ("morning", "day", "evening", ""): + preferred_time_of_day = "" + if not client_name or not client_phone: return {"error": "missing_client_info", "hint": "client_name and client_phone are required"} @@ -1144,19 +1161,27 @@ def _handle_measurement_request(body: dict[str, Any]) -> dict[str, Any]: client_name=client_name, client_phone=client_phone, notes=notes, + preferred_type=preferred_type, + preferred_date=preferred_date, + preferred_time_of_day=preferred_time_of_day, + preferred_note=preferred_note, )) # Уведомление назначенному замерщику if assigned_to: + timing_line = _format_preferred_human( + preferred_type, preferred_date, preferred_time_of_day, preferred_note + ) 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"Откройте кабинет — назначьте дату." + f"Откройте кабинет — согласуйте точную дату с клиентом." ) sheets.log_event("measurement_requested", tg_id, { @@ -1206,6 +1231,10 @@ def _handle_measurement_inbox(body: dict[str, Any]) -> dict[str, Any]: "notes": row.get("notes", ""), "manager_tg_id": row.get("manager_tg_id", ""), "requested_by_tg_id": row.get("requested_by_tg_id", ""), + "preferred_type": row.get("preferred_type", ""), + "preferred_date": row.get("preferred_date", ""), + "preferred_time_of_day": row.get("preferred_time_of_day", ""), + "preferred_note": row.get("preferred_note", ""), }) # Назначенная дата → первая; затем requested без даты def _sort_key(item): @@ -1308,6 +1337,34 @@ def _format_date_human(iso: str) -> str: return iso +def _format_preferred_human(p_type: str, p_date: str, p_tod: str, p_note: str) -> str: + """Приблизительная дата от менеджера в человекочитаемом виде.""" + tod_map = {"morning": "утром", "day": "днём", "evening": "вечером"} + if p_type == "specific": + date_part = p_date + if p_date: + try: + from datetime import datetime as _dt + date_part = _dt.strptime(p_date, "%Y-%m-%d").strftime("%d.%m.%Y") + except Exception: + pass + parts = [] + if date_part: + parts.append(date_part) + if p_tod in tod_map: + parts.append(tod_map[p_tod]) + s = " ".join(parts) if parts else "конкретная дата" + elif p_type == "this_week": + s = "эта неделя" + elif p_type == "next_week": + s = "следующая неделя" + else: + s = "согласовать с клиентом" + if p_note: + s += f" · {p_note}" + return s + + def _handle_measurement_detail(body: dict[str, Any]) -> dict[str, Any]: """Возвращает один замер целиком — для детальной страницы и печати.""" cfg = get_config() @@ -1372,6 +1429,11 @@ def _handle_measurement_detail(body: dict[str, Any]) -> dict[str, Any]: "zamer_date": row.get("zamer_date", ""), "floor_base": row.get("floor_base", ""), "photos_meta": _safe_json(row.get("photos_meta", "")), + # Приблизительная дата от менеджера (Commit C2) + "preferred_type": row.get("preferred_type", ""), + "preferred_date": row.get("preferred_date", ""), + "preferred_time_of_day": row.get("preferred_time_of_day", ""), + "preferred_note": row.get("preferred_note", ""), } diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js index d0b79e3..016c57c 100644 --- a/miniapp/assets/app.js +++ b/miniapp/assets/app.js @@ -511,7 +511,13 @@ function renderInboxItem(m) { scheduled: "📅 назначен", in_progress: "🔵 в работе", })[m.status] || m.status; - const sched = m.scheduled_at ? formatDateHuman(m.scheduled_at) : "дата не назначена"; + // Когда: точная дата если назначена, иначе приблизительная + let whenText; + if (m.scheduled_at) { + whenText = "📅 " + formatDateHuman(m.scheduled_at); + } else { + whenText = "🕐 " + formatPreferredHuman(m); + } const item = el(` @@ -521,7 +527,7 @@ function renderInboxItem(m) { ${escHtml(m.address || "адрес не указан")} - ${escHtml(sched)} · ${statusLabel} + ${escHtml(whenText)} · ${statusLabel} ${ICONS.chevron || "›"} @@ -534,6 +540,33 @@ function renderInboxItem(m) { return item; } +function formatPreferredHuman(m) { + const todMap = { morning: "утром", day: "днём", evening: "вечером" }; + const t = m.preferred_type || "tbd"; + const parts = []; + if (t === "specific") { + if (m.preferred_date) { + try { + const d = new Date(m.preferred_date); + parts.push(`${String(d.getDate()).padStart(2,"0")}.${String(d.getMonth()+1).padStart(2,"0")}`); + } catch (e) { parts.push(m.preferred_date); } + } + if (m.preferred_time_of_day && todMap[m.preferred_time_of_day]) { + parts.push(todMap[m.preferred_time_of_day]); + } + if (!parts.length) parts.push("конкретная дата"); + } else if (t === "this_week") { + parts.push("эта неделя"); + } else if (t === "next_week") { + parts.push("следующая неделя"); + } else { + parts.push("согласовать с клиентом"); + } + let s = parts.join(" "); + if (m.preferred_note) s += " · " + m.preferred_note; + return s; +} + function formatDateHuman(iso) { if (!iso) return "—"; try { @@ -607,6 +640,20 @@ async function renderInboxDetail(measurementId) { `)); + // Приблизительная дата от менеджера (если точной ещё нет — это подсказка) + if (!m.scheduled_at && (m.preferred_type || m.preferred_note)) { + const prefText = formatPreferredHuman(m); + app.appendChild(el(` + + ⏰ Когда удобно клиенту (от менеджера) + ${escHtml(prefText)} + + Позвоните клиенту и согласуйте точную дату — она появится ниже. + + + `)); + } + // Заметки от менеджера if (m.notes) { app.appendChild(el(` diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index 6d19403..72e3aa0 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -2066,6 +2066,49 @@ flex-shrink: 0; } +/* ===== Заявка на замер: выбор «когда удобно» ===== */ +.preferred-options { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin: 8px 0 6px; +} +.pref-opt { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + background: var(--card, #fff); + border: 1px solid var(--line-strong, rgba(15, 15, 14, 0.16)); + border-radius: 10px; + cursor: pointer; + user-select: none; + transition: background 0.12s, border-color 0.12s; +} +.pref-opt input[type="radio"] { + margin: 0; + accent-color: var(--walnut, #6B4A2B); +} +.pref-opt input[type="radio"]:checked + .pref-label { + font-weight: 600; + color: var(--walnut, #6B4A2B); +} +.pref-opt:has(input:checked) { + background: var(--warm, rgba(107, 74, 43, 0.08)); + border-color: var(--walnut, #6B4A2B); +} +.pref-label { + font-size: 13px; + color: var(--ink, #1F1A14); + flex: 1; +} + +/* Блок «когда удобно» в карточке замерщика */ +.preferred-block { + background: var(--warm, rgba(107, 74, 43, 0.08)); + border-left: 3px solid var(--walnut, #6B4A2B); +} + /* ===== Замер: фото с тегами ===== */ .podbor-header .podbor-help { background: transparent; diff --git a/miniapp/assets/request.js b/miniapp/assets/request.js index 6919c11..7cf07a8 100644 --- a/miniapp/assets/request.js +++ b/miniapp/assets/request.js @@ -10,6 +10,11 @@ const MeasurementRequest = (function () { 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 = []; @@ -18,7 +23,10 @@ const MeasurementRequest = (function () { document.body.classList.remove("has-bottom-nav"); const oldNav = document.getElementById("bottom-nav"); if (oldNav) oldNav.remove(); - state = { client_name: "", client_phone: "", address: "", assigned_to_tg_id: "", notes: "" }; + state = { + client_name: "", client_phone: "", address: "", assigned_to_tg_id: "", notes: "", + preferred_type: "tbd", preferred_date: "", preferred_time_of_day: "", preferred_note: "", + }; render(); loadMeasurers(); } @@ -67,6 +75,49 @@ const MeasurementRequest = (function () { + ⏰ Когда удобно клиенту + + + + Конкретная дата + + + + Эта неделя + + + + Следующая неделя + + + + Согласовать с клиентом + + + + + + Дата + + + + Время дня + + не важно + утром + днём + вечером + + + + + + + Уточнение по времени + + + + Заметки для замерщика @@ -96,6 +147,23 @@ 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() { @@ -151,11 +219,17 @@ const MeasurementRequest = (function () { method: "POST", body: JSON.stringify({ initData: tg?.initData || "", + initDataUnsafe: tg?.initDataUnsafe || null, client_name: name, 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 || "", }), }); const data = await res.json(); diff --git a/miniapp/index.html b/miniapp/index.html index 1e0ed3e..ac9ac72 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -12,8 +12,8 @@ - - + + @@ -31,13 +31,13 @@ Сделано с душой! - - - - - - - - + + + + + + + +
{client_phone}