zov-tech/icon-picker.html
wasrusgen 02f8dba469 feat: expeditor cabinet, electronic signature (OTP+canvas), invoice room picker
New modules:
- expeditor_dashboard.js: route list (date-grouped) + act detail + signature screen
- invoice.js: 3-col chip room picker, 2500₽ base + 1000₽ extra logic
- act4.js, measurer_dashboard.js, finance_summary.js, client_timeline.js, feedback.js, staff_roster.js

Backend:
- /api/expeditor_inbox: filtered assembly list for expeditor role
- /api/act4_request_otp: 6-digit OTP via Telegram, 10-min expiry
- /api/act4_verify_otp: validates code, marks act as signed
- /api/act4_save_signature: saves base64 canvas signature
- Act4s sheet: added signature_b64, otp_code, otp_expires_at columns

Tests:
- tests/expeditor_scenarios.md: 11 manual test scenarios

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 14:11:20 +03:00

346 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Выбор иконок — Tabler Icons (MIT)</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #EFE9DF;
padding: 32px 24px;
color: #3a2a1a;
}
h1 { color: #6B4A2B; font-size: 22px; margin-bottom: 6px; }
.subtitle { color: #9E7A5A; font-size: 13px; margin-bottom: 36px; }
.section { margin-bottom: 36px; }
.section-title {
font-size: 13px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.1em; color: #6B4A2B; margin-bottom: 16px;
display: flex; align-items: center; gap: 10px;
}
.section-title::after {
content: ''; flex: 1; height: 1px; background: #C4A882;
}
.icons-row {
display: flex; flex-wrap: wrap; gap: 12px;
}
.icon-card {
background: #FBF7F0;
border: 2px solid transparent;
border-radius: 14px;
padding: 18px 14px 12px;
display: flex; flex-direction: column;
align-items: center; gap: 10px;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
width: 110px;
box-shadow: 0 2px 6px rgba(107,74,43,0.08);
user-select: none;
}
.icon-card:hover {
border-color: #C4A882;
box-shadow: 0 4px 14px rgba(107,74,43,0.15);
transform: translateY(-2px);
}
.icon-card.selected {
border-color: #6B4A2B;
background: #F0E8DC;
box-shadow: 0 4px 16px rgba(107,74,43,0.2);
}
.icon-card svg {
display: block;
}
.icon-name {
font-size: 11px; color: #9E7A5A; text-align: center;
font-weight: 500; line-height: 1.3;
}
.icon-card.selected .icon-name { color: #6B4A2B; font-weight: 700; }
.result-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: #6B4A2B; color: #FBF7F0;
padding: 14px 24px;
display: flex; align-items: center; justify-content: space-between;
font-size: 14px;
transform: translateY(100%);
transition: transform 0.25s;
}
.result-bar.visible { transform: translateY(0); }
.result-bar strong { font-weight: 700; }
.result-bar code {
background: rgba(255,255,255,0.15); border-radius: 4px;
padding: 2px 8px; font-family: monospace; font-size: 12px;
}
.result-bar .hint { color: #C4A882; font-size: 12px; }
</style>
</head>
<body>
<h1>Выбор иконок для роли</h1>
<p class="subtitle">Источник: <strong>Tabler Icons</strong> — MIT лицензия, бесплатно в коммерческих проектах. Нажмите на иконку, чтобы выбрать.</p>
<!-- ====== МЕНЕДЖЕР ====== -->
<div class="section">
<div class="section-title">Менеджер — «Я менеджер / Веду клиентов и заказы»</div>
<div class="icons-row">
<div class="icon-card" data-role="manager" data-name="user" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"/>
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"/>
</svg>
<div class="icon-name">Человек</div>
</div>
<div class="icon-card" data-role="manager" data-name="user-circle" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/>
<path d="M9 10a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/>
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855"/>
</svg>
<div class="icon-name">Профиль в круге</div>
</div>
<div class="icon-card" data-role="manager" data-name="user-check" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"/>
<path d="M6 21v-2a4 4 0 0 1 4 -4h4"/>
<path d="M15 19l2 2l4 -4"/>
</svg>
<div class="icon-name">Подтверждённый</div>
</div>
<div class="icon-card" data-role="manager" data-name="user-star" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"/>
<path d="M6 21v-2a4 4 0 0 1 4 -4h.5"/>
<path d="M17.8 20.817l-2.172 1.138a.392 .392 0 0 1 -.568 -.41l.415 -2.411l-1.757 -1.707a.389 .389 0 0 1 .217 -.665l2.428 -.352l1.086 -2.193a.392 .392 0 0 1 .702 0l1.086 2.193l2.428 .352a.39 .39 0 0 1 .217 .665l-1.757 1.707l.414 2.41a.39 .39 0 0 1 -.567 .411z"/>
</svg>
<div class="icon-name">VIP / Звезда</div>
</div>
<div class="icon-card" data-role="manager" data-name="tie" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22l4 -4l-2.5 -11l.993 -2.649a1 1 0 0 0 -.936 -1.351h-3.114a1 1 0 0 0 -.936 1.351l.993 2.649l-2.5 11l4 4"/>
<path d="M10.5 7h3l5 5.5"/>
</svg>
<div class="icon-name">Галстук</div>
</div>
<div class="icon-card" data-role="manager" data-name="briefcase" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2l0 -9"/>
<path d="M8 7v-2a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v2"/>
<path d="M12 12l0 .01"/>
<path d="M3 13a20 20 0 0 0 18 0"/>
</svg>
<div class="icon-name">Портфель</div>
</div>
<div class="icon-card" data-role="manager" data-name="device-laptop" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 19l18 0"/>
<path d="M5 7a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v8a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1l0 -8"/>
</svg>
<div class="icon-name">Ноутбук</div>
</div>
</div>
</div>
<!-- ====== КЛИЕНТ ====== -->
<div class="section">
<div class="section-title">Клиент — «Я клиент / Заказал кухню ЗОВ»</div>
<div class="icons-row">
<div class="icon-card" data-role="client" data-name="home" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12l-2 0l9 -9l9 9l-2 0"/>
<path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7"/>
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6"/>
</svg>
<div class="icon-name">Дом классик</div>
</div>
<div class="icon-card" data-role="client" data-name="home-2" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12l-2 0l9 -9l9 9l-2 0"/>
<path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7"/>
<path d="M10 12h4v4h-4l0 -4"/>
</svg>
<div class="icon-name">Дом с окном</div>
</div>
<div class="icon-card" data-role="client" data-name="smart-home" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 8.71l-5.333 -4.148a2.666 2.666 0 0 0 -3.274 0l-5.334 4.148a2.665 2.665 0 0 0 -1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-7.2c0 -.823 -.38 -1.6 -1.03 -2.105"/>
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0"/>
</svg>
<div class="icon-name">Смарт-дом</div>
</div>
<div class="icon-card" data-role="client" data-name="home-heart" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12l-9 -9l-9 9h2v7a2 2 0 0 0 2 2h6"/>
<path d="M9 21v-6a2 2 0 0 1 2 -2h2c.39 0 .754 .112 1.061 .304"/>
<path d="M19 21.5l2.518 -2.58a1.74 1.74 0 0 0 0 -2.413a1.627 1.627 0 0 0 -2.346 0l-.168 .172l-.168 -.172a1.627 1.627 0 0 0 -2.346 0a1.74 1.74 0 0 0 0 2.412l2.51 2.59z"/>
</svg>
<div class="icon-name">Дом с сердцем</div>
</div>
<div class="icon-card" data-role="client" data-name="home-check" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2"/>
<path d="M19 13.488v-1.488h2l-9 -9l-9 9h2v7a2 2 0 0 0 2 2h4.525"/>
<path d="M15 19l2 2l4 -4"/>
</svg>
<div class="icon-name">Дом с галкой</div>
</div>
<div class="icon-card" data-role="client" data-name="building" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 21l18 0"/>
<path d="M9 8l1 0"/><path d="M9 12l1 0"/><path d="M9 16l1 0"/>
<path d="M14 8l1 0"/><path d="M14 12l1 0"/><path d="M14 16l1 0"/>
<path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16"/>
</svg>
<div class="icon-name">Здание</div>
</div>
<div class="icon-card" data-role="client" data-name="door" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 12v.01"/>
<path d="M3 21h18"/>
<path d="M6 21v-16a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v16"/>
</svg>
<div class="icon-name">Дверь</div>
</div>
</div>
</div>
<!-- ====== СОТРУДНИК ====== -->
<div class="section" style="padding-bottom: 80px;">
<div class="section-title">Сотрудник — «Я сотрудник / Замерщик или сборщик ЗОВ»</div>
<div class="icons-row">
<div class="icon-card" data-role="staff" data-name="helmet" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 4a9 9 0 0 1 5.656 16h-11.312a9 9 0 0 1 5.656 -16"/>
<path d="M20 9h-8.8a1 1 0 0 0 -.968 1.246c.507 2 1.596 3.418 3.268 4.254c2 1 4.333 1.5 7 1.5"/>
</svg>
<div class="icon-name">Каска</div>
</div>
<div class="icon-card" data-role="staff" data-name="tool" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"/>
</svg>
<div class="icon-name">Гаечный ключ</div>
</div>
<div class="icon-card" data-role="staff" data-name="tools" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 21h4l13 -13a1.5 1.5 0 0 0 -4 -4l-13 13v4"/>
<path d="M14.5 5.5l4 4"/>
<path d="M12 8l-5 -5l-4 4l5 5"/>
<path d="M7 8l-1.5 1.5"/>
<path d="M16 12l5 5l-4 4l-5 -5"/>
<path d="M16 17l-1.5 1.5"/>
</svg>
<div class="icon-name">Набор инструментов</div>
</div>
<div class="icon-card" data-role="staff" data-name="hammer" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M11.414 10l-7.383 7.418a2.091 2.091 0 0 0 0 2.967a2.11 2.11 0 0 0 2.976 0l7.407 -7.385"/>
<path d="M18.121 15.293l2.586 -2.586a1 1 0 0 0 0 -1.414l-7.586 -7.586a1 1 0 0 0 -1.414 0l-2.586 2.586a1 1 0 0 0 0 1.414l7.586 7.586a1 1 0 0 0 1.414 0z"/>
</svg>
<div class="icon-name">Молоток</div>
</div>
<div class="icon-card" data-role="staff" data-name="ruler-2" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 3l4 4l-14 14l-4 -4l14 -14"/>
<path d="M16 7l-1.5 -1.5"/>
<path d="M13 10l-1.5 -1.5"/>
<path d="M10 13l-1.5 -1.5"/>
<path d="M7 16l-1.5 -1.5"/>
</svg>
<div class="icon-name">Линейка / замер</div>
</div>
<div class="icon-card" data-role="staff" data-name="user-cog" onclick="pick(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="#6B4A2B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"/>
<path d="M6 21v-2a4 4 0 0 1 4 -4h2.5"/>
<path d="M19.001 19a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/>
<path d="M19.001 15.5v1.5"/>
<path d="M19.001 21v1.5"/>
<path d="M22.032 17.25l-1.299 .75"/>
<path d="M17.27 20l-1.3 .75"/>
<path d="M15.97 17.25l1.3 .75"/>
<path d="M20.733 20l1.3 .75"/>
</svg>
<div class="icon-name">Сотрудник+настройки</div>
</div>
</div>
</div>
<!-- Статус-бар выбора -->
<div class="result-bar" id="resultBar">
<div>
<span id="selectionText">Выберите иконки для всех трёх ролей</span><br>
<span class="hint" id="selectionHint">Нажмите на карточку</span>
</div>
<div id="selectionIcons" style="display:flex; gap:24px; align-items:center;"></div>
</div>
<script>
const selected = { manager: null, client: null, staff: null };
const roleNames = { manager: 'Менеджер', client: 'Клиент', staff: 'Сотрудник' };
function pick(card) {
const role = card.dataset.role;
const name = card.dataset.name;
// Снять выбор с предыдущей карточки той же роли
document.querySelectorAll(`.icon-card[data-role="${role}"]`).forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selected[role] = name;
updateBar();
}
function updateBar() {
const bar = document.getElementById('resultBar');
const text = document.getElementById('selectionText');
const hint = document.getElementById('selectionHint');
const iconsDiv = document.getElementById('selectionIcons');
const filled = Object.entries(selected).filter(([, v]) => v !== null);
bar.classList.add('visible');
if (filled.length === 3) {
text.textContent = '✅ Все три роли выбраны!';
hint.innerHTML = Object.entries(selected).map(([role, name]) =>
`${roleNames[role]}: <code>${name}</code>`
).join(' · ');
} else {
text.textContent = `Выбрано ${filled.length} из 3`;
hint.textContent = filled.map(([role, name]) => `${roleNames[role]}: ${name}`).join(' · ') || 'Нажмите на карточку';
}
iconsDiv.innerHTML = filled.map(([role, name]) => {
const card = document.querySelector(`.icon-card[data-role="${role}"][data-name="${name}"]`);
return card ? `<div style="text-align:center"><div style="background:rgba(255,255,255,0.15);border-radius:8px;padding:6px">${card.querySelector('svg').outerHTML.replace(/width="\d+"/, 'width="36"').replace(/height="\d+"/, 'height="36"').replace(/stroke="#6B4A2B"/g, 'stroke="#FBF7F0"')}</div><div style="font-size:10px;color:#C4A882;margin-top:4px">${roleNames[role]}</div></div>` : '';
}).join('');
}
</script>
</body>
</html>