diff --git a/backend-py/app/main.py b/backend-py/app/main.py
index 90d629d..6351077 100644
--- a/backend-py/app/main.py
+++ b/backend-py/app/main.py
@@ -111,6 +111,7 @@ async def _dispatch_post(request: Request):
"measurement_request": _handle_measurement_request,
"measurement_inbox": _handle_measurement_inbox,
"measurement_schedule": _handle_measurement_schedule,
+ "measurement_next_no": _handle_measurement_next_no,
"ping": lambda b: {"pong": True, "time": _now_iso()},
"seed_admin": lambda b: _handle_seed_admin(),
"test_ai": lambda b: _handle_test_ai(),
@@ -201,6 +202,12 @@ async def api_measurement_schedule(request: Request):
return _handle_measurement_schedule(body)
+@app.post("/api/measurement_next_no")
+async def api_measurement_next_no(request: Request):
+ body = await _safe_json(request)
+ return _handle_measurement_next_no(body)
+
+
@app.post("/api/grant_role")
async def api_grant_role(request: Request):
"""Админ выдаёт роль другому пользователю.
@@ -1254,6 +1261,42 @@ def _handle_measurement_schedule(body: dict[str, Any]) -> dict[str, Any]:
return {"ok": True, "id": measurement_id, "status": "scheduled", "scheduled_at": scheduled_at}
+def _handle_measurement_next_no(body: dict[str, Any]) -> dict[str, Any]:
+ """Возвращает следующий свободный номер замера (max существующих + 1).
+ Если в Sheets ничего нет — стартуем с 1. Менеджер может скорректировать вручную
+ (например первый раз поставить 158, если до этого замеры были вне системы)."""
+ cfg = get_config()
+ auth = verify_init_data(body.get("initData") or "", cfg.bot_token)
+ if not auth or not auth.get("user"):
+ unsafe = body.get("initDataUnsafe") or {}
+ if not (isinstance(unsafe, dict) and unsafe.get("user", {}).get("id")):
+ return {"error": "invalid_init_data"}
+
+ _ensure_measurements_sheet()
+ try:
+ ws = sheets.sheet("Measurements")
+ rows = ws.get_all_values()
+ except Exception:
+ return {"ok": True, "next_no": 1}
+ if not rows or len(rows) < 2:
+ return {"ok": True, "next_no": 1}
+ headers = rows[0]
+ if "zamer_no" not in headers:
+ return {"ok": True, "next_no": 1}
+ idx = headers.index("zamer_no")
+ max_n = 0
+ for r in rows[1:]:
+ if idx >= len(r):
+ continue
+ try:
+ n = int(str(r[idx]).strip())
+ if n > max_n:
+ max_n = n
+ except (ValueError, TypeError):
+ pass
+ return {"ok": True, "next_no": max_n + 1}
+
+
def _format_date_human(iso: str) -> str:
"""ISO datetime → '15.05.2026 14:00' для уведомлений."""
if not iso:
diff --git a/miniapp/assets/measurements.js b/miniapp/assets/measurements.js
index f0258e5..1115589 100644
--- a/miniapp/assets/measurements.js
+++ b/miniapp/assets/measurements.js
@@ -44,10 +44,10 @@ const Measurements = (function () {
client_phone: "",
address: "",
notes: "",
- // Общая инфа замера (по чек-листу)
+ // Общая инфа замера. zamer_no подгружается из бэка автоматически,
+ // floor_base убран — он на самих фото с замером.
zamer_no: "",
zamer_date: todayStr,
- floor_base: "0,000 = +88 мм над плитой",
};
}
@@ -170,19 +170,14 @@ const Measurements = (function () {
-
-
-
📷 Фото замера
@@ -224,9 +219,44 @@ const Measurements = (function () {
location.hash = "#/measure/checklist";
});
node.querySelector("#submitBtn").addEventListener("click", () => onSubmit(node));
+
+ // Подгружаем следующий № замера если поле пустое
+ if (!state.zamer_no) {
+ fetchNextZamerNo(node);
+ } else {
+ const hint = node.querySelector("#zamerNoHint");
+ if (hint) hint.textContent = "Можно переписать вручную";
+ }
+
return node;
}
+ async function fetchNextZamerNo(node) {
+ try {
+ const res = await fetch(`${BACKEND_URL}/api/measurement_next_no`, {
+ method: "POST",
+ body: JSON.stringify({
+ initData: tg?.initData || "",
+ initDataUnsafe: tg?.initDataUnsafe || null,
+ }),
+ });
+ const data = await res.json();
+ const hint = node.querySelector("#zamerNoHint");
+ const input = node.querySelector("#zamerNoInput");
+ if (data.ok && data.next_no && input && !state.zamer_no) {
+ input.value = String(data.next_no);
+ state.zamer_no = String(data.next_no);
+ saveState();
+ if (hint) hint.textContent = "Подобран автоматически — можно изменить";
+ } else if (hint) {
+ hint.textContent = "Введите номер вручную";
+ }
+ } catch (e) {
+ const hint = node.querySelector("#zamerNoHint");
+ if (hint) hint.textContent = "Введите номер вручную";
+ }
+ }
+
function renderClientReadOnly() {
return el(`
@@ -377,17 +407,31 @@ const Measurements = (function () {
/* ===================== Чек-лист — отдельный экран ===================== */
+ // Состояние галочек хранится в localStorage по measurement_id (или draft)
+ function checklistKey() {
+ return `zov-checklist-${measurementId || "draft"}`;
+ }
+ function loadChecklistState() {
+ try { return JSON.parse(localStorage.getItem(checklistKey()) || "{}"); }
+ catch (e) { return {}; }
+ }
+ function saveChecklistState(s) {
+ try { localStorage.setItem(checklistKey(), JSON.stringify(s)); } catch (e) {}
+ }
+ function resetChecklistDraft() {
+ try { localStorage.removeItem(`zov-checklist-draft`); } catch (e) {}
+ }
+
async function renderChecklist() {
root.innerHTML = "";
root.appendChild(el(`
`));
root.querySelector(".podbor-back").addEventListener("click", () => {
- // Возврат к мастеру (если был открыт через #/measure?id=X)
if (measurementId) location.hash = `#/measure?id=${measurementId}`;
else location.hash = "#/measure";
});
@@ -399,14 +443,63 @@ const Measurements = (function () {
try {
const res = await fetch("./assets/zamer-checklist.md", { cache: "no-cache" });
const md = await res.text();
- wrap.innerHTML = `
${renderMarkdown(md)}
`;
+ const clState = loadChecklistState();
+ wrap.innerHTML = `
+
+
${renderMarkdown(md, clState)}
+ `;
+ bindChecklistInteractions(wrap, clState);
+ updateChecklistProgress(wrap, clState);
+
+ root.querySelector("#resetCl").addEventListener("click", () => {
+ if (!confirm("Сбросить все галочки?")) return;
+ const empty = {};
+ saveChecklistState(empty);
+ renderChecklist();
+ });
} catch (e) {
wrap.innerHTML = `
Не удалось загрузить чек-лист: ${e.message}
`;
}
}
- /* Минимальный markdown → HTML: заголовки, списки, таблицы, code */
- function renderMarkdown(md) {
+ function bindChecklistInteractions(wrap, clState) {
+ wrap.querySelectorAll(".cl-item").forEach(item => {
+ item.addEventListener("click", () => {
+ const key = item.dataset.key;
+ if (!key) return;
+ const isChecked = item.classList.contains("checked");
+ const checkSpan = item.querySelector(".cl-check");
+ if (isChecked) {
+ item.classList.remove("checked");
+ if (checkSpan) checkSpan.textContent = "☐";
+ delete clState[key];
+ } else {
+ item.classList.add("checked");
+ if (checkSpan) checkSpan.textContent = "☑";
+ clState[key] = true;
+ }
+ saveChecklistState(clState);
+ updateChecklistProgress(wrap, clState);
+ haptic && haptic("impact");
+ });
+ });
+ }
+
+ function updateChecklistProgress(wrap, clState) {
+ const total = wrap.querySelectorAll(".cl-item").length;
+ const done = Object.keys(clState).filter(k => clState[k]).length;
+ const pct = total ? Math.round((done / total) * 100) : 0;
+ const bar = wrap.querySelector("#clProgress");
+ if (bar) {
+ bar.innerHTML = `
+
+
${done} из ${total} · ${pct}%
+ `;
+ }
+ }
+
+ /* Минимальный markdown → HTML: заголовки, списки, таблицы, code, чекбоксы */
+ function renderMarkdown(md, clState = {}) {
const lines = md.split("\n");
const out = [];
let inList = false;
@@ -461,11 +554,17 @@ const Measurements = (function () {
} else if (line.startsWith("- ") || line.startsWith("* ")) {
if (!inList) { out.push("
"); inList = true; }
let content = line.slice(2);
- // [ ] checkbox
- if (content.startsWith("[ ] ")) {
- out.push(`- ☐ ${inline(content.slice(4))}
`);
- } else if (content.startsWith("[x] ") || content.startsWith("[X] ")) {
- out.push(`- ☑ ${inline(content.slice(4))}
`);
+ // [ ] checkbox — делаем интерактивным с уникальным ключом
+ if (content.startsWith("[ ] ") || content.startsWith("[x] ") || content.startsWith("[X] ")) {
+ const text = content.slice(4);
+ // Ключ = первые 60 символов содержимого (для стабильности при edit)
+ const key = "cl_" + text.replace(/[^\wа-яА-ЯёЁ]+/g, "_").slice(0, 60).toLowerCase();
+ const checked = !!clState[key];
+ out.push(
+ `- ` +
+ `${checked ? "☑" : "☐"} ${inline(text)}` +
+ `
`
+ );
} else {
out.push(`- ${inline(content)}
`);
}
@@ -526,7 +625,6 @@ const Measurements = (function () {
// Общая инфа замера
zamer_no: state.zamer_no || "",
zamer_date: state.zamer_date || "",
- floor_base: state.floor_base || "",
notes: state.notes || "",
// Клиент
client_name: isUpdate ? prefilledClient.name : state.client_name,
diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css
index c30cba0..87276d6 100644
--- a/miniapp/assets/podbor.css
+++ b/miniapp/assets/podbor.css
@@ -2067,9 +2067,56 @@
.checklist-md ul { margin: 6px 0 6px 6px; padding-left: 14px; }
.checklist-md li { margin: 3px 0; list-style: none; position: relative; padding-left: 22px; }
.checklist-md li::before { content: ""; position: absolute; left: 6px; top: 9px; width: 6px; height: 6px; background: var(--walnut, #6B4A2B); opacity: 0.45; border-radius: 50%; }
-.checklist-md li .cl-check { position: absolute; left: 0; top: 0; font-size: 14px; color: var(--walnut, #6B4A2B); }
-.checklist-md li .cl-check.checked { color: var(--accent-1, #003E7E); }
+.checklist-md li .cl-check { position: absolute; left: 0; top: 0; font-size: 16px; color: var(--walnut, #6B4A2B); line-height: 1.3; }
.checklist-md li:has(.cl-check)::before { display: none; }
+
+/* Активный чекбокс — кликабельный с подсветкой */
+.checklist-md .cl-item {
+ cursor: pointer;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ padding: 4px 6px 4px 22px;
+ border-radius: 6px;
+ transition: background 0.12s;
+}
+.checklist-md .cl-item:active { background: rgba(107, 74, 43, 0.10); }
+.checklist-md .cl-item.checked { color: var(--muted, #998877); text-decoration: line-through; }
+.checklist-md .cl-item.checked .cl-check {
+ color: var(--accent-1, #003E7E);
+ text-decoration: none;
+ display: inline-block;
+}
+
+/* Прогресс-бар чек-листа */
+.checklist-progress {
+ position: sticky;
+ top: 0;
+ background: var(--paper, #FBF7F0);
+ padding: 10px 0 12px;
+ margin: -6px -6px 8px;
+ z-index: 5;
+ border-bottom: 1px solid rgba(107, 74, 43, 0.12);
+}
+.cl-pbar {
+ height: 3px;
+ background: rgba(107, 74, 43, 0.12);
+ border-radius: 2px;
+ overflow: hidden;
+}
+.cl-pbar-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--walnut, #6B4A2B), var(--accent-1, #003E7E));
+ transition: width 0.25s ease;
+}
+.cl-pcount {
+ font-family: var(--font-mono, "JetBrains Mono", monospace);
+ font-size: 10.5px;
+ color: var(--muted, #998877);
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ margin-top: 6px;
+ text-align: right;
+}
.checklist-md hr { margin: 18px 0; border: none; border-top: 1px dashed rgba(107, 74, 43, 0.25); }
.checklist-md code { background: rgba(107, 74, 43, 0.08); padding: 1px 5px; border-radius: 3px; font-family: var(--font-mono, "JetBrains Mono", monospace); font-size: 12.5px; }
.checklist-md strong { color: var(--ink, #1F1A14); }
diff --git a/miniapp/index.html b/miniapp/index.html
index 9eaab7c..a56f9f8 100644
--- a/miniapp/index.html
+++ b/miniapp/index.html
@@ -12,8 +12,8 @@
-
-
+
+
@@ -34,13 +34,13 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+