fix(note): view/edit toggle — textarea closes after save

Note block now has display mode (read-only text) and edit mode (textarea).
Default is display. "Изменить" opens editor, "Сохранить" saves and returns
to display, "Отмена" discards. No more always-open textarea confusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
wasrusgen 2026-05-14 14:18:16 +03:00
parent dd8691671a
commit cbea202de5
3 changed files with 111 additions and 37 deletions

View File

@ -1205,58 +1205,98 @@ const Clients = (function () {
<section class="block client-note-block">
<div class="block-head">
<span>📝 Примечание</span>
<button class="note-edit-toggle" id="noteEditBtn" type="button" title="Редактировать">Изменить</button>
</div>
<!-- Режим просмотра -->
<div class="note-view" id="noteView">
<p class="note-text" id="noteDisplayText" style="color:var(--muted,#998877);font-style:italic;">Загружаем...</p>
<span class="note-meta" id="noteMeta"></span>
</div>
<div class="note-editor">
<textarea id="noteText" rows="3" placeholder="Заметки по клиенту — характер, предпочтения, договорённости, статус..."></textarea>
<!-- Режим редактирования (скрыт по умолчанию) -->
<div class="note-editor" id="noteEditor" style="display:none;">
<textarea id="noteText" rows="4" placeholder="Заметки по клиенту — характер, предпочтения, договорённости, статус..."></textarea>
<div class="note-actions">
<button class="btn-mic" id="noteMic" type="button" title="Голосовой ввод">🎤 Диктовать</button>
<button class="btn-secondary" id="noteSave" type="button">Сохранить</button>
<button class="btn-secondary" id="noteCancel" type="button">Отмена</button>
<button class="btn-primary" id="noteSave" type="button">Сохранить</button>
</div>
<div class="note-status" id="noteStatus"></div>
</div>
</section>
`);
const textarea = section.querySelector("#noteText");
const meta = section.querySelector("#noteMeta");
const status = section.querySelector("#noteStatus");
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 = "";
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 showEditor() {
textarea.value = savedText;
status.textContent = "";
status.className = "note-status";
editor.style.display = "";
view.style.display = "none";
editBtn.textContent = "Свернуть";
textarea.focus();
}
// Загружаем сохранённую заметку
fetchClientNote(client).then(data => {
if (data?.note) textarea.value = data.note;
if (data?.updated_at) {
meta.textContent = "обновлено " + formatDate(data.updated_at);
}
}).catch(() => {});
fetchClientNote(client)
.then(data => showView(data?.note || "", data?.updated_at || ""))
.catch(() => showView("", ""));
// Переключатель просмотр ↔ редактирование
editBtn.addEventListener("click", () => {
if (editor.style.display === "none") showEditor();
else showView(savedText, meta.textContent.replace("обновлено ", ""));
});
// Отмена — вернуть исходный текст
section.querySelector("#noteCancel").addEventListener("click", () => {
showView(savedText, meta.textContent.replace("обновлено ", ""));
});
// Сохранение
section.querySelector("#noteSave").addEventListener("click", async () => {
const btn = section.querySelector("#noteSave");
btn.disabled = true;
btn.textContent = "Сохраняем...";
btn.disabled = true; btn.textContent = "Сохраняем...";
status.textContent = ""; status.className = "note-status";
try {
const data = await saveClientNote(client, textarea.value);
if (data?.ok) {
status.textContent = "✓ сохранено";
status.className = "note-status ok";
if (data.updated_at) meta.textContent = "обновлено " + formatDate(data.updated_at);
setTimeout(() => { status.textContent = ""; }, 2500);
haptic && haptic("success");
showView(textarea.value, data.updated_at || "");
} else {
status.textContent = "Ошибка: " + (data?.error || "не сохранилось");
status.className = "note-status err";
btn.disabled = false; btn.textContent = "Сохранить";
}
} catch (e) {
status.textContent = "Сеть: " + e.message;
status.className = "note-status err";
btn.disabled = false; btn.textContent = "Сохранить";
}
btn.disabled = false;
btn.textContent = "Сохранить";
});
// Голосовой ввод через Web Speech API
const micBtn = section.querySelector("#noteMic");
setupVoiceInput(micBtn, textarea, status);
// Голосовой ввод
setupVoiceInput(section.querySelector("#noteMic"), textarea, status);
return section;
}

View File

@ -2309,7 +2309,41 @@
.client-note-block .block-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.client-note-block .block-head > span:first-child {
flex: 1;
}
/* Кнопка-переключатель Изменить / Свернуть */
.note-edit-toggle {
padding: 4px 12px;
background: transparent;
border: 1px solid rgba(107, 74, 43, 0.30);
border-radius: 20px;
font-size: 12px;
font-weight: 500;
color: var(--walnut, #6B4A2B);
cursor: pointer;
font-family: inherit;
white-space: nowrap;
transition: background 0.12s, color 0.12s;
}
.note-edit-toggle:active {
background: rgba(107, 74, 43, 0.10);
}
/* Просмотр-режим */
.note-view {
padding: 6px 2px 4px;
}
.note-text {
font-size: 14px;
line-height: 1.5;
color: var(--ink, #1F1A14);
white-space: pre-wrap;
word-break: break-word;
margin: 0 0 4px;
}
.client-note-block .note-meta {
font-size: 11px;

View File

@ -12,14 +12,14 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&family=Caveat:wght@500;700&display=swap">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="assets/styles.css?v=20260514g">
<link rel="stylesheet" href="assets/podbor.css?v=20260514g">
<link rel="stylesheet" href="assets/styles.css?v=20260514h">
<link rel="stylesheet" href="assets/podbor.css?v=20260514h">
</head>
<body>
<!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск -->
<div class="loader splash" id="splash">
<div class="brand-logo-wrap">
<img class="brand-logo" src="assets/wasrusgen-logo.svg?v=20260514g" alt="@wasrusgen1">
<img class="brand-logo" src="assets/wasrusgen-logo.svg?v=20260514h" alt="@wasrusgen1">
<div class="splash-dust" aria-hidden="true">
<span class="dust d1"></span> <span class="dust d2"></span>
<span class="dust d3"></span> <span class="dust d4"></span>
@ -35,15 +35,15 @@
<div class="brand-tagline-gold">CRM</div>
</div>
<main id="app"></main>
<script src="assets/icons.js?v=20260514g"></script>
<script src="assets/podbor.config.js?v=20260514g"></script>
<script src="assets/podbor.picts.js?v=20260514g"></script>
<script src="assets/podbor.js?v=20260514g"></script>
<script src="assets/clients.js?v=20260514g"></script>
<script src="assets/zamer-picts.js?v=20260514g"></script>
<script src="assets/measurements.js?v=20260514g"></script>
<script src="assets/request.js?v=20260514g"></script>
<script src="assets/assembly.js?v=20260514g"></script>
<script src="assets/app.js?v=20260514g"></script>
<script src="assets/icons.js?v=20260514h"></script>
<script src="assets/podbor.config.js?v=20260514h"></script>
<script src="assets/podbor.picts.js?v=20260514h"></script>
<script src="assets/podbor.js?v=20260514h"></script>
<script src="assets/clients.js?v=20260514h"></script>
<script src="assets/zamer-picts.js?v=20260514h"></script>
<script src="assets/measurements.js?v=20260514h"></script>
<script src="assets/request.js?v=20260514h"></script>
<script src="assets/assembly.js?v=20260514h"></script>
<script src="assets/app.js?v=20260514h"></script>
</body>
</html>