UX: карточка клиента — кнопки наверх, объёмные, хронология свёрнута

1. Кнопки управления (Редактировать/Удалить) теперь сразу под шапкой
   карточки — не нужно скроллить через таймлайн/файлы.

2. Брендовые объёмные кнопки .ct-btn вместо текста с эмодзи:
   - inline SVG (карандаш + корзина), монолиния stroke-width 1.7
   - градиент 3-стопа (highlight → base → shadow)
   - inset highlights + drop-shadow для лёгкого 3D
   - Edit: палитра ореха #8A6541 → #6B4A2B → #523620
   - Delete: благородный кирпич #C95A4A → #A6382A → #832418
   - press-state: invert insets + translateY(1px)
   - filter:brightness на hover

3. Хронология свёрнута по умолчанию (<details> без open).
   Шеврон поворачивается при раскрытии. summary без default-маркера.

index.html: cache bump v=20260514e
This commit is contained in:
wasrusgen 2026-05-14 11:53:29 +03:00
parent 4612c3a4e4
commit f280dea9ea
3 changed files with 203 additions and 36 deletions

View File

@ -416,6 +416,9 @@ const Clients = (function () {
</div>
`));
// Управление карточкой — кнопки прямо под шапкой
root.appendChild(renderClientManagement(client));
// Быстрые действия для менеджера
const actionsRow = el(`
<div class="client-quick-actions">
@ -482,28 +485,48 @@ const Clients = (function () {
// Детальные списки внизу (свёрнуты)
detailsPlaceholder.replaceWith(renderClientDetails(client, myMeasurements));
// Управление карточкой клиента — редактировать + (условно) удалить
root.appendChild(renderClientManagement(client));
// (управление перенесено наверх — сразу под шапку)
}
/* ===================== Управление карточкой (edit / delete) ===================== */
// Кастомные SVG-иконки в брендовом монолинейном стиле (stroke-width 1.7)
const ICON_EDIT_SVG = `
<svg class="ct-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3.5 20.5h4.5l10-10-4.5-4.5-10 10v4.5z"/>
<path d="M14 6l4 4"/>
<path d="M14.5 4.5l1.5-1.5a2 2 0 0 1 2.8 0l1.7 1.7a2 2 0 0 1 0 2.8L19 9"/>
</svg>`;
const ICON_TRASH_SVG = `
<svg class="ct-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 7h16"/>
<path d="M9.5 7V5.2A1.7 1.7 0 0 1 11.2 3.5h1.6A1.7 1.7 0 0 1 14.5 5.2V7"/>
<path d="M6 7l1.1 12.3a1.8 1.8 0 0 0 1.8 1.7h6.2a1.8 1.8 0 0 0 1.8-1.7L18 7"/>
<path d="M10 11.5v5.5"/>
<path d="M14 11.5v5.5"/>
</svg>`;
function renderClientManagement(client) {
const inWork = !!client.in_work;
const wrap = el(`
<section class="block client-manage" style="margin-top:18px;">
<div class="block-head"> Управление карточкой</div>
<div class="client-manage-info" style="padding:6px 4px 10px;font-size:12.5px;color:var(--muted);line-height:1.4;">
${inWork
? "Клиент в работе. Удалить нельзя, можно только отредактировать данные."
: "Клиент ещё не передан в работу — можно изменить данные или удалить карточку."}
<div class="client-toolbar ${inWork ? "is-locked" : "is-free"}">
<button class="ct-btn ct-edit" id="editClient" type="button" aria-label="Редактировать">
${ICON_EDIT_SVG}
<span class="ct-label">Редактировать</span>
</button>
${inWork ? "" : `
<button class="ct-btn ct-delete" id="deleteClient" type="button" aria-label="Удалить">
${ICON_TRASH_SVG}
<span class="ct-label">Удалить</span>
</button>`}
<div class="ct-hint">${inWork
? "В работе — данные можно править"
: "Не в работе — можно править или удалить"}
</div>
<div class="podbor-cta-row" style="gap:8px;flex-wrap:wrap;">
<button class="btn-secondary" id="editClient" type="button"> Редактировать</button>
${inWork ? "" : `<button class="btn-danger" id="deleteClient" type="button">🗑 Удалить клиента</button>`}
<div class="ct-result" id="manageResult"></div>
</div>
<div id="manageResult" style="margin-top:10px;font-size:12.5px;"></div>
</section>
`);
wrap.querySelector("#editClient")?.addEventListener("click", () => {
@ -515,8 +538,10 @@ const Clients = (function () {
const confirmed = await confirmDialog(`Удалить клиента ${client.client_name}? Это нельзя отменить из бота.`);
if (!confirmed) return;
const btn = wrap.querySelector("#deleteClient");
const labelEl = btn.querySelector(".ct-label");
const result = wrap.querySelector("#manageResult");
btn.disabled = true; btn.textContent = "Удаляем...";
btn.disabled = true;
if (labelEl) labelEl.textContent = "Удаляем…";
try {
const res = await fetch(`${BACKEND_URL}/api/client_delete`, {
method: "POST",
@ -529,17 +554,19 @@ const Clients = (function () {
const data = await res.json();
if (data.error) {
const msg = data.msg || data.error;
result.innerHTML = `<span style="color:#C0392B;">${escHtml(msg)}</span>`;
btn.disabled = false; btn.textContent = "🗑 Удалить клиента";
result.innerHTML = `<span class="ct-err">${escHtml(msg)}</span>`;
btn.disabled = false;
if (labelEl) labelEl.textContent = "Удалить";
return;
}
haptic && haptic("success");
clientsCache = null;
result.innerHTML = `<span style="color:#27AE60;">Архивировано ${data.archived} записей. Возвращаемся в список...</span>`;
result.innerHTML = `<span class="ct-ok">Архивировано ${data.archived} записей. Возвращаемся в список</span>`;
setTimeout(() => { location.hash = "#/clients"; window.location.reload(); }, 1200);
} catch (e) {
result.innerHTML = `<span style="color:#C0392B;">Сеть: ${escHtml(e.message)}</span>`;
btn.disabled = false; btn.textContent = "🗑 Удалить клиента";
result.innerHTML = `<span class="ct-err">Сеть: ${escHtml(e.message)}</span>`;
btn.disabled = false;
if (labelEl) labelEl.textContent = "Удалить";
}
});
@ -729,8 +756,11 @@ const Clients = (function () {
events.sort((a, b) => (b.ts || "").localeCompare(a.ts || ""));
const section = el(`
<section class="block client-timeline-block">
<div class="block-head">🕒 Хронология · ${events.length}</div>
<details class="block client-timeline-block client-collapse">
<summary class="block-head collapse-head">
<span class="collapse-title">🕒 Хронология · ${events.length}</span>
<span class="collapse-chev" aria-hidden="true"></span>
</summary>
${events.length === 0
? `<div class="empty" style="padding:14px;text-align:center;color:var(--muted);font-size:13px;">Пока нет событий</div>`
: `<div class="timeline">${events.map(ev => `
@ -743,7 +773,7 @@ const Clients = (function () {
</div>
</a>
`).join("")}</div>`}
</section>
</details>
`);
return section;
}

View File

@ -3052,6 +3052,143 @@
flex-shrink: 0;
}
/* ===== Тулбар управления карточкой клиента — объёмные кнопки ===== */
.client-toolbar {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin: 12px 0 4px;
align-items: stretch;
}
.client-toolbar.is-locked {
grid-template-columns: 1fr; /* только «Редактировать», когда в работе */
}
.ct-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
min-height: 46px;
padding: 0 16px;
border: none;
border-radius: 13px;
font-family: var(--font-ui, "Inter", system-ui, sans-serif);
font-size: 14px;
font-weight: 600;
letter-spacing: 0.01em;
color: #FBF7F0;
cursor: pointer;
text-transform: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease;
}
.ct-btn .ct-icon {
width: 18px;
height: 18px;
stroke-width: 1.7;
flex-shrink: 0;
filter: drop-shadow(0 1px 0 rgba(0,0,0,0.18));
}
/* Edit — глубокий орех */
.ct-edit {
background:
linear-gradient(180deg,
rgba(255,255,255,0.10) 0%,
rgba(255,255,255,0.00) 36%,
rgba(0,0,0,0.00) 64%,
rgba(0,0,0,0.16) 100%),
linear-gradient(180deg, #8A6541 0%, #6B4A2B 55%, #523620 100%);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.22),
inset 0 -1px 0 rgba(0,0,0,0.18),
0 3px 8px rgba(82, 54, 32, 0.32),
0 1px 2px rgba(0, 0, 0, 0.12);
}
.ct-edit:hover { filter: brightness(1.04); }
.ct-edit:active {
transform: translateY(1px);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.10),
inset 0 2px 4px rgba(0,0,0,0.22),
0 1px 2px rgba(0,0,0,0.10);
}
/* Delete — благородный кирпично-красный, не «алярм» */
.ct-delete {
background:
linear-gradient(180deg,
rgba(255,255,255,0.10) 0%,
rgba(255,255,255,0.00) 36%,
rgba(0,0,0,0.00) 64%,
rgba(0,0,0,0.18) 100%),
linear-gradient(180deg, #C95A4A 0%, #A6382A 55%, #832418 100%);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.22),
inset 0 -1px 0 rgba(0,0,0,0.20),
0 3px 8px rgba(131, 36, 24, 0.30),
0 1px 2px rgba(0, 0, 0, 0.12);
}
.ct-delete:hover { filter: brightness(1.05); }
.ct-delete:active {
transform: translateY(1px);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.10),
inset 0 2px 4px rgba(0,0,0,0.22),
0 1px 2px rgba(0,0,0,0.10);
}
.ct-btn:disabled {
opacity: 0.55;
cursor: wait;
filter: saturate(0.7);
transform: none;
}
.ct-hint {
grid-column: 1 / -1;
font-size: 11.5px;
color: var(--muted, #998877);
text-align: center;
letter-spacing: 0.01em;
margin-top: 2px;
line-height: 1.3;
}
.ct-result { grid-column: 1 / -1; min-height: 0; font-size: 12.5px; }
.ct-result:empty { display: none; }
.ct-ok { color: #4A8016; font-weight: 500; }
.ct-err { color: #A6382A; font-weight: 500; }
/* ===== Сворачиваемые блоки (хронология и т.п.) ===== */
.client-collapse {
/* убираем дефолтный маркер */
}
.client-collapse > summary {
list-style: none;
cursor: pointer;
padding: 12px 6px;
user-select: none;
}
.client-collapse > summary::-webkit-details-marker { display: none; }
.client-collapse > summary.collapse-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.client-collapse .collapse-chev {
display: inline-block;
color: var(--muted, #998877);
font-size: 22px;
line-height: 1;
transform: rotate(90deg);
transition: transform 0.2s ease;
}
.client-collapse[open] .collapse-chev { transform: rotate(-90deg); }
/* ===== Сборки (Phase 4) ===== */
.assembly-list {
display: flex;

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=20260514d">
<link rel="stylesheet" href="assets/podbor.css?v=20260514d">
<link rel="stylesheet" href="assets/styles.css?v=20260514e">
<link rel="stylesheet" href="assets/podbor.css?v=20260514e">
</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=20260514d" alt="@wasrusgen1">
<img class="brand-logo" src="assets/wasrusgen-logo.svg?v=20260514e" 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=20260514d"></script>
<script src="assets/podbor.config.js?v=20260514d"></script>
<script src="assets/podbor.picts.js?v=20260514d"></script>
<script src="assets/podbor.js?v=20260514d"></script>
<script src="assets/clients.js?v=20260514d"></script>
<script src="assets/zamer-picts.js?v=20260514d"></script>
<script src="assets/measurements.js?v=20260514d"></script>
<script src="assets/request.js?v=20260514d"></script>
<script src="assets/assembly.js?v=20260514d"></script>
<script src="assets/app.js?v=20260514d"></script>
<script src="assets/icons.js?v=20260514e"></script>
<script src="assets/podbor.config.js?v=20260514e"></script>
<script src="assets/podbor.picts.js?v=20260514e"></script>
<script src="assets/podbor.js?v=20260514e"></script>
<script src="assets/clients.js?v=20260514e"></script>
<script src="assets/zamer-picts.js?v=20260514e"></script>
<script src="assets/measurements.js?v=20260514e"></script>
<script src="assets/request.js?v=20260514e"></script>
<script src="assets/assembly.js?v=20260514e"></script>
<script src="assets/app.js?v=20260514e"></script>
</body>
</html>