diff --git a/backend-py/app/main.py b/backend-py/app/main.py index 51e63f8..b05b0d5 100644 --- a/backend-py/app/main.py +++ b/backend-py/app/main.py @@ -2401,11 +2401,11 @@ def _ensure_client_notes_sheet(): def _handle_client_note(body: dict[str, Any]) -> dict[str, Any]: - """Чтение/запись примечания менеджера по клиенту. - body: {initData, client_name, client_phone, note?, read?} + """Чтение/запись примечаний менеджера по клиенту (append-only история). + body: {initData, client_name, client_phone, note?} - Если note передано — пишем (upsert). Иначе просто читаем. - Возвращает {ok, note, updated_at}.""" + Если note передано — добавляем новую запись (append). + Возвращает {ok, notes: [{note, updated_at}, ...], note, updated_at} (notes = все записи, новые сверху).""" cfg = get_config() auth = verify_init_data(body.get("initData") or "", cfg.bot_token) if not auth or not auth.get("user"): @@ -2426,7 +2426,16 @@ def _handle_client_note(body: dict[str, Any]) -> dict[str, Any]: _ensure_client_notes_sheet() - # Ищем существующую заметку этого менеджера по этому клиенту + # Если note передано — пишем новую запись (append-only, история не перезаписывается) + if "note" in body and body.get("note") is not None: + new_note = str(body.get("note") or "").strip()[:4000] + if not new_note: + return {"error": "empty_note"} + now_iso = _now_iso() + sheets.append_row("ClientNotes", [str(tg_id), key, new_note, now_iso]) + sheets.log_event("client_note_added", tg_id, {"key": key, "len": len(new_note)}) + + # Читаем все заметки этого менеджера по этому клиенту try: ws = sheets.sheet("ClientNotes") rows = ws.get_all_values() @@ -2434,41 +2443,35 @@ def _handle_client_note(body: dict[str, Any]) -> dict[str, Any]: log.warning("ClientNotes read failed: %s", e) rows = [] - headers = rows[0] if rows else _CLIENT_NOTES_HEADERS - found_row_index = None # 1-based в Sheets - current_note = "" - current_updated = "" + notes: list[dict[str, str]] = [] if rows and len(rows) >= 2: + headers = rows[0] try: - idx_mgr = headers.index("manager_tg_id") - idx_key = headers.index("client_key") + idx_mgr = headers.index("manager_tg_id") + idx_key = headers.index("client_key") idx_note = headers.index("note") - idx_upd = headers.index("updated_at") + idx_upd = headers.index("updated_at") except ValueError: idx_mgr = idx_key = idx_note = idx_upd = -1 if idx_mgr >= 0 and idx_key >= 0: - for i, r in enumerate(rows[1:], start=2): - row_mgr = r[idx_mgr] if idx_mgr < len(r) else "" - row_key = r[idx_key] if idx_key < len(r) else "" - if str(row_mgr) == str(tg_id) and row_key == key: - found_row_index = i - current_note = r[idx_note] if idx_note < len(r) else "" - current_updated = r[idx_upd] if idx_upd < len(r) else "" - break + for r in rows[1:]: + if (r[idx_mgr] if idx_mgr < len(r) else "") == str(tg_id) \ + and (r[idx_key] if idx_key < len(r) else "") == key: + note_text = r[idx_note] if idx_note < len(r) else "" + upd = r[idx_upd] if idx_upd < len(r) else "" + if note_text: + notes.append({"note": note_text, "updated_at": upd}) - # Если note передано — пишем (upsert) - if "note" in body and body.get("note") is not None: - new_note = str(body.get("note") or "").strip()[:4000] - now_iso = _now_iso() - if found_row_index: - ws.update_cell(found_row_index, headers.index("note") + 1, new_note) - ws.update_cell(found_row_index, headers.index("updated_at") + 1, now_iso) - else: - sheets.append_row("ClientNotes", [str(tg_id), key, new_note, now_iso]) - sheets.log_event("client_note_updated", tg_id, {"key": key, "len": len(new_note)}) - return {"ok": True, "note": new_note, "updated_at": now_iso, "client_key": key} - - return {"ok": True, "note": current_note, "updated_at": current_updated, "client_key": key} + # Новые сверху + notes.sort(key=lambda x: x.get("updated_at", ""), reverse=True) + latest = notes[0] if notes else {} + return { + "ok": True, + "notes": notes, + "note": latest.get("note", ""), + "updated_at": latest.get("updated_at", ""), + "client_key": key, + } def _handle_geocode(body: dict[str, Any]) -> dict[str, Any]: diff --git a/miniapp/assets/clients.js b/miniapp/assets/clients.js index 5f8242e..1c9d92d 100644 --- a/miniapp/assets/clients.js +++ b/miniapp/assets/clients.js @@ -1373,19 +1373,13 @@ const Clients = (function () { const section = el(`
- 📝 Примечание - + 📝 Примечания +
- -
-

Загружаем...

- -
- - + + + +
+
Загружаем...
+
`); - const view = section.querySelector("#noteView"); - const editor = section.querySelector("#noteEditor"); - const displayTx = section.querySelector("#noteDisplayText"); - const textarea = section.querySelector("#noteText"); - const meta = section.querySelector("#noteMeta"); - const status = section.querySelector("#noteStatus"); - const editBtn = section.querySelector("#noteEditBtn"); - let savedText = ""; + const editor = section.querySelector("#noteEditor"); + const history = section.querySelector("#noteHistory"); + const textarea = section.querySelector("#noteText"); + const addBtn = section.querySelector("#noteAddBtn"); + const status = section.querySelector("#noteStatus"); - function showView(text, updatedAt) { - savedText = text || ""; - displayTx.style.fontStyle = text ? "normal" : "italic"; - displayTx.style.color = text ? "var(--ink,#1F1A14)" : "var(--muted,#998877)"; - displayTx.textContent = text || "Нет примечания"; - if (updatedAt) meta.textContent = "обновлено " + formatDate(updatedAt); - editor.style.display = "none"; - view.style.display = ""; - editBtn.textContent = "Изменить"; + function renderFeed(notes) { + history.innerHTML = ""; + if (!notes || !notes.length) { + history.innerHTML = `
Примечаний пока нет
`; + return; + } + notes.forEach(n => { + const entry = el(` +
+

${escHtml(n.note)}

+ ${n.updated_at ? `${escHtml(formatDate(n.updated_at))}` : ""} +
+ `); + history.appendChild(entry); + }); } - function showEditor() { - textarea.value = savedText; - status.textContent = ""; - status.className = "note-status"; + function openEditor() { + textarea.value = ""; + status.textContent = ""; + status.className = "note-status"; editor.style.display = ""; - view.style.display = "none"; - editBtn.textContent = "Свернуть"; + addBtn.textContent = "Свернуть"; textarea.focus(); } - // Загружаем сохранённую заметку + function closeEditor() { + editor.style.display = "none"; + addBtn.textContent = "+ Добавить"; + } + + // Загружаем историю fetchClientNote(client) - .then(data => showView(data?.note || "", data?.updated_at || "")) - .catch(() => showView("", "")); + .then(data => renderFeed(data?.notes || [])) + .catch(() => renderFeed([])); - // Переключатель просмотр ↔ редактирование - editBtn.addEventListener("click", () => { - if (editor.style.display === "none") showEditor(); - else showView(savedText, meta.textContent.replace("обновлено ", "")); + addBtn.addEventListener("click", () => { + if (editor.style.display === "none") openEditor(); else closeEditor(); }); - // Отмена — вернуть исходный текст - section.querySelector("#noteCancel").addEventListener("click", () => { - showView(savedText, meta.textContent.replace("обновлено ", "")); - }); + section.querySelector("#noteCancel").addEventListener("click", closeEditor); - // Сохранение section.querySelector("#noteSave").addEventListener("click", async () => { + const txt = (textarea.value || "").trim(); + if (!txt) { status.textContent = "Напишите заметку"; return; } const btn = section.querySelector("#noteSave"); btn.disabled = true; btn.textContent = "Сохраняем..."; status.textContent = ""; status.className = "note-status"; try { - const data = await saveClientNote(client, textarea.value); + const data = await saveClientNote(client, txt); if (data?.ok) { haptic && haptic("success"); - showView(textarea.value, data.updated_at || ""); + closeEditor(); + renderFeed(data.notes || []); } else { status.textContent = "Ошибка: " + (data?.error || "не сохранилось"); status.className = "note-status err"; @@ -1464,9 +1467,7 @@ const Clients = (function () { } }); - // Голосовой ввод setupVoiceInput(section.querySelector("#noteMic"), textarea, status); - return section; } diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index 1af4f33..037db28 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -2360,9 +2360,20 @@ background: rgba(107, 74, 43, 0.10); } -/* Просмотр-режим */ -.note-view { - padding: 6px 2px 4px; +/* Лента примечаний */ +.note-history { + margin-top: 6px; +} +.note-entry { + padding: 8px 0; + border-bottom: 1px solid rgba(107,74,43,0.08); +} +.note-entry:last-child { border-bottom: none; } +.note-loading, .note-empty { + font-size: 13px; + color: var(--muted, #998877); + font-style: italic; + padding: 6px 0; } .note-text { font-size: 14px; diff --git a/miniapp/index.html b/miniapp/index.html index 83dac2b..5f5cec9 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -12,14 +12,14 @@ - - + +
- +
- - - - - - - - - - + + + + + + + + + +