wasrusgen1-crm/docs/mockup_worker.html

1795 lines
111 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">
<title>@wasrusgen1 CRM — Работник</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Montserrat:wght@700;800&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#C8CACD;min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px;font-family:'Inter',sans-serif}
body[data-theme="zov"] {--accent:#003E7E;--accent2:#76BD22;--bg:#F5F6F8;--card:#FFFFFF;--ink:#1A1A2E;--muted:#8A94A6;--danger:#EF4444;--warn:#F59E0B;--success:#10B981}
body[data-theme="radar"] {--accent:#4338CA;--accent2:#6366F1;--bg:#F9FAFB;--card:#FFFFFF;--ink:#111827;--muted:#6B7280;--danger:#EF4444;--warn:#F59E0B;--success:#10B981}
body[data-theme="dark"] {--accent:#4338CA;--accent2:#6366F1;--bg:#111827;--card:#1F2937;--ink:#F9FAFB;--muted:#9CA3AF;--danger:#EF4444;--warn:#F59E0B;--success:#10B981}
#controls{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap;justify-content:center;width:100%;max-width:700px}
#controls label{color:#fff;font-size:13px;font-weight:600}
#screenSelect{padding:8px 12px;border-radius:10px;border:none;background:#fff;font-size:13px;color:#333;cursor:pointer;min-width:200px;box-shadow:0 2px 8px rgba(0,0,0,.15)}
#roleSelect{padding:8px 12px;border-radius:10px;border:none;background:#fff;font-size:13px;color:#333;cursor:pointer;min-width:220px;box-shadow:0 2px 8px rgba(0,0,0,.15)}
#themeButtons{display:flex;gap:6px}
.theme-btn{padding:7px 14px;border-radius:9px;border:2px solid transparent;font-size:12px;font-weight:700;cursor:pointer;transition:all .2s}
.theme-btn.active{border-color:#fff;transform:scale(1.05)}
.theme-btn[data-t="zov"] {background:#003E7E;color:#fff}
.theme-btn[data-t="radar"]{background:#4338CA;color:#fff}
.theme-btn[data-t="dark"] {background:#111827;color:#6366F1}
#phoneWrap{position:relative;width:390px;flex-shrink:0}
#phoneFrame{width:390px;height:844px;background:var(--bg);border-radius:44px;overflow:hidden;box-shadow:0 24px 80px rgba(0,0,0,.4),inset 0 0 0 1px rgba(255,255,255,.15);position:relative;display:flex;flex-direction:column}
#statusBar{height:44px;background:var(--card);display:flex;align-items:center;justify-content:space-between;padding:0 24px;flex-shrink:0;font-size:13px;font-weight:600;color:var(--ink);z-index:10}
.sb-right{display:flex;align-items:center;gap:6px}
#screen{flex:1;overflow-y:auto;overflow-x:hidden;position:relative;scrollbar-width:none;background:var(--bg)}
#screen::-webkit-scrollbar{display:none}
.bottom-nav{height:60px;background:rgba(255,255,255,.92);backdrop-filter:blur(12px);border-top:1px solid rgba(0,0,0,.06);display:flex;align-items:center;justify-content:space-around;flex-shrink:0;position:relative;z-index:100}
.nav-item{display:flex;flex-direction:column;align-items:center;gap:2px;cursor:pointer;padding:6px 10px;border-radius:10px;transition:all .15s;flex:1}
.nav-item svg{width:22px;height:22px;color:var(--muted)}
.nav-item span{font-size:10px;color:var(--muted);font-weight:500}
.nav-item.active svg{color:var(--accent)}
.nav-item.active span{color:var(--accent)}
.page{padding:0 0 80px;min-height:100%}
.page-header{display:flex;align-items:center;gap:12px;padding:16px 16px 12px;background:var(--card);border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:50}
.page-header h2{font-size:17px;font-weight:700;color:var(--ink);flex:1}
.back-btn{width:32px;height:32px;border-radius:50%;background:var(--bg);display:flex;align-items:center;justify-content:center;cursor:pointer;border:none;flex-shrink:0}
.back-btn svg{color:var(--accent);width:18px;height:18px}
.card{background:var(--card);border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,.07);padding:16px;margin-bottom:12px}
.card.warn-border{border-left:4px solid var(--warn)}
.card.success-border{border-left:4px solid var(--success)}
.card.accent-border{border-left:4px solid var(--accent)}
.card.danger-border{border-left:4px solid var(--danger)}
.section-label{text-transform:uppercase;font-size:11px;letter-spacing:.06em;color:var(--muted);margin:20px 16px 8px;font-weight:600}
.btn-primary{width:100%;background:var(--accent);color:#fff;border:none;border-radius:12px;padding:14px;font-size:15px;font-weight:600;cursor:pointer;transition:opacity .2s}
.btn-primary:hover{opacity:.9}
.btn-primary:disabled{opacity:.45;cursor:default}
.btn-secondary{width:100%;background:transparent;color:var(--accent);border:1.5px solid var(--accent);border-radius:12px;padding:13px;font-size:15px;font-weight:600;cursor:pointer;transition:all .2s;margin-top:8px}
.btn-sm{padding:8px 14px;font-size:13px;font-weight:600;border-radius:9px;cursor:pointer;border:none;background:var(--accent);color:#fff}
.badge{display:inline-flex;align-items:center;padding:4px 10px;border-radius:20px;font-size:12px;font-weight:600;letter-spacing:.02em}
.badge.blue{background:#DBEAFE;color:#1D4ED8}
.badge.yellow{background:#FEF3C7;color:#D97706}
.badge.green{background:#DCFCE7;color:#15803D}
.badge.red{background:#FEE2E2;color:#DC2626}
.badge.gray{background:#F1F5F9;color:#64748B}
.badge.accent{background:var(--accent);color:#fff}
.badge.teal{background:#CCFBF1;color:#0F766E}
.chip-row{display:flex;flex-wrap:wrap;gap:6px;padding:0 16px;margin-bottom:8px}
.chip{padding:5px 10px;border-radius:20px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;border:1.5px solid #E2E8F0;background:var(--card);color:var(--muted);transition:all .2s}
.chip.active{background:var(--accent);color:#fff;border-color:var(--accent)}
.progress-bar{height:6px;background:#E2E8F0;border-radius:3px;overflow:hidden;flex:1}
.progress-fill{height:100%;background:var(--accent);border-radius:3px}
.progress-fill.green{background:var(--success)}
.divider{height:1px;background:rgba(0,0,0,.07);margin:8px 0}
.row{display:flex;align-items:center;gap:10px}
.row.sb{justify-content:space-between}
.col{display:flex;flex-direction:column;gap:4px}
.avatar{width:40px;height:40px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;color:#fff;font-size:15px;font-weight:700;flex-shrink:0}
.avatar.lg{width:64px;height:64px;font-size:22px}
.checklist-item{display:flex;align-items:flex-start;gap:12px;padding:12px 0;border-bottom:1px solid rgba(0,0,0,.06)}
.checklist-item:last-child{border-bottom:none}
.check-box{width:24px;height:24px;border-radius:6px;border:2px solid #CBD5E1;display:flex;align-items:center;justify-content:center;flex-shrink:0;cursor:pointer;transition:all .2s;margin-top:1px}
.check-box.done{background:var(--success);border-color:var(--success)}
.check-box.done svg{display:block}
.check-box svg{display:none;color:#fff;width:14px;height:14px}
.check-text{flex:1}
.check-text h4{font-size:14px;font-weight:600;color:var(--ink);line-height:1.3}
.check-text.done h4{text-decoration:line-through;color:var(--muted)}
.photo-slot{width:80px;height:80px;border-radius:12px;background:#F1F5F9;border:2px dashed #CBD5E1;display:flex;align-items:center;justify-content:center;flex-shrink:0;cursor:pointer}
.photo-slot svg{color:var(--muted);width:24px;height:24px}
.photo-slot.filled{background:linear-gradient(135deg,#E2E8F0,#CBD5E1);border:none;font-size:28px}
.info-row{display:flex;gap:8px;padding:10px 0;border-bottom:1px solid rgba(0,0,0,.06)}
.info-row:last-child{border-bottom:none}
.info-label{font-size:12px;color:var(--muted);font-weight:500;width:130px;flex-shrink:0}
.info-val{font-size:13px;font-weight:600;color:var(--ink);flex:1}
.earnings-card{background:linear-gradient(135deg,var(--accent),var(--accent2));border-radius:16px;padding:20px;color:#fff;margin-bottom:12px}
.stat-table{width:100%;border-collapse:collapse;font-size:13px}
.stat-table th{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-weight:600;padding:6px 0;border-bottom:1px solid rgba(0,0,0,.08);text-align:left}
.stat-table td{padding:8px 0;border-bottom:1px solid rgba(0,0,0,.05);color:var(--ink);font-size:13px}
.stat-table tr:last-child td{border-bottom:none}
/* measurer-specific */
.dim-row{display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid rgba(0,0,0,.06)}
.dim-row:last-child{border-bottom:none}
.dim-label{font-size:13px;font-weight:500;color:var(--ink);flex:1}
.dim-input-wrap{display:flex;align-items:center;gap:6px}
.dim-input{width:72px;padding:8px 10px;border:1.5px solid #E2E8F0;border-radius:10px;font-size:15px;font-weight:600;color:var(--ink);background:var(--card);text-align:right;outline:none}
.dim-unit{font-size:12px;color:var(--muted);font-weight:500}
.step-progress{display:flex;align-items:center;gap:0;padding:16px 16px 0}
.step-item{display:flex;flex-direction:column;align-items:center;flex:1;position:relative}
.step-item:not(:last-child)::after{content:'';position:absolute;top:12px;left:50%;width:100%;height:2px;background:#E2E8F0;z-index:0}
.step-item.done::after{background:var(--success)}
.step-item.active::after{background:var(--accent)}
.step-dot{width:24px;height:24px;border-radius:50%;border:2px solid #E2E8F0;background:var(--card);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--muted);z-index:1;position:relative}
.step-item.done .step-dot{background:var(--success);border-color:var(--success);color:#fff}
.step-item.active .step-dot{background:var(--accent);border-color:var(--accent);color:#fff}
.step-name{font-size:10px;color:var(--muted);margin-top:4px;font-weight:500;text-align:center}
.step-item.active .step-name{color:var(--accent);font-weight:700}
.step-item.done .step-name{color:var(--success)}
/* dark overrides */
body[data-theme="dark"] .chip{background:#1F2937;border-color:#374151;color:#9CA3AF}
body[data-theme="dark"] .progress-bar{background:#374151}
body[data-theme="dark"] .page-header{background:var(--card);border-bottom:1px solid rgba(255,255,255,.08)}
body[data-theme="dark"] .bottom-nav{background:rgba(17,24,39,.95);border-color:rgba(255,255,255,.08)}
body[data-theme="dark"] #statusBar{background:var(--card)}
body[data-theme="dark"] .back-btn{background:#374151}
body[data-theme="dark"] .dim-input{background:#374151;border-color:#4B5563;color:#F9FAFB}
body[data-theme="dark"] .checklist-item{border-color:#374151}
body[data-theme="dark"] .photo-slot{background:#374151;border-color:#4B5563}
body[data-theme="dark"] .info-row{border-color:#374151}
body[data-theme="dark"] .stat-table th{border-color:#374151}
body[data-theme="dark"] .stat-table td{border-color:#374151}
body[data-theme="dark"] .step-item::after{background:#374151}
body[data-theme="dark"] .step-dot{background:#1F2937;border-color:#374151}
body[data-theme="dark"] .dim-row{border-color:#374151}
body[data-theme="dark"] .badge.gray{background:#374151;color:#9CA3AF}
@keyframes slideFade{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
.page{animation:slideFade .18s ease-out}
</style>
</head>
<body data-theme="zov">
<div id="crm-back-nav" style="position:fixed;top:0;left:0;right:0;z-index:9999;background:rgba(255,255,255,0.92);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border-bottom:1px solid rgba(0,0,0,.08);padding:8px 16px;display:flex;align-items:center">
<a href="https://wasrusgen.github.io/wasrusgen1-crm/" style="display:inline-flex;align-items:center;gap:6px;font-family:Inter,system-ui,sans-serif;font-size:13px;font-weight:600;color:#003E7E;text-decoration:none;padding:4px 12px;border-radius:8px;background:#F0F4FF;transition:background .15s" onmouseover="this.style.background='#DDE8FF'" onmouseout="this.style.background='#F0F4FF'">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 5l-7 7 7 7"/></svg>
Все кабинеты
</a>
<span style="margin-left:12px;font-family:Inter,system-ui,sans-serif;font-size:12px;color:#8A94A6">@wasrusgen1 CRM</span>
</div>
<div style="height:40px"></div>
<div id="controls">
<div>
<label>Роль:</label><br>
<select id="roleSelect" onchange="setRole(this.value)">
<option value="both">Оба роли (замерщик + сборщик)</option>
<option value="measurer">Только замерщик</option>
<option value="assembler">Только сборщик</option>
</select>
</div>
<div>
<label>Экран:</label><br>
<select id="screenSelect" onchange="navigate(this.value)">
<option value="home">Сегодня</option>
<option value="measurements">Мои замеры</option>
<option value="measurement_detail">Карточка замера</option>
<option value="assemblies">Мои сборки</option>
<option value="assembly_detail">Карточка сборки</option>
<option value="finances">Финансы</option>
<option value="profile">Профиль</option>
</select>
</div>
<div>
<label>Тема:</label><br>
<div id="themeButtons">
<button class="theme-btn active" data-t="zov" onclick="setTheme('zov',this)">Синяя</button>
<button class="theme-btn" data-t="radar" onclick="setTheme('radar',this)">Radar</button>
<button class="theme-btn" data-t="dark" onclick="setTheme('dark',this)">Dark</button>
</div>
</div>
</div>
<div id="phoneWrap">
<div id="phoneFrame">
<div id="statusBar">
<span>9:41</span>
<div class="sb-right">
<svg width="16" height="12" fill="currentColor" viewBox="0 0 16 12"><rect x="0" y="3" width="3" height="9" rx="1"/><rect x="4.5" y="2" width="3" height="10" rx="1"/><rect x="9" y="0" width="3" height="12" rx="1"/><rect x="13.5" y="1" width="2.5" height="11" rx="1" opacity=".3"/></svg>
<svg width="16" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12.55a11 11 0 0114.08 0M1.42 9a16 16 0 0121.16 0M8.53 16.11a6 6 0 016.95 0M12 20h.01"/></svg>
<span>100%</span>
</div>
</div>
<div id="screen"></div>
<div id="bottomNav"></div>
</div>
</div>
<script>
window._workerRole = 'both';
var _currentScreen = 'home';
var SCREENS = {
home: 'Сегодня',
measurements: 'Мои замеры',
measurement_detail: 'Карточка замера',
assemblies: 'Мои сборки',
assembly_detail: 'Карточка сборки',
calendar: 'Маршрут',
finances: 'Финансы',
profile: 'Профиль',
measure_calc: 'Расчёт стоимости'
};
function setTheme(t, btn) {
document.body.setAttribute('data-theme', t);
document.querySelectorAll('.theme-btn').forEach(function(b){ b.classList.remove('active'); });
btn.classList.add('active');
}
function setRole(r) {
window._workerRole = r;
navigate('home');
}
function navigate(id) {
_currentScreen = id;
var sel = document.getElementById('screenSelect');
if (sel) sel.value = id;
document.getElementById('screen').innerHTML = renderScreen(id);
document.getElementById('bottomNav').innerHTML = navBar(id);
document.getElementById('screen').scrollTop = 0;
}
// ── АВТО-ОТЧЁТ: вызывается когда чек-лист закрыт полностью ──────────────────
function _ergoComplete(total) {
if (window._reportSent) return;
window._reportSent = true;
// Шапка чеклиста → зелёная
var lbl = document.getElementById('ergo-progress-label');
if (lbl) {
lbl.textContent = 'Кухня · Замер завершён ✓';
lbl.style.color = '#16A34A';
lbl.style.fontWeight = '700';
}
var hdrWrap = lbl && lbl.closest ? lbl.closest('[onclick]') : null;
if (hdrWrap) hdrWrap.style.background = 'rgba(34,197,94,.06)';
// Toast — «Отчёт отправлен»
var old = document.getElementById('_report-toast');
if (old) old.parentNode.removeChild(old);
var toast = document.createElement('div');
toast.id = '_report-toast';
toast.innerHTML =
'<div style="font-size:20px;line-height:1">📤</div>'
+ '<div>'
+ '<div style="font-weight:700;font-size:14px;color:#fff">Отчёт отправлен менеджеру</div>'
+ '<div style="font-size:12px;color:rgba(255,255,255,.75);margin-top:2px">Кухня · 9/9 · ' + new Date().toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'}) + '</div>'
+ '</div>'
+ '<div style="font-size:18px">✓</div>';
toast.style.cssText = [
'position:fixed','bottom:90px','left:50%','transform:translateX(-50%) translateY(120px)',
'background:linear-gradient(135deg,#16A34A,#15803D)',
'color:#fff','border-radius:16px','padding:14px 18px',
'display:flex','align-items:center','gap:12px',
'box-shadow:0 8px 32px rgba(22,163,74,.4)',
'z-index:9999','min-width:260px','max-width:340px',
'transition:transform .4s cubic-bezier(.34,1.56,.64,1)'
].join(';');
document.body.appendChild(toast);
// Slide in
requestAnimationFrame(function(){ requestAnimationFrame(function(){
toast.style.transform = 'translateX(-50%) translateY(0)';
}); });
// Slide out после 3.5 сек
setTimeout(function(){
toast.style.transform = 'translateX(-50%) translateY(120px)';
setTimeout(function(){ if (toast.parentNode) toast.parentNode.removeChild(toast); }, 400);
}, 3500);
// Пульс на кнопке «Расчёт и оплата»
setTimeout(function(){
var btn = document.querySelector('[onclick*="measure_calc"]');
if (btn) {
btn.style.animation = 'none';
btn.style.boxShadow = '0 0 0 0 rgba(99,102,241,.6)';
btn.style.animation = '_ergoPulse 1s ease 3';
}
}, 400);
}
// Keyframes для пульса (добавляются один раз)
(function(){
if (!document.getElementById('_ergo-keyframes')) {
var s = document.createElement('style');
s.id = '_ergo-keyframes';
s.textContent = '@keyframes _ergoPulse{0%{box-shadow:0 0 0 0 rgba(99,102,241,.6)}70%{box-shadow:0 0 0 12px rgba(99,102,241,0)}100%{box-shadow:0 0 0 0 rgba(99,102,241,0)}}';
document.head.appendChild(s);
}
})();
// ── АВТО-УВЕДОМЛЕНИЕ: фото замера загружено ─────────────────────────────────
function _photoUploaded(slot) {
if (window._photoNotified) return;
window._photoNotified = true;
// Слот → показываем «uploaded» состояние
if (slot) {
slot.style.background = 'rgba(99,102,241,.12)';
slot.style.borderColor = 'var(--accent)';
}
// Toast
var old = document.getElementById('_photo-toast');
if (old) old.parentNode.removeChild(old);
var t = new Date().toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'});
var toast = document.createElement('div');
toast.id = '_photo-toast';
toast.innerHTML =
'<div style="font-size:22px;line-height:1">📐</div>'
+ '<div style="flex:1">'
+ '<div style="font-weight:700;font-size:13px;color:#fff;margin-bottom:6px">Замер загружен · Кухня · ' + t + '</div>'
+ '<div style="display:flex;flex-direction:column;gap:3px">'
+ '<div style="font-size:12px;color:rgba(255,255,255,.9);display:flex;align-items:center;gap:5px"><span style="font-size:10px;background:rgba(255,255,255,.2);border-radius:4px;padding:1px 5px">✓</span>Менеджер уведомлён</div>'
+ '<div style="font-size:12px;color:rgba(255,255,255,.9);display:flex;align-items:center;gap:5px"><span style="font-size:10px;background:rgba(255,255,255,.2);border-radius:4px;padding:1px 5px">✓</span>Клиент получил статус в кабинете</div>'
+ '</div>'
+ '</div>';
toast.style.cssText = [
'position:fixed','bottom:90px','left:50%','transform:translateX(-50%) translateY(120px)',
'background:linear-gradient(135deg,#6366F1,#4F46E5)',
'color:#fff','border-radius:16px','padding:14px 18px',
'display:flex','align-items:flex-start','gap:12px',
'box-shadow:0 8px 32px rgba(99,102,241,.4)',
'z-index:9999','min-width:280px','max-width:340px',
'transition:transform .4s cubic-bezier(.34,1.56,.64,1)'
].join(';');
document.body.appendChild(toast);
requestAnimationFrame(function(){ requestAnimationFrame(function(){
toast.style.transform = 'translateX(-50%) translateY(0)';
}); });
setTimeout(function(){
toast.style.transform = 'translateX(-50%) translateY(120px)';
setTimeout(function(){ if (toast.parentNode) toast.parentNode.removeChild(toast); }, 400);
}, 3500);
}
// ── ФИКСАЦИЯ доп. помещения добавленного на объекте ─────────────────────────
function _showExtraRoomToast(roomName) {
var old = document.getElementById('_extra-room-toast');
if (old) old.parentNode.removeChild(old);
var toast = document.createElement('div');
toast.id = '_extra-room-toast';
toast.innerHTML =
'<div style="font-size:22px;line-height:1">🏠</div>'
+ '<div style="flex:1">'
+ '<div style="font-weight:700;font-size:13px;color:#fff;margin-bottom:4px">Доп. помещение: ' + roomName + '</div>'
+ '<div style="font-size:11px;color:rgba(255,255,255,.85);line-height:1.5">'
+ 'Зафиксировано как дополнительный заказ на объекте'
+ '</div>'
+ '</div>';
toast.style.cssText = [
'position:fixed','bottom:90px','left:50%','transform:translateX(-50%) translateY(120px)',
'background:linear-gradient(135deg,#B45309,#92400E)',
'color:#fff','border-radius:16px','padding:14px 18px',
'display:flex','align-items:flex-start','gap:12px',
'box-shadow:0 8px 32px rgba(180,83,9,.35)',
'z-index:9999','min-width:280px','max-width:340px',
'transition:transform .4s cubic-bezier(.34,1.56,.64,1)'
].join(';');
document.body.appendChild(toast);
requestAnimationFrame(function(){ requestAnimationFrame(function(){
toast.style.transform = 'translateX(-50%) translateY(0)';
}); });
setTimeout(function(){
toast.style.transform = 'translateX(-50%) translateY(120px)';
setTimeout(function(){ if (toast.parentNode) toast.parentNode.removeChild(toast); }, 400);
}, 4000);
}
function navBar(active) {
var role = window._workerRole || 'both';
var items = [];
items.push({id:'home', label:'Главная', icon:'<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>'});
items.push({id:'calendar', label:'Маршрут', icon:'<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>'});
if (role === 'both' || role === 'measurer') {
items.push({id:'measurements', label:'Замеры', icon:'<polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>'});
}
if (role === 'both' || role === 'assembler') {
items.push({id:'assemblies', label:'Сборки', icon:'<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>'});
}
items.push({id:'profile', label:'Профиль', icon:'<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>'});
var activeId = (active === 'measurement_detail') ? 'measurements' : (active === 'assembly_detail') ? 'assemblies' : active;
return '<div class="bottom-nav">' + items.map(function(item){
var cls = (item.id === activeId) ? 'nav-item active' : 'nav-item';
return '<div class="' + cls + '" onclick="navigate(\'' + item.id + '\')">' +
'<svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">' + item.icon + '</svg>' +
'<span>' + item.label + '</span>' +
'</div>';
}).join('') + '</div>';
}
function renderScreen(id) {
switch(id) {
case 'home': return screenHome();
case 'calendar': return screenCalendar();
case 'measurements': return screenMeasurements();
case 'measurement_detail': return screenMeasurementDetail();
case 'measure_calc': return screenMeasureCalc();
case 'assemblies': return screenAssemblies();
case 'assembly_detail': return screenAssemblyDetail();
case 'finances': return screenFinances();
case 'profile': return screenProfile();
default: return screenHome();
}
}
function roleBadges() {
var role = window._workerRole;
var out = '';
if (role === 'both' || role === 'measurer') out += '<span class="badge blue" style="font-size:11px;padding:3px 8px">Замерщик</span> ';
if (role === 'both' || role === 'assembler') out += '<span class="badge teal" style="font-size:11px;padding:3px 8px">Сборщик</span>';
return out;
}
/* ─────────── SCREEN: HOME ─────────── */
function screenHome() {
var role = window._workerRole || 'both';
var measSection = '';
if (role === 'both' || role === 'measurer') {
measSection = `
<div class="section-label">Замеры сегодня</div>
<div style="padding:0 16px">
<div class="card accent-border" style="cursor:pointer" onclick="navigate('measurement_detail')">
<div class="row sb" style="margin-bottom:6px">
<span style="font-size:13px;font-weight:700;color:var(--ink)">09:00 · Ленина 12, кв.45</span>
<span class="badge blue">Новый</span>
</div>
<div style="font-size:13px;color:var(--muted)">Кухня угловая</div>
<div style="font-size:12px;color:var(--muted);margin-top:4px;display:flex;align-items:center;gap:4px">
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
1 этаж, лифт есть
</div>
</div>
<div class="card danger-border" style="cursor:pointer" onclick="navigate('measurement_detail')">
<div class="row sb" style="margin-bottom:6px">
<span style="font-size:13px;font-weight:700;color:var(--ink)">14:30 · Мира 7, кв.18</span>
<span class="badge red">Срочно</span>
</div>
<div style="font-size:13px;color:var(--muted)">Гардероб</div>
<div style="font-size:12px;color:var(--muted);margin-top:4px;display:flex;align-items:center;gap:4px">
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
3 этаж, без лифта
</div>
</div>
</div>`;
}
var asmSection = '';
if (role === 'both' || role === 'assembler') {
asmSection = `
<div class="section-label">Сборки сегодня</div>
<div style="padding:0 16px">
<div class="card warn-border" style="cursor:pointer" onclick="navigate('assembly_detail')">
<div class="row sb" style="margin-bottom:6px">
<span style="font-size:13px;font-weight:700;color:var(--ink)">#А-2847 · Черниговская 14</span>
<span class="badge yellow">В работе</span>
</div>
<div style="font-size:13px;color:var(--muted)">Кухня · 11:00</div>
<div style="display:flex;align-items:center;gap:6px;margin-top:8px">
<div class="progress-bar" style="flex:1"><div class="progress-fill" style="width:57%"></div></div>
<span style="font-size:11px;color:var(--muted)">4/7</span>
</div>
</div>
</div>`;
}
return `<div class="page">
<div style="background:var(--card);padding:20px 16px 16px;border-bottom:1px solid rgba(0,0,0,.06)">
<div class="row sb" style="margin-bottom:8px">
<div class="col">
<div style="font-size:20px;font-weight:800;color:var(--ink)">Кириллов А.В.</div>
<div style="font-size:13px;color:var(--muted);margin-top:2px">Пн, 23 мая</div>
</div>
<div class="avatar lg">КА</div>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap">${roleBadges()}</div>
</div>
${measSection}
${asmSection}
<div class="section-label">Мой заработок · Май</div>
<div style="padding:0 16px">
<div class="earnings-card">
<div style="font-size:13px;opacity:.8;margin-bottom:6px;font-weight:500">Заработано</div>
<div style="font-size:30px;font-weight:800;letter-spacing:-.5px;margin-bottom:4px">28 400 ₽</div>
<div style="font-size:12px;opacity:.75;margin-bottom:12px">план 40 000 ₽</div>
<div class="row" style="gap:10px;margin-bottom:6px">
<div class="progress-bar" style="background:rgba(255,255,255,.25)">
<div class="progress-fill" style="width:71%;background:rgba(255,255,255,.9)"></div>
</div>
<span style="font-size:12px;font-weight:700;opacity:.9">71%</span>
</div>
<div style="font-size:12px;opacity:.75">17 завершено</div>
</div>
</div>
</div>`;
}
/* ─────────── SCREEN: MEASUREMENTS ─────────── */
function screenMeasurements() {
var data = [
{time:'09:00', addr:'Ленина 12, кв.45', type:'Кухня угловая', status:'Новый', statusCls:'blue'},
{time:'14:30', addr:'Мира 7, кв.18', type:'Гардероб', status:'Срочно', statusCls:'red'},
{time:'вчера', addr:'Садовая 4, кв.12', type:'Прихожая', status:'Выполнен', statusCls:'green'},
{time:'21 мая', addr:'Юбилейная 33', type:'Кухня прямая', status:'Выполнен', statusCls:'green'},
{time:'20 мая', addr:'Пушкина 2, кв.8', type:'Гардероб', status:'В пути', statusCls:'yellow'},
];
return `<div class="page">
<div class="page-header"><h2>Мои замеры</h2></div>
<div style="height:12px"></div>
<div class="chip-row">
<div class="chip active">Все</div>
<div class="chip">Сегодня</div>
<div class="chip">Ожидают</div>
<div class="chip">Завершены</div>
</div>
<div style="padding:4px 16px 0">
${data.map(function(d){
return `<div class="card" style="cursor:pointer;margin-bottom:10px" onclick="navigate('measurement_detail')">
<div class="row sb" style="margin-bottom:6px">
<span style="font-size:13px;font-weight:700;color:var(--ink)">${d.time} · ${d.addr}</span>
<span class="badge ${d.statusCls}">${d.status}</span>
</div>
<div class="row sb">
<span style="font-size:13px;color:var(--muted)">${d.type}</span>
<svg width="16" height="16" fill="none" stroke="var(--muted)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>`;
}).join('')}
</div>
</div>`;
}
/* ─────────── SCREEN: MEASUREMENT DETAIL ─────────── */
function screenMeasurementDetail() {
// ── Состояние помещений ──────────────────────────────────────────────────
if (!window._rooms) {
window._rooms = [
{ name:'Кухня', ergoChecked:{}, photoDone:true, measureDone:true,
projectFiles:[{name:'Визуализация_кухня.jpg',size:'2.1 МБ',date:'04.05.2026'},{name:'Спецификация.pdf',size:'340 КБ',date:'04.05.2026'}] },
{ name:'Коридор', ergoChecked:{}, photoDone:false, measureDone:false,
projectFiles:[] },
];
}
if (window._activeRoom === undefined) window._activeRoom = 0;
var rooms = window._rooms;
var ri = window._activeRoom;
var cur = rooms[ri];
var roomName = cur.name;
// Статус помещения: 'done' | 'active' | 'empty'
function roomStatus(r) {
var cnt = Object.values(r.ergoChecked).filter(Boolean).length;
var ergoTotal = (ROOM_ERGO[r.name] || ROOM_ERGO['Кухня']).params.length;
if (cnt === ergoTotal && r.measureDone) return 'done';
if (cnt > 0 || r.photoDone || r.measureDone) return 'active';
return 'empty';
}
// ── Таб-помещения ────────────────────────────────────────────────────────
var ROOM_PRESETS = ['Гостиная','Спальня','Прихожая','Ванная','Санузел','Балкон','Детская','Гардероб'];
var tabsHtml = '<div style="display:flex;gap:6px;overflow-x:auto;padding:0 16px 12px;scrollbar-width:none;-ms-overflow-style:none">';
rooms.forEach(function(r, idx) {
var st = roomStatus(r);
var isActive = idx === ri;
var badge;
if (st === 'done') {
badge = isActive
? '<span style="font-size:11px;margin-left:5px;opacity:.85">✓</span>'
: '<span style="font-size:10px;font-weight:700;color:#16A34A;background:rgba(34,197,94,.12);border-radius:10px;padding:1px 6px;margin-left:5px">✓ Готово</span>';
} else if (st === 'active') {
badge = '<span style="width:7px;height:7px;border-radius:50%;background:'+(isActive?'rgba(255,255,255,.7)':'#6366F1')+';display:inline-block;margin-right:4px;flex-shrink:0"></span>';
} else {
badge = '<span style="width:7px;height:7px;border-radius:50%;background:'+(isActive?'rgba(255,255,255,.4)':'rgba(0,0,0,.18)')+';display:inline-block;margin-right:4px;flex-shrink:0"></span>';
}
// Бейдж «на объекте» для доп. помещений
var extraBadge = r.addedOnSite
? '<span style="font-size:9px;font-weight:700;background:'+(isActive?'rgba(255,255,255,.2)':'rgba(245,158,11,.15)')+';color:'+(isActive?'#fff':'#B45309')+';border-radius:8px;padding:1px 5px;margin-left:5px;letter-spacing:.02em">+ доп.</span>'
: '';
var bg = isActive ? (st==='done' ? '#16A34A' : 'var(--accent)') : 'var(--card)';
var clr = isActive ? '#fff' : 'var(--ink)';
var bdr = isActive ? 'none' : (st==='done' ? '1.5px solid rgba(34,197,94,.3)' : r.addedOnSite ? '1.5px solid rgba(245,158,11,.35)' : '1.5px solid rgba(0,0,0,.08)');
tabsHtml += '<button onclick="(function(){window._activeRoom='+idx+';window._ergoOpen=false;'
+ 'document.getElementById(\'screen\').innerHTML=renderScreen(\'measurement_detail\')})()"'
+ ' style="display:flex;align-items:center;white-space:nowrap;padding:7px 13px;border-radius:20px;border:'+bdr+';background:'+bg+';color:'+clr+';font-size:13px;font-weight:600;cursor:pointer;flex-shrink:0">'
+ (st !== 'done' ? badge : '')
+ r.name
+ (st === 'done' ? badge : '')
+ extraBadge
+ '</button>';
});
// Кнопка «+ Помещение»
tabsHtml += '<button onclick="(function(){'
+ 'var presets='+JSON.stringify(ROOM_PRESETS)+';'
+ 'var used=window._rooms.map(function(r){return r.name});'
+ 'var avail=presets.filter(function(p){return used.indexOf(p)<0});'
+ 'var name=avail.length?avail[0]:\'Помещение \'+( window._rooms.length+1);'
+ 'window._rooms.push({name:name,ergoChecked:{},photoDone:false,measureDone:false,projectFiles:[],addedOnSite:true});'
+ 'window._activeRoom=window._rooms.length-1;window._ergoOpen=false;'
+ 'document.getElementById(\'screen\').innerHTML=renderScreen(\'measurement_detail\');'
+ '_showExtraRoomToast(name);'
+ '})()"'
+ ' style="display:flex;align-items:center;gap:4px;white-space:nowrap;padding:7px 13px;border-radius:20px;border:1.5px dashed rgba(99,102,241,.4);background:rgba(99,102,241,.06);color:var(--accent);font-size:13px;font-weight:600;cursor:pointer;flex-shrink:0">'
+ '+ Помещение</button>';
tabsHtml += '</div>';
// ── Чеклист текущего помещения ───────────────────────────────────────────
var ergoRoomData = ROOM_ERGO[roomName] || ROOM_ERGO['Кухня'];
var total = ergoRoomData.params.length;
var ergoChecked = cur.ergoChecked;
var doneCnt = Object.values(ergoChecked).filter(Boolean).length;
var ergoBody = ergoRoomData.params.map(function(p, i){
var key = 'p'+i;
var done = !!ergoChecked[key];
return '<div onclick="(function(){'
+ 'var r=window._rooms[window._activeRoom];'
+ 'if(!r.ergoChecked)r.ergoChecked={};'
+ 'r.ergoChecked[\''+key+'\']=!r.ergoChecked[\''+key+'\'];'
+ 'var done=r.ergoChecked[\''+key+'\'];'
+ 'var row=document.getElementById(\'ep'+i+'\');'
+ 'var cb=document.getElementById(\'ec'+i+'\');'
+ 'row.style.opacity=done?\'0.45\':\'1\';'
+ 'cb.style.background=done?\'var(--accent)\':(\''+( p.critical ? 'rgba(239,68,68,.12)' : 'rgba(0,0,0,.06)' )+'\');'
+ 'cb.style.borderColor=done?\'var(--accent)\':(\''+( p.critical ? 'var(--danger)' : 'rgba(0,0,0,.2)' )+'\');'
+ 'cb.innerHTML=done?\'<svg width=&quot;10&quot; height=&quot;10&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;#fff&quot; stroke-width=&quot;3.5&quot;><polyline points=&quot;20 6 9 17 4 12&quot;/></svg>\':\'\';'
+ 'var cnt=Object.values(r.ergoChecked).filter(Boolean).length;'
+ 'var lbl=document.getElementById(\'ergo-progress-label\');'
+ 'if(lbl)lbl.textContent=r.name+\' · \'+cnt+\' из '+total+'\';'
+ 'if(cnt==='+total+')_ergoComplete('+total+');'
+ '})()"'
+ ' id="ep'+i+'" style="display:flex;align-items:flex-start;gap:10px;padding:9px 14px;border-bottom:1px solid rgba(0,0,0,.04);cursor:pointer;transition:.15s;opacity:'+( done?'0.45':'1' )+'">'
+ '<div id="ec'+i+'" style="width:18px;height:18px;border-radius:5px;border:1.5px solid '+( p.critical?'var(--danger)':'rgba(0,0,0,.2)' )+';background:'+( done?'var(--accent)':( p.critical?'rgba(239,68,68,.12)':'rgba(0,0,0,.06)' ) )+';flex-shrink:0;margin-top:1px;display:flex;align-items:center;justify-content:center">'
+ (done ? '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3.5"><polyline points="20 6 9 17 4 12"/></svg>' : '')
+ '</div>'
+ '<div style="flex:1">'
+ '<div style="font-size:13px;font-weight:'+(p.critical?'700':'500')+';color:'+(p.critical?'var(--danger)':'var(--ink)')+'">'+p.label+'</div>'
+ '<div style="font-size:11px;color:var(--muted);margin-top:1px">'+p.hint+'</div>'
+ '</div>'
+ '</div>';
}).join('');
var ergoTips = '<div style="padding:10px 14px;background:rgba(99,102,241,.04)">'
+ ergoRoomData.tips.map(function(t){ return '<div style="font-size:12px;color:var(--muted);margin-bottom:4px;line-height:1.5">💡 '+t+'</div>'; }).join('')
+ '<div style="font-size:10px;color:rgba(0,0,0,.25);margin-top:6px">'+ergoRoomData.source+'</div>'
+ '</div>';
// ── Фото замера (measure slot) ───────────────────────────────────────────
var measureSlot = cur.measureDone
? '<div style="aspect-ratio:1;border-radius:10px;overflow:hidden;position:relative;background:#E0E7FF;cursor:pointer">'
+ '<div style="width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px">'
+ '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="rgba(99,102,241,.6)" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><circle cx="7.5" cy="6" r=".5" fill="rgba(99,102,241,.6)"/><path d="M7 15l3-3 2 2 3-4 3 5H7z"/></svg>'
+ '<span style="font-size:9px;color:rgba(99,102,241,.7);font-weight:600">замер.jpg</span>'
+ '</div>'
+ '<div style="position:absolute;top:4px;right:4px;width:16px;height:16px;background:rgba(0,0,0,.25);border-radius:50%;display:flex;align-items:center;justify-content:center">'
+ '<svg width="8" height="8" viewBox="0 0 10 10" fill="white"><path d="M1 1l8 8M9 1L1 9" stroke="white" stroke-width="1.5"/></svg>'
+ '</div></div>'
: '<div class="photo-slot" onclick="(function(){window._rooms[window._activeRoom].measureDone=true;_photoUploaded(this);document.getElementById(\'screen\').innerHTML=renderScreen(\'measurement_detail\')})()">'
+ '<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>'
+ '</div>';
return `<div class="page">
<div class="page-header">
<button class="back-btn" onclick="navigate('measurements')">
<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<h2>Карточка замера</h2>
</div>
<div class="step-progress" style="padding-bottom:16px">
<div class="step-item done">
<div class="step-dot">
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="step-name">Выезд</div>
</div>
<div class="step-item active">
<div class="step-dot">2</div>
<div class="step-name">Замер</div>
</div>
<div class="step-item">
<div class="step-dot">3</div>
<div class="step-name">Отчёт</div>
</div>
</div>
<div style="padding:0 16px">
<div class="card accent-border">
<div style="font-size:15px;font-weight:700;color:var(--ink);margin-bottom:10px">Иванова М.С.</div>
<div class="info-row">
<div class="info-label">Телефон</div>
<div class="info-val" style="color:var(--accent)">+7 (900) 123-45-67</div>
</div>
<div class="info-row">
<div class="info-label">Адрес</div>
<div class="info-val">Ленина 12, кв.45</div>
</div>
<div class="info-row">
<div class="info-label">Этаж</div>
<div class="info-val">1 (лифт есть)</div>
</div>
<div class="info-row">
<div class="info-label">Примечания</div>
<div class="info-val" style="font-weight:400;font-size:13px;color:var(--muted)">Старая кухня демонтирована, место свободно</div>
</div>
</div>
</div>
<!-- Таб-помещения -->
${tabsHtml}
<div style="padding:0 16px">
<!-- Проект от менеджера — привязан к помещению -->
${(function(){
function fileIcon(name) {
var ext = (name.split('.').pop()||'').toLowerCase();
if (['jpg','jpeg','png','webp','heic','gif'].indexOf(ext)>=0) return {icon:'🖼️', bg:'linear-gradient(135deg,#e0e7ff,#c7d2fe)'};
if (ext==='pdf') return {icon:'📄', bg:'linear-gradient(135deg,#fee2e2,#fecaca)'};
if (['dwg','dxf','skp'].indexOf(ext)>=0) return {icon:'📐', bg:'linear-gradient(135deg,#d1fae5,#a7f3d0)'};
if (['doc','docx'].indexOf(ext)>=0) return {icon:'📝', bg:'linear-gradient(135deg,#dbeafe,#bfdbfe)'};
if (['xls','xlsx'].indexOf(ext)>=0) return {icon:'📊', bg:'linear-gradient(135deg,#dcfce7,#bbf7d0)'};
if (['zip','rar'].indexOf(ext)>=0) return {icon:'🗜️', bg:'linear-gradient(135deg,#fef9c3,#fef08a)'};
return {icon:'📎', bg:'linear-gradient(135deg,#f3f4f6,#e5e7eb)'};
}
var files = cur.projectFiles || [];
var header = '<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:0 0 8px">Проект от менеджера · '+roomName+'</div>';
if (!files.length) {
return header
+ '<div class="card" style="padding:12px 14px;display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">'
+ '<div style="font-size:13px;color:var(--muted)">Файлы не прикреплены</div>'
+ '</div>';
}
return header + files.map(function(f){
var fi = fileIcon(f.name);
return '<div class="card" style="padding:10px 12px;display:flex;align-items:center;gap:10px;margin-bottom:6px">'
+ '<div style="width:40px;height:40px;border-radius:9px;background:'+fi.bg+';display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0">'+fi.icon+'</div>'
+ '<div style="flex:1;min-width:0">'
+ '<div style="font-size:13px;font-weight:700;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+f.name+'</div>'
+ '<div style="font-size:11px;color:var(--muted);margin-top:1px">'+f.date+' · '+f.size+'</div>'
+ '</div>'
+ '<button style="background:rgba(99,102,241,.1);border:none;border-radius:8px;padding:5px 10px;font-size:12px;color:var(--accent);font-weight:700;cursor:pointer;flex-shrink:0">↗</button>'
+ '</div>';
}).join('');
})()}
<!-- Кабинет помещения: чеклист -->
<div class="card" style="padding:0;overflow:hidden;margin-bottom:4px">
<div onclick="(function(){window._ergoOpen=!window._ergoOpen;var b=document.getElementById('ergo-body');var i=document.getElementById('ergo-chev');if(b){b.style.display=window._ergoOpen?'block':'none';i.style.transform=window._ergoOpen?'rotate(180deg)':''}})()"
style="padding:12px 14px;cursor:pointer;display:flex;align-items:center;gap:8px">
<span style="font-size:16px">📐</span>
<div style="flex:1">
<div style="font-size:13px;font-weight:700;color:var(--ink)">Проверьте себя по чек-листу</div>
<div id="ergo-progress-label" style="font-size:11px;color:var(--muted);margin-top:1px">${doneCnt === total ? roomName + ' · Закрыт ✓' : roomName + ' · ' + doneCnt + ' из ' + total + ' — каждая отметка = ваша ответственность'}</div>
</div>
<svg id="ergo-chev" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color:var(--muted);transition:.2s"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div id="ergo-body" style="display:none;border-top:1px solid rgba(0,0,0,.06)">
${ergoBody + ergoTips}
</div>
</div>
<!-- Фото помещения -->
<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:16px 0 10px">Фото помещения</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px">
${cur.photoDone
? '<div class="photo-slot filled" style="background:rgba(0,0,0,.06)">📷</div><div class="photo-slot filled" style="background:rgba(0,0,0,.06)">📷</div>'
: '<div class="photo-slot"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div><div class="photo-slot"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div>'
}
<div class="photo-slot"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div>
</div>
<!-- Замер -->
<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:0 0 10px">Замер</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:6px">
${measureSlot}
<div class="photo-slot" onclick="_photoUploaded(this)">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
<div class="photo-slot" onclick="_photoUploaded(this)">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
</div>
<div style="font-size:11px;color:var(--muted);margin-bottom:16px">Фото замера на бумаге или скриншот из программы</div>
<!-- Комментарий -->
<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:0 0 8px">Комментарии · ${roomName}</div>
<textarea placeholder="Особенности, нюансы монтажа, пожелания..."
style="width:100%;border:1.5px solid rgba(0,0,0,.1);border-radius:10px;padding:10px 12px;font-size:13px;font-family:inherit;color:var(--ink);background:var(--card);outline:none;resize:none;min-height:64px;margin-bottom:16px;line-height:1.5"
>${roomName === 'Кухня' ? 'Старая кухня демонтирована, место свободно. Потолок ровный, стяжка +88 мм.' : ''}</textarea>
${(function(){
var missing = [];
var cnt = Object.values(cur.ergoChecked).filter(Boolean).length;
var ergoTotal2 = (ROOM_ERGO[roomName] || ROOM_ERGO['Кухня']).params.length;
if (cnt < ergoTotal2) missing.push({ icon:'📋', text:'Не промаркировал — не закрыл — не отправил', sub: 'Чек-лист ' + roomName + ': ' + cnt + ' из ' + ergoTotal2 + '. Каждая отметка — это ваша подпись под пунктом.' });
if (!cur.measureDone) missing.push({ icon:'📐', text:'Замер не загружен — менеджер не получил', sub: 'Без файла замера проект не запустится. Сфотографируйте бумагу или выгрузите из программы.' });
if (!cur.photoDone) missing.push({ icon:'📷', text:'Фото помещения не добавлено', sub: 'Нужно для подтверждения условий на объекте.' });
if (!missing.length) return '';
return '<div style="background:rgba(245,158,11,.07);border-left:3px solid #F59E0B;border-radius:0 10px 10px 0;padding:12px 14px;margin-bottom:14px">'
+ '<div style="font-size:11px;font-weight:700;color:#92400E;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px">Замер не закрыт</div>'
+ missing.map(function(m){
return '<div style="display:flex;gap:10px;margin-bottom:10px;align-items:flex-start">'
+ '<span style="font-size:15px;line-height:1.4;flex-shrink:0">' + m.icon + '</span>'
+ '<div><div style="font-size:13px;font-weight:700;color:var(--ink);line-height:1.3">' + m.text + '</div>'
+ '<div style="font-size:11px;color:var(--muted);margin-top:3px;line-height:1.5">' + m.sub + '</div></div>'
+ '</div>';
}).join('')
+ '</div>';
})()}
<!-- Оценка визита -->
${(function(){
var v = window._visitRating || null;
var opts = [
{key:'ok', emoji:'😊', label:'Всё штатно'},
{key:'notes', emoji:'🤔', label:'Есть нюансы'},
{key:'hard', emoji:'😤', label:'Было сложно'},
];
var btns = opts.map(function(o){
var active = v === o.key;
var bg = active
? (o.key==='ok' ? '#16A34A' : o.key==='notes' ? '#B45309' : '#DC2626')
: 'var(--card)';
var clr = active ? '#fff' : 'var(--ink)';
var bdr = active ? 'none' : '1.5px solid rgba(0,0,0,.08)';
return '<button onclick="(function(){'
+ 'window._visitRating=\''+ o.key +'\';'
+ 'document.getElementById(\'screen\').innerHTML=renderScreen(\'measurement_detail\');'
+ 'var s=Array.from(document.querySelectorAll(\'*\')).find(function(el){return el.scrollHeight>el.clientHeight+10&&getComputedStyle(el).overflowY!==\'visible\';});'
+ 'if(s)s.scrollTop=9999;'
+ '})()"'
+ ' style="flex:1;padding:10px 4px;border-radius:12px;border:'+bdr+';background:'+bg+';color:'+clr+';font-size:12px;font-weight:600;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:4px">'
+ '<span style="font-size:22px">'+o.emoji+'</span>'
+ '<span>'+o.label+'</span>'
+ '</button>';
}).join('');
var noteField = (v === 'notes' || v === 'hard')
? '<textarea placeholder="Что именно? Клиент, доступ, нестандартная ситуация..."'
+ ' style="width:100%;border:1.5px solid rgba(0,0,0,.1);border-radius:10px;padding:10px 12px;font-size:13px;font-family:inherit;color:var(--ink);background:var(--card);outline:none;resize:none;min-height:60px;margin-top:10px;line-height:1.5"></textarea>'
: '';
return '<div style="margin-bottom:16px">'
+ '<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin-bottom:2px">Как прошёл замер?</div>'
+ '<div style="font-size:11px;color:var(--muted);margin-bottom:10px">Только для вас — поднимается при разборе, если что-то пойдёт не так</div>'
+ '<div style="display:flex;gap:8px">' + btns + '</div>'
+ noteField
+ '</div>';
})()}
<button class="btn-primary" onclick="navigate('measure_calc')">💰 Расчёт и оплата →</button>
<button class="btn-secondary">Позвонить клиенту</button>
<div style="height:16px"></div>
</div>
</div>`;
}
/* ─────────── SCREEN: MEASURE CALC ─────────── */
function screenMeasureCalc() {
var RATE1 = 2500, RATEN = 1000;
var PRESETS = ['Кухня','Гостиная','Спальня','Прихожая','Коридор','Ванная','Санузел','Балкон / Лоджия','Кабинет','Детская','Гардероб','Столовая'];
if (!window._calcCustomRooms) window._calcCustomRooms = []; // {name, flagged}
if (!window._calcRooms) {
window._calcRooms = PRESETS.map(function(n){ return {name:n, on:false}; });
}
if (window._calcClientMode === undefined) window._calcClientMode = false;
var rooms = window._calcRooms;
var checked = rooms.filter(function(r){ return r.on; });
var n = checked.length;
var total = n === 0 ? 0 : n === 1 ? RATE1 : RATE1 + (n - 1) * RATEN;
function tog(i) {
return 'onclick="(function(){window._calcRooms[' + i + '].on=!window._calcRooms[' + i + '].on;'
+ 'document.getElementById(\'screen\').innerHTML=renderScreen(\'measure_calc\')})()"';
}
// ── CLIENT MODE (fullscreen overlay) ──────────────────────────
if (window._calcClientMode) {
if (window._calcStep === undefined) window._calcStep = 'pay'; // 'pay' | 'sign'
if (window._calcPayMethod === undefined) window._calcPayMethod = null;
var lines = checked.map(function(r, idx){
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid rgba(255,255,255,.12)">'
+ '<span style="font-size:15px;font-weight:600;color:rgba(255,255,255,.85)">' + (idx===0?'1-е помещение':'+ ещё помещение') + '</span>'
+ '<span style="font-size:15px;font-weight:700;color:#fff">' + (idx===0?RATE1:RATEN) + ' ₽</span>'
+ '</div>';
}).join('');
var backBtn = '<button onclick="(function(){'
+ (window._calcStep==='sign'
? 'window._calcStep=\'pay\';window._calcPayMethod=null;'
: 'window._calcClientMode=false;window._calcStep=undefined;window._calcPayMethod=null;')
+ 'document.getElementById(\'screen\').innerHTML=renderScreen(\'measure_calc\')})()"'
+ ' style="background:rgba(255,255,255,.1);border:none;border-radius:10px;width:36px;height:36px;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0">'
+ '<svg width="18" height="18" fill="none" stroke="#fff" stroke-width="2.5" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>'
+ '</button>';
var header = '<div style="padding:20px 24px 0;display:flex;align-items:center;gap:12px">'
+ backBtn
+ '<span style="font-size:14px;font-weight:600;color:rgba(255,255,255,.6)">'
+ (window._calcStep==='sign' ? 'Подпись клиента' : 'Стоимость замера') + '</span>'
+ '</div>';
var totalDisplay = '<div style="text-align:center;margin-bottom:28px">'
+ '<div style="font-size:13px;font-weight:700;color:rgba(255,255,255,.5);text-transform:uppercase;letter-spacing:.1em;margin-bottom:10px">К оплате</div>'
+ '<div style="font-size:60px;font-weight:900;color:#fff;line-height:1;margin-bottom:6px">' + total.toLocaleString('ru') + ' ₽</div>'
+ '<div style="font-size:14px;color:rgba(255,255,255,.5)">' + n + ' ' + (n===1?'помещение':n<5?'помещения':'помещений') + '</div>'
+ '</div>';
var breakdown = '<div style="width:100%;background:rgba(255,255,255,.08);border-radius:16px;padding:4px 16px;margin-bottom:28px">' + lines + '</div>';
// ── STEP 1: выбор способа оплаты ──
if (window._calcStep === 'pay') {
var payBtns = '<div style="display:flex;gap:12px;width:100%">'
+ '<button onclick="(function(){window._calcPayMethod=\'card\';window._calcStep=\'pay\';'
+ 'window._calcClientMode=false;window._calcStep=undefined;window._calcPayMethod=null;window._calcRooms=null;'
+ 'document.getElementById(\'screen\').innerHTML=renderScreen(\'measurements\')})()"'
+ ' style="flex:1;padding:16px 8px;background:rgba(255,255,255,.12);border:1.5px solid rgba(255,255,255,.25);border-radius:14px;color:#fff;font-size:14px;font-weight:700;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:6px">'
+ '<span style="font-size:28px">💳</span><span>Карта / QR</span>'
+ '</button>'
+ '<button onclick="(function(){window._calcStep=\'sign\';'
+ 'document.getElementById(\'screen\').innerHTML=renderScreen(\'measure_calc\')})()"'
+ ' style="flex:1;padding:16px 8px;background:rgba(255,255,255,.12);border:1.5px solid rgba(255,255,255,.25);border-radius:14px;color:#fff;font-size:14px;font-weight:700;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:6px">'
+ '<span style="font-size:28px">💵</span><span>Наличные</span>'
+ '</button>'
+ '</div>';
return '<div style="min-height:100vh;background:linear-gradient(160deg,#1e1b4b,#312e81);display:flex;flex-direction:column">'
+ header
+ '<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 28px">'
+ totalDisplay + breakdown + payBtns
+ '</div></div>';
}
// ── STEP 2: подпись клиента (наличные) ──
if (window._calcStep === 'sign') {
var sigArea = '<div style="width:100%;background:rgba(255,255,255,.95);border-radius:16px;overflow:hidden;margin-bottom:16px">'
+ '<div style="padding:10px 16px;border-bottom:1px solid rgba(0,0,0,.08);display:flex;justify-content:space-between;align-items:center">'
+ '<span style="font-size:12px;color:#666;font-weight:600">Подпись клиента</span>'
+ '<button onclick="(function(){var cv=document.getElementById(\'sig-cv\');cv.getContext(\'2d\').clearRect(0,0,cv.width,cv.height);window._calcSigned=false})()"'
+ ' style="font-size:12px;color:#6366F1;background:none;border:none;cursor:pointer;font-weight:600">Очистить</button>'
+ '</div>'
+ '<canvas id="sig-cv" width="340" height="160" style="display:block;width:100%;height:160px;touch-action:none"></canvas>'
+ '</div>'
+ '<div style="font-size:12px;color:rgba(255,255,255,.5);text-align:center;margin-bottom:20px">'
+ 'Подписывая, клиент подтверждает согласие с результатами замера и оплату ' + total.toLocaleString('ru') + ' ₽ наличными'
+ '</div>'
+ '<button id="sig-confirm-btn" disabled'
+ ' onclick="(function(){window._calcClientMode=false;window._calcStep=undefined;window._calcPayMethod=null;window._calcRooms=null;window._calcSigned=false;'
+ 'document.getElementById(\'screen\').innerHTML=renderScreen(\'measurements\')})()"'
+ ' style="width:100%;padding:16px;background:#22c55e;color:#fff;border:none;border-radius:14px;font-size:17px;font-weight:800;cursor:pointer;opacity:.45">'
+ '✓ Оплачено и подписано</button>'
+ '<img src="x" onerror="initSigCanvas()" style="display:none">';
return '<div style="min-height:100vh;background:linear-gradient(160deg,#1e1b4b,#312e81);display:flex;flex-direction:column">'
+ header
+ '<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 24px">'
+ '<div style="font-size:13px;font-weight:700;color:rgba(255,255,255,.5);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px">Итог: ' + total.toLocaleString('ru') + ' ₽</div>'
+ sigArea
+ '</div></div>';
}
}
// ── CALC MODE ─────────────────────────────────────────────────
// Add custom rooms from _calcCustomRooms
window._calcCustomRooms.forEach(function(cr, ci){
var idx = rooms.findIndex(function(x){return x.name===cr.name;});
if (idx === -1) rooms.push({name:cr.name, on:cr.on||false, custom:true});
});
var roomList = rooms.map(function(r, i){
return '<div ' + tog(i)
+ ' style="display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid rgba(0,0,0,.05);cursor:pointer">'
+ '<div style="width:24px;height:24px;border-radius:7px;border:1.5px solid '
+ (r.on ? 'var(--accent)' : 'rgba(0,0,0,.15)') + ';background:'
+ (r.on ? 'var(--accent)' : 'transparent') + ';display:flex;align-items:center;justify-content:center;flex-shrink:0">'
+ (r.on ? '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3.5"><polyline points="20 6 9 17 4 12"/></svg>' : '')
+ '</div>'
+ '<span style="flex:1;font-size:14px;color:' + (r.on ? 'var(--ink)' : 'var(--muted)') + ';font-weight:' + (r.on ? '600' : '400') + '">' + r.name + '</span>'
+ (r.on ? '<span style="font-size:13px;font-weight:700;color:var(--accent)">' + (checked.indexOf(r) === 0 ? RATE1 : RATEN) + ' ₽</span>' : '')
+ '</div>';
}).join('');
var totalBlock = '<div style="margin-top:0;background:' + (n > 0 ? 'var(--accent)' : 'rgba(0,0,0,.05)') + ';border-radius:16px;padding:18px 20px;display:flex;align-items:center;justify-content:space-between">'
+ '<div>'
+ '<div style="font-size:12px;font-weight:700;color:' + (n > 0 ? 'rgba(255,255,255,.7)' : 'var(--muted)') + ';text-transform:uppercase;letter-spacing:.05em">Итого</div>'
+ '<div style="font-size:13px;color:' + (n > 0 ? 'rgba(255,255,255,.7)' : 'var(--muted)') + ';margin-top:2px">' + n + ' ' + (n===1?'помещение':n<5?'помещения':'помещений') + (n > 1 ? ' · 1 × 2500 + ' + (n-1) + ' × 1000' : '') + '</div>'
+ '</div>'
+ '<div style="font-size:28px;font-weight:900;color:' + (n > 0 ? '#fff' : 'var(--muted)') + '">' + (n > 0 ? total.toLocaleString('ru') + ' ₽' : '—') + '</div>'
+ '</div>';
var clientBtn = n > 0
? '<button onclick="(function(){window._calcClientMode=true;document.getElementById(\'screen\').innerHTML=renderScreen(\'measure_calc\')})()"'
+ ' style="width:100%;padding:15px;background:var(--accent);color:#fff;border:none;border-radius:14px;font-size:16px;font-weight:800;cursor:pointer;margin-top:12px">'
+ '📲 Показать клиенту</button>'
: '<button disabled style="width:100%;padding:15px;background:rgba(0,0,0,.06);color:var(--muted);border:none;border-radius:14px;font-size:16px;font-weight:700;margin-top:12px">Выберите помещения</button>';
return '<div class="page">'
+ '<div class="page-header">'
+ '<button class="back-btn" onclick="navigate(\'measurement_detail\')">'
+ '<svg fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>'
+ '</button>'
+ '<h2>Расчёт стоимости</h2>'
+ '</div>'
+ '<div style="padding:0 16px">'
+ '<div class="card">'
+ '<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px">Помещения</div>'
+ roomList
+ (function(){
return '<div style="margin-top:10px;padding-top:10px;border-top:1px solid rgba(0,0,0,.06)">'
+ '<div style="font-size:11px;color:var(--muted);margin-bottom:8px">Нет нужного помещения? Добавьте — директор увидит и решит включить в справочник</div>'
+ '<div style="display:flex;gap:8px">'
+ '<input id="custom-room" type="text" placeholder="Название помещения..." style="flex:1;border:1.5px solid rgba(0,0,0,.12);border-radius:8px;padding:8px 10px;font-size:13px;outline:none">'
+ '<button onclick="(function(){var v=document.getElementById(\'custom-room\').value.trim();'
+ 'if(!v)return;'
+ 'window._calcCustomRooms.push({name:v,on:true,flagged:true});'
+ 'window._calcRooms=null;'
+ 'document.getElementById(\'screen\').innerHTML=renderScreen(\'measure_calc\')})()" style="background:var(--accent);color:#fff;border:none;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:700;cursor:pointer">+ Добавить</button>'
+ '</div>'
+ '</div>';
})()
+ '</div>'
+ totalBlock
+ clientBtn
+ '<div style="height:24px"></div>'
+ '</div>'
+ '</div>';
}
/* ─────────── SCREEN: ASSEMBLIES ─────────── */
function screenAssemblies() {
return `<div class="page">
<div class="page-header"><h2>Мои сборки</h2></div>
<div style="height:12px"></div>
<div class="chip-row">
<div class="chip active">Все</div>
<div class="chip">Сегодня</div>
<div class="chip">Активные</div>
<div class="chip">Завершённые</div>
</div>
<div style="padding:4px 16px 0">
<div class="card warn-border" style="cursor:pointer;margin-bottom:10px" onclick="navigate('assembly_detail')">
<div class="row sb" style="margin-bottom:6px">
<span style="font-size:14px;font-weight:700;color:var(--ink)">#А-2847</span>
<span class="badge yellow">В работе</span>
</div>
<div style="font-size:13px;color:var(--muted);margin-bottom:8px">Черниговская 14 · Кухня</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<div class="progress-bar"><div class="progress-fill" style="width:57%"></div></div>
<span style="font-size:11px;color:var(--muted)">4/7</span>
</div>
<div class="row sb">
<span class="badge red" style="font-size:11px">Срочно</span>
<span style="font-size:12px;color:var(--muted)">сегодня 11:00</span>
</div>
</div>
<div class="card accent-border" style="cursor:pointer;margin-bottom:10px" onclick="navigate('assembly_detail')">
<div class="row sb" style="margin-bottom:6px">
<span style="font-size:14px;font-weight:700;color:var(--ink)">#А-2851</span>
<span class="badge gray">Ожидает</span>
</div>
<div style="font-size:13px;color:var(--muted);margin-bottom:6px">Ленина 8 · Гардероб</div>
<div style="font-size:12px;color:var(--muted)">25 мая</div>
</div>
<div class="card success-border" style="cursor:pointer;margin-bottom:10px" onclick="navigate('assembly_detail')">
<div class="row sb" style="margin-bottom:6px">
<span style="font-size:14px;font-weight:700;color:var(--ink)">#А-2836</span>
<span class="badge green">Завершена ✓</span>
</div>
<div style="font-size:13px;color:var(--muted)">Мира 22 · Прихожая</div>
</div>
</div>
</div>`;
}
/* ─────────── SCREEN: ASSEMBLY DETAIL ─────────── */
function screenAssemblyDetail() {
var items = [
{text:'Получить комплект на складе', done:true},
{text:'Доставка на объект', done:true},
{text:'Инструмент проверен', done:true},
{text:'Согласование с клиентом', done:true},
{text:'Сборка корпусов', done:false},
{text:'Установка фасадов и фурнитуры', done:false},
{text:'Финальная проверка + фото', done:false},
];
var doneCount = items.filter(function(i){ return i.done; }).length;
var checkHtml = items.map(function(item, idx){
var boxCls = item.done ? 'check-box done' : 'check-box';
var txtCls = item.done ? 'check-text done' : 'check-text';
return `<div class="checklist-item">
<div class="${boxCls}">
<svg fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="${txtCls}"><h4>${item.text}</h4></div>
</div>`;
}).join('');
return `<div class="page">
<div class="page-header">
<button class="back-btn" onclick="navigate('assemblies')">
<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<h2>Сборка #А-2847</h2>
<span class="badge yellow" style="margin-left:auto">В работе</span>
</div>
<div style="padding:14px 16px 0">
<div class="card" style="margin-bottom:12px">
<div class="info-row">
<div class="info-label">Адрес</div>
<div class="info-val">Черниговская 14</div>
</div>
<div class="info-row">
<div class="info-label">Тип мебели</div>
<div class="info-val">Кухня</div>
</div>
<div class="info-row">
<div class="info-label">Время</div>
<div class="info-val">Сегодня 11:00</div>
</div>
</div>
<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px">
Чеклист <span style="font-size:12px;font-weight:700;color:var(--ink)">${doneCount}/${items.length}</span>
</div>
<div class="card">${checkHtml}</div>
<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:16px 0 10px">Фото отчёт</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px">
<div class="photo-slot filled">📷</div>
<div class="photo-slot filled">📷</div>
<div class="photo-slot">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
<div class="photo-slot">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
<div class="photo-slot">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
</div>
<div style="font-size:12px;color:var(--muted);text-align:center;margin-bottom:16px">Добавить фото отчёт</div>
<button class="btn-secondary">Связаться с менеджером</button>
<button class="btn-primary" style="margin-top:8px" disabled>
Завершить сборку → (${doneCount}/${items.length})
</button>
<div style="height:16px"></div>
</div>
</div>`;
}
/* ─────────── SCREEN: CALENDAR / МАРШРУТ ─────────── */
var WORKER_HOME = 'Садовая 5, кв.12';
var KM_RATE = 4; // ₽/км
var ALL_JOBS = [
{date:'2026-05-05', type:'meas', time:'10:00', addr:'Мира 22, кв.7', name:'Кухня прямая'},
{date:'2026-05-07', type:'asm', time:'09:30', addr:'Пушкина 8', name:'#А-2836 · Прихожая'},
{date:'2026-05-12', type:'meas', time:'11:00', addr:'Ленина 45, кв.12', name:'Гардероб'},
{date:'2026-05-12', type:'asm', time:'14:00', addr:'Советская 3', name:'#А-2840 · Кухня'},
{date:'2026-05-15', type:'asm', time:'10:00', addr:'Краснодарская 7', name:'#А-2843 · Спальня'},
{date:'2026-05-19', type:'meas', time:'09:00', addr:'Красная 17, кв.33', name:'Кухня угловая'},
{date:'2026-05-19', type:'meas', time:'15:00', addr:'Кубанская 5, кв.8', name:'Спальня'},
{date:'2026-05-21', type:'asm', time:'10:00', addr:'Ленина 8', name:'#А-2851 · Гардероб'},
{date:'2026-05-23', type:'meas', time:'09:00', addr:'Ленина 12, кв.45', name:'Кухня угловая'},
{date:'2026-05-23', type:'asm', time:'11:00', addr:'Черниговская 14', name:'#А-2847 · Кухня'},
{date:'2026-05-23', type:'meas', time:'14:30', addr:'Мира 7, кв.18', name:'Гардероб'},
{date:'2026-05-25', type:'asm', time:'11:00', addr:'Садовая 33', name:'#А-2855 · Гостиная'},
{date:'2026-05-27', type:'meas', time:'10:00', addr:'Красноармейская 12', name:'Кухня прямая'},
{date:'2026-05-27', type:'asm', time:'13:00', addr:'Мира 15', name:'#А-2858 · Прихожая'},
{date:'2026-05-28', type:'meas', time:'09:30', addr:'Гагарина 45, кв.4', name:'Кухня угловая'},
{date:'2026-05-30', type:'asm', time:'10:00', addr:'Советская 22', name:'#А-2862 · Кухня'},
];
// km arrays: [home→job0, job0→job1, ..., lastJob→home]
var ROUTE_KM = {
'2026-05-05': [7.4, 8.1],
'2026-05-07': [5.2, 6.8],
'2026-05-12': [9.1, 4.2, 11.3],
'2026-05-15': [6.7, 5.9],
'2026-05-19': [8.5, 5.1, 9.8],
'2026-05-21': [7.2, 8.4],
'2026-05-23': [8.2, 3.4, 12.1, 7.3],
'2026-05-25': [6.3, 7.1],
'2026-05-27': [5.8, 3.2, 7.5],
'2026-05-28': [9.4, 10.2],
'2026-05-30': [4.8, 6.3],
};
function refreshCal() {
document.getElementById('screen').innerHTML = screenCalendar();
}
function screenCalendar() {
var role = window._workerRole || 'both';
var selDate = window._calDate || '2026-05-23';
var jobs = ALL_JOBS.filter(function(j) {
if (role === 'measurer') return j.type === 'meas';
if (role === 'assembler') return j.type === 'asm';
return true;
});
// Build day lookup
var dayMap = {};
jobs.forEach(function(j) {
if (!dayMap[j.date]) dayMap[j.date] = [];
dayMap[j.date].push(j);
});
// May 2026 calendar
var year = 2026, monthIdx = 4; // May = 4
var daysInMonth = 31;
var firstWday = new Date(year, monthIdx, 1).getDay(); // 0=Sun
var startOffset = (firstWday + 6) % 7; // Mon-first
var todayStr = '2026-05-23';
var dayNames = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
// Header
var html = '<div class="page">'
+ '<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 16px 10px;background:var(--card);border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:50">'
+ '<h2 style="font-size:17px;font-weight:700;color:var(--ink)">Маршрут</h2>'
+ '<div style="font-size:14px;font-weight:700;color:var(--ink)">МАЙ 2026</div>'
+ '</div>';
// Weekday labels
html += '<div style="display:grid;grid-template-columns:repeat(7,1fr);padding:10px 10px 4px;background:var(--card)">'
+ dayNames.map(function(d,i){
return '<div style="text-align:center;font-size:10px;font-weight:700;color:' + (i>=5?'var(--warn)':'var(--muted)') + '">' + d + '</div>';
}).join('')
+ '</div>';
// Calendar grid
var totalCells = Math.ceil((startOffset + daysInMonth) / 7) * 7;
html += '<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:1px;padding:0 8px 8px;background:var(--card)">';
for (var i = 0; i < totalCells; i++) {
var dayNum = i - startOffset + 1;
var valid = dayNum >= 1 && dayNum <= daysInMonth;
var ds = valid ? (year + '-05-' + String(dayNum).padStart(2,'0')) : '';
var isToday = ds === todayStr;
var isSel = ds === selDate;
var isWknd = (i % 7) >= 5;
var dayJobs = valid ? (dayMap[ds] || []) : [];
var hasMeas = dayJobs.some(function(j){ return j.type==='meas'; });
var hasAsm = dayJobs.some(function(j){ return j.type==='asm'; });
var cellBg = isSel ? 'background:var(--accent);border-radius:10px;' : '';
var numColor = isSel ? '#fff' : (isWknd ? 'var(--warn)' : 'var(--ink)');
var numW = (isToday && !isSel) ? '800' : '600';
var onclick = valid ? ' onclick="(function(){window._calDate=\'' + ds + '\';refreshCal()})()"' : '';
var todayRing = (isToday && !isSel)
? '<div style="position:absolute;inset:0;border:2px solid var(--accent);border-radius:10px;pointer-events:none"></div>'
: '';
var dots = '';
if (valid && (hasMeas || hasAsm)) {
var dotBg = isSel ? 'rgba(255,255,255,.75)' : '';
dots = '<div style="display:flex;gap:2px;justify-content:center;margin-top:3px">'
+ (hasMeas ? '<div style="width:5px;height:5px;border-radius:50%;background:' + (isSel ? dotBg : '#3B82F6') + '"></div>' : '')
+ (hasAsm ? '<div style="width:5px;height:5px;border-radius:50%;background:' + (isSel ? dotBg : '#10B981') + '"></div>' : '')
+ '</div>';
}
html += '<div' + onclick + ' style="position:relative;display:flex;flex-direction:column;align-items:center;padding:7px 2px;cursor:' + (valid?'pointer':'default') + ';' + cellBg + '">'
+ todayRing
+ (valid ? '<div style="font-size:13px;font-weight:' + numW + ';color:' + numColor + ';line-height:1">' + dayNum + '</div>' + dots : '')
+ '</div>';
}
html += '</div>';
// Legend
html += '<div style="display:flex;gap:16px;align-items:center;padding:6px 14px 10px;background:var(--card);border-bottom:1px solid rgba(0,0,0,.08)">'
+ '<div style="display:flex;align-items:center;gap:5px"><div style="width:8px;height:8px;border-radius:50%;background:#3B82F6"></div><span style="font-size:11px;color:var(--muted)">Замер</span></div>'
+ '<div style="display:flex;align-items:center;gap:5px"><div style="width:8px;height:8px;border-radius:50%;background:#10B981"></div><span style="font-size:11px;color:var(--muted)">Сборка</span></div>'
+ '<div style="flex:1"></div>'
+ '<div style="font-size:11px;color:var(--muted)">' + KM_RATE + ' ₽/км компенс.</div>'
+ '</div>';
// Route for selected day
html += '<div style="padding:12px 14px">';
var selJobs = dayMap[selDate] || [];
if (!selDate || selJobs.length === 0) {
html += '<div style="text-align:center;padding:20px;color:var(--muted);font-size:14px">Нет задач в этот день</div>';
} else {
var rKms = ROUTE_KM[selDate] || [];
var totalKm = rKms.reduce(function(a,b){return a+b;},0);
var comp = Math.round(totalKm * KM_RATE);
// Date sub-header
var sdObj = new Date(selDate + 'T00:00:00');
var wdRu = ['Вс','Пн','Вт','Ср','Чт','Пт','Сб'][sdObj.getDay()];
var mRu = ['янв','фев','мар','апр','мая','июн','июл','авг','сен','окт','ноя','дек'][sdObj.getMonth()];
html += '<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:10px">'
+ '<div style="font-size:14px;font-weight:700;color:var(--ink)">' + wdRu + ', ' + sdObj.getDate() + ' ' + mRu + '</div>'
+ '<div style="font-size:12px;color:var(--muted)">' + selJobs.length + ' задач' + (totalKm>0 ? ' · ' + totalKm.toFixed(1) + ' км' : '') + '</div>'
+ '</div>';
// Route card
html += '<div class="card" style="padding:10px 14px">';
var iconMap = {meas:'📐', asm:'🔧'};
var colorMap = {meas:'#3B82F6', asm:'#10B981'};
var bgMap = {meas:'rgba(59,130,246,.1)', asm:'rgba(16,185,129,.1)'};
var nameMap = {meas:'Замер', asm:'Сборка'};
var screenMap = {meas:'measurement_detail', asm:'assembly_detail'};
// Home → first job
html += '<div style="display:flex;align-items:center;gap:10px;padding:8px 0">'
+ '<div style="width:34px;height:34px;border-radius:50%;background:rgba(0,62,126,.1);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0">🏠</div>'
+ '<div><div style="font-size:13px;font-weight:700;color:var(--ink)">Дом</div>'
+ '<div style="font-size:11px;color:var(--muted)">' + WORKER_HOME + '</div></div>'
+ '</div>';
selJobs.forEach(function(job, idx) {
var kmToThis = rKms[idx] !== undefined ? rKms[idx] : 0;
// Connector line + km
html += '<div style="display:flex;align-items:center;gap:10px;padding:2px 0">'
+ '<div style="width:34px;display:flex;justify-content:center;flex-shrink:0">'
+ '<div style="width:2px;height:20px;background:rgba(0,0,0,.12);border-radius:1px"></div>'
+ '</div>'
+ '<div style="font-size:11px;color:var(--muted);font-weight:600">' + kmToThis.toFixed(1) + ' км</div>'
+ '</div>';
// Job row
var jColor = colorMap[job.type];
var jBg = bgMap[job.type];
html += '<div style="display:flex;align-items:center;gap:10px;padding:8px 0;cursor:pointer" onclick="navigate(\'' + screenMap[job.type] + '\')">'
+ '<div style="width:34px;height:34px;border-radius:50%;background:' + jBg + ';display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0">' + iconMap[job.type] + '</div>'
+ '<div style="flex:1;min-width:0">'
+ '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">'
+ '<span style="font-size:13px;font-weight:700;color:var(--ink)">' + job.time + ' · ' + job.addr + '</span>'
+ '<span style="font-size:10px;font-weight:700;color:' + jColor + ';background:' + jBg + ';border-radius:4px;padding:1px 6px">' + nameMap[job.type] + '</span>'
+ '</div>'
+ '<div style="font-size:11px;color:var(--muted);margin-top:2px">' + job.name + '</div>'
+ '</div>'
+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color:var(--muted);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>'
+ '</div>';
});
// Return km + home
var retKm = rKms[selJobs.length] !== undefined ? rKms[selJobs.length] : 0;
html += '<div style="display:flex;align-items:center;gap:10px;padding:2px 0">'
+ '<div style="width:34px;display:flex;justify-content:center;flex-shrink:0">'
+ '<div style="width:2px;height:20px;background:rgba(0,0,0,.12);border-radius:1px"></div>'
+ '</div>'
+ '<div style="font-size:11px;color:var(--muted);font-weight:600">' + retKm.toFixed(1) + ' км</div>'
+ '</div>';
html += '<div style="display:flex;align-items:center;gap:10px;padding:8px 0">'
+ '<div style="width:34px;height:34px;border-radius:50%;background:rgba(0,62,126,.1);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0">🏠</div>'
+ '<div><div style="font-size:13px;font-weight:700;color:var(--ink)">Дом</div>'
+ '<div style="font-size:12px;color:var(--muted);margin-top:1px">'
+ 'Итого: <b style="color:var(--ink)">' + totalKm.toFixed(1) + ' км</b>'
+ ' &nbsp;·&nbsp; компенсация: <b style="color:var(--success)">' + comp + ' ₽</b>'
+ '</div></div>'
+ '</div>';
html += '</div>'; // card
}
html += '</div>'; // padding div
html += '</div>'; // page
return html;
}
/* ─────────── SCREEN: FINANCES ─────────── */
function screenFinances() {
var role = window._workerRole || 'both';
var rows = [
{date:'22 мая', obj:'Ленина 12', type:'Замер', sum:'1 200 ₽'},
{date:'21 мая', obj:'Черниговская 14', type:'Сборка', sum:'4 800 ₽'},
{date:'20 мая', obj:'Мира 7', type:'Замер', sum:'800 ₽'},
{date:'19 мая', obj:'Садовая 4', type:'Сборка', sum:'3 600 ₽'},
{date:'18 мая', obj:'Пушкина 2', type:'Сборка', sum:'2 400 ₽'},
];
// filter by role
if (role === 'measurer') rows = rows.filter(function(r){ return r.type === 'Замер'; });
if (role === 'assembler') rows = rows.filter(function(r){ return r.type === 'Сборка'; });
var measCount = (role === 'assembler') ? 0 : 8;
var asmCount = (role === 'measurer') ? 0 : 9;
var total = measCount + asmCount;
return `<div class="page">
<div class="page-header"><h2>Финансы</h2></div>
<div style="padding:16px 16px 0">
<div class="earnings-card">
<div style="font-size:13px;opacity:.8;margin-bottom:6px">Получено в мае</div>
<div style="font-size:28px;font-weight:800;letter-spacing:-.5px;margin-bottom:2px">28 400 ₽</div>
<div style="font-size:12px;opacity:.75;margin-bottom:12px">к выплате: <b>6 200 ₽</b></div>
<div class="progress-bar" style="background:rgba(255,255,255,.25)">
<div class="progress-fill" style="width:71%;background:rgba(255,255,255,.9)"></div>
</div>
</div>
<div class="card">
<table class="stat-table">
<thead>
<tr>
<th>Дата</th>
<th>Объект</th>
<th>Вид</th>
<th style="text-align:right">Сумма</th>
</tr>
</thead>
<tbody>
${rows.map(function(r){
var badgeCls = r.type === 'Замер' ? 'blue' : 'teal';
return `<tr>
<td style="color:var(--muted)">${r.date}</td>
<td>${r.obj}</td>
<td><span class="badge ${badgeCls}" style="font-size:10px;padding:2px 7px">${r.type}</span></td>
<td style="text-align:right;font-weight:600">${r.sum}</td>
</tr>`;
}).join('')}
</tbody>
</table>
</div>
<div class="card" style="display:flex;justify-content:space-around;text-align:center">
${measCount > 0 ? `<div><div style="font-size:22px;font-weight:800;color:var(--ink)">${measCount}</div><div style="font-size:11px;color:var(--muted);margin-top:2px">Замеров</div></div>` : ''}
${measCount > 0 && asmCount > 0 ? '<div style="width:1px;background:rgba(0,0,0,.08)"></div>' : ''}
${asmCount > 0 ? `<div><div style="font-size:22px;font-weight:800;color:var(--ink)">${asmCount}</div><div style="font-size:11px;color:var(--muted);margin-top:2px">Сборок</div></div>` : ''}
<div style="width:1px;background:rgba(0,0,0,.08)"></div>
<div><div style="font-size:22px;font-weight:800;color:var(--ink)">${total}</div><div style="font-size:11px;color:var(--muted);margin-top:2px">Итого</div></div>
</div>
</div>
</div>`;
}
/* ─────────── SCREEN: PROFILE ─────────── */
function screenProfile() {
var role = window._workerRole || 'both';
var measStat = (role === 'assembler') ? '' : `<div class="card" style="text-align:center;flex:1">
<div style="font-size:24px;font-weight:800;color:var(--ink)">8</div>
<div style="font-size:11px;color:var(--muted);margin-top:2px">Замеров в мае</div>
</div>`;
var asmStat = (role === 'measurer') ? '' : `<div class="card" style="text-align:center;flex:1">
<div style="font-size:24px;font-weight:800;color:var(--ink)">9</div>
<div style="font-size:11px;color:var(--muted);margin-top:2px">Сборок в мае</div>
</div>`;
return `<div class="page">
<div class="page-header"><h2>Профиль</h2></div>
<div style="padding:24px 16px 0;text-align:center">
<div class="avatar lg" style="width:80px;height:80px;font-size:26px;margin:0 auto 12px">КА</div>
<div style="font-size:18px;font-weight:700;color:var(--ink);margin-bottom:6px">Кириллов Алексей Владимирович</div>
<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin-bottom:6px">${roleBadges()}</div>
<div style="font-size:13px;color:var(--muted)">Категория А</div>
</div>
<div style="padding:20px 16px 0;display:flex;gap:10px;flex-wrap:wrap">
${measStat}
${asmStat}
<div class="card" style="text-align:center;flex:1">
<div style="font-size:24px;font-weight:800;color:var(--ink)">4.8 ⭐</div>
<div style="font-size:11px;color:var(--muted);margin-top:2px">Рейтинг</div>
</div>
</div>
<div style="padding:0 16px">
<div class="card" style="margin-top:10px">
<div class="info-row">
<div class="info-label">Домашний адрес</div>
<div class="info-val" style="display:flex;align-items:center;gap:6px">
<span>ул. Садовая, 5, кв. 12</span>
<span style="font-size:11px;color:var(--muted)">(старт маршрута)</span>
</div>
</div>
<div class="info-row">
<div class="info-label">Телефон</div>
<div class="info-val" style="color:var(--accent)">+7 (912) 456-78-90</div>
</div>
<div class="info-row">
<div class="info-label">На проекте</div>
<div class="info-val">с марта 2024</div>
</div>
<div class="info-row">
<div class="info-label">Всего работ</div>
<div class="info-val">143</div>
</div>
</div>
${role !== 'assembler' ? `
<!-- Tool kit — read-only, shown only for measurer -->
<div class="card" style="margin-top:0">
<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px">Набор инструмента</div>
${[
{key:'laser', icon:'📏', label:'Лазерный дальномер', on:true},
{key:'level', icon:'📐', label:'Электронный уровень', on:true},
{key:'metal', icon:'🔍', label:'Металлодетектор', on:false},
{key:'photo', icon:'📷', label:'Фотофиксация (камера)', on:true},
{key:'tablet', icon:'💻', label:'Планшет с ПО', on:false},
{key:'blueprint',icon:'🗺️', label:'Чертёжный комплект', on:false},
].map(t => `
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,.05)">
<div style="width:20px;height:20px;border-radius:5px;border:1.5px solid ${t.on ? 'var(--accent)' : 'rgba(0,0,0,.15)'};background:${t.on ? 'var(--accent)' : 'transparent'};display:flex;align-items:center;justify-content:center;flex-shrink:0">
${t.on ? '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3.5"><polyline points="20 6 9 17 4 12"/></svg>' : ''}
</div>
<span style="font-size:13px">${t.icon}</span>
<span style="font-size:13px;color:${t.on ? 'var(--ink)' : 'var(--muted)'};font-weight:${t.on ? '600' : '400'}">${t.label}</span>
</div>
`).join('')}
<div style="margin-top:10px;font-size:11px;color:var(--muted)">Состав набора устанавливает директор</div>
</div>` : ''}
<button class="btn-secondary" style="margin-top:4px">Связаться с диспетчером</button>
<div style="height:16px"></div>
</div>
</div>`;
}
// ── ERGO CATALOG (Рита · v1.0 · ЧЕКЛИСТ_ЗАМЕРА) ────────────
var ROOM_ERGO = {
'Кухня': {
icon: '🍳',
params: [
{label:'Ширина рабочей стены', hint:'В 3 высотах — стены не параллельны', critical:true},
{label:'Высота потолка', hint:'У стены установки, не в центре', critical:true},
{label:'V1 — Вентбокс', hint:× В + расстояние от обоих углов стены', critical:true},
{label:'Wc1 / Wh1 — Вода', hint:'Горизонталь от базы + вертикаль от пола', critical:true},
{label:'D1 / D1\' — Слив + фановая',hint:'Выпуск от стены, горизонталь и вертикаль', critical:true},
{label:'Углы (ЛН, ПН, ЛВ, ПВ)', hint:'Если ≠ 90° — пишем значение в градусах', critical:false},
{label:'R1 × N — Розетки', hint:'Высота от пола + горизонталь от базы', critical:false},
{label:'Rs1 — Силовые розетки', hint:'Под духовку, варочную — позицию точно', critical:false},
{label:'ОК1 — Окно / подоконник', hint:'Высота низа, ширина, глубина подоконника', critical:false},
],
tips: [
'V1 (вентбокс) — главная засада: замерь до мм, потом не переделать',
'Базу (БАЗА: ЛУ или ПУ) выбери с первого угла и держи на всей стене',
'Уточни у клиента встройку — она определяет высоту верхних шкафов',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Гостиная': {
icon: '🛋️',
params: [
{label:'Ширина стены под мебель', hint:'В 3 точках — у пола, середины, потолка', critical:true},
{label:'Высота потолка', hint:'У стены + в центре (если есть перепад)', critical:true},
{label:В1 / ОК1 — Проёмы', hint:'Ширина + высота + отступ от базы до края', critical:true},
{label:'Углы (ЛН, ПН)', hint:'Если ≠ 90° — особенно углы под мебель', critical:false},
{label:'Tv1 / Net1 — ТВ, интернет', hint:'Горизонталь от базы + высота от пола', critical:false},
{label:'R1 × N — Розетки', hint:'Высота + горизонталь; будут скрыты мебелью', critical:false},
{label:'Ld1 — Освещение', hint:'Точка на потолке — горизонт и вертикаль', critical:false},
],
tips: [
'Диагональ комнаты: если разница > 20 мм — стены не прямые, пишем углы',
'Tv1/Net1 фиксируй точно — стенка закроет, потом не доберёшься',
'Уточни место ТВ у клиента — оно определяет центр симметрии стенки',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Спальня': {
icon: '🛏️',
params: [
{label:'Ширина стены под шкаф', hint:'Полная + за дверью если угловой', critical:true},
{label:'Высота потолка у стены', hint:'Именно у стены шкафа, не в центре', critical:true},
{label:'Плинтус', hint:'Высота + глубина — шкаф стоит на нём', critical:true},
{label:В1 — Проём двери', hint:'Высота от пола влияет на петли и полку', critical:true},
{label:'Углы (ЛН, ПН)', hint:'Угловой шкаф: проверь 90° обеих стен', critical:false},
{label:'R1 — Розетки в зоне шкафа', hint:'Не закрыть — горизонталь + высота', critical:false},
{label:'Sw1 / Ld1 — Выключатель', hint:'Положение на стене, высота от пола', critical:false},
],
tips: [
'Плинтус часто забывают — без подрезки цоколя шкаф не встанет вплотную',
'Угловой шкаф: замерь обе стены и проверь угол — 90° не всегда 90°',
'Высоту потолка снимай у стены, а не в центре — там может быть перепад',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Прихожая': {
icon: '🚪',
params: [
{label:'Ширина стены', hint:'Между коробками / стенами — от базы', critical:true},
{label:'Глубина коридора', hint:'Свободная в самом узком месте', critical:true},
{label:'Высота потолка у стены', hint:'У стены установки шкафа', critical:true},
{label:'Плинтус', hint:'Высота + глубина', critical:true},
{label:В1 — Все проёмы', hint:'Ширина + высота + отступ от базы до края', critical:true},
{label:'ЭЩ — Электрощиток', hint:'Если в зоне шкафа: размеры + расположение', critical:false},
{label:'Sw1 / Ld1 — Выключатель', hint:'Положение, высота — не закрыть шкафом', critical:false},
{label:'J1 — Распаечная коробка', hint:'Есть ли на стене шкафа — позиция', critical:false},
],
tips: [
'Минимум под шкаф-купе: 600 мм шкаф + 450 мм проход = 1050 мм свободно',
'Электрощиток закрывать нельзя — уточни его наличие в зоне шкафа сразу',
'Глубину коридора снимай в самом узком месте, не в среднем',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Ванная': {
icon: '🚿',
params: [
{label:'D1 / D1\' — Слив + фановая',hint:'Горизонталь от угла-базы + вертикаль от пола', critical:true},
{label:'Wc1 / Wh1 — Трубы', hint:'Вынос от стены + горизонталь от базы', critical:true},
{label:'Зона под тумбу / пенал', hint:'Ширина + глубина свободной зоны', critical:true},
{label:'Высота потолка', hint:'Для зеркал, шкафов, ниш', critical:false},
{label:'Rs1 — Силовая розетка', hint:'Под стиральную машину — позиция точно', critical:false},
{label:'R1 / Sw1 — Розетка, выкл.', hint:'Высота + горизонталь от базы', critical:false},
],
tips: [
'В плитке замеряй с учётом её толщины — обычно 1012 мм на сторону',
'D1\' (фановая труба) определяет минимальную высоту тумбы — зафиксируй точно',
'Wc1/Wh1 часто смещены от угла асимметрично — не угадывай, замерь',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Гардероб': {
icon: '👔',
params: [
{label:'Все 4 стены × 3 точки', hint:'Ширина у пола, середины, потолка каждой стены', critical:true},
{label:'Высота потолка у каждой стены',hint:'Перепад возможен — снимай у стены', critical:true},
{label:В1 — Проём', hint:'Ширина + высота + положение на стене', critical:true},
{label:'Углы — все 4', hint:'В градусах; в гардеробе углы часто не 90°', critical:false},
{label:'V — Вентиляция', hint:'Есть ли продух, расположение, размер', critical:false},
{label:'R1 / Sw1 / Ld1', hint:'Освещение и розетки внутри — позиции', critical:false},
],
tips: [
'Гардероб — самый сложный объект: каждая стена в трёх точках, без исключений',
'Минимальная глубина секции: 550 мм (плечики) + 50 мм зазор = 600 мм',
'Уточни у клиента: зона глажки? место чемодана? — это меняет всю планировку',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Балкон / Лоджия': {
icon: '🌿',
params: [
{label:'Высота до балконной балки', hint:'До балки — не до потолка! Критично для шкафа', critical:true},
{label:'Ширина × глубина', hint:'Ширина в 3 местах (часто трапеция)', critical:true},
{label:'Парапет', hint:'Высота + толщина + материал', critical:true},
{label:К1 — Балконный блок', hint:'Ширина + высота + тип открывания + вынос', critical:false},
{label:'R1 — Розетка', hint:'Есть ли, позиция', critical:false},
{label:'Ld1 — Освещение', hint:'Есть ли точка на потолке / стене', critical:false},
],
tips: [
'Балконная балка ограничивает высоту шкафа — замерь до неё, не до потолка',
'Трапеция: снимай ширину у стены дома, в середине и у остекления — три числа',
'Утеплён ли балкон — влияет на допустимые материалы мебели',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Кабинет': {
icon: '💼',
params: [
{label:'Стена под стол / стеллаж', hint:'Ширина + высота; от какого угла база', critical:true},
{label:'Высота потолка у стены', hint:'У стены установки', critical:true},
{label:В1 — Проём', hint:'Ширина + высота + отступ от угла', critical:true},
{label:'R1 × N — Розетки', hint:'Рабочее место: мин. 4 шт. — позиции точно', critical:false},
{label:'Net1 — Интернет', hint:'Горизонталь от базы + высота от пола', critical:false},
{label:'Tv1 — ТВ / монитор', hint:'Позиция на стене если планируется', critical:false},
{label:'Ld1 — Освещение', hint:'Точка на потолке для рабочего света', critical:false},
],
tips: [
'Розетки рабочего стола закроет мебель — фиксируй точно, потом не доберёшься',
'Минимум 4 розетки + Net1 в зоне стола — уточни у клиента заранее',
'Плинтус и стяжка: если стеллаж до потолка — высота у стены критична',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Детская': {
icon: '🧸',
params: [
{label:'Ширина стены под шкаф', hint:'Полная + за дверью если угловой', critical:true},
{label:'Высота потолка у стены', hint:'В старых домах потолок «гуляет» — снимай у стены',critical:true},
{label:'Плинтус', hint:'Высота + глубина', critical:true},
{label:В1 — Проём', hint:'Ширина + высота + отступ от угла', critical:true},
{label:'ОК1 — Окно', hint:'Высота подоконника + ширина + глубина', critical:false},
{label:'R1 — Розетки', hint:'Высота может быть нестандартной — уточни', critical:false},
{label:'Sw1 — Выключатели', hint:'У двери и у кровати — оба зафиксируй', critical:false},
],
tips: [
'Высота потолка у стены — особенно в старом доме: там перепады до 30 мм',
'Подоконник в детской часто используют как сиденье — уточни глубину у клиента',
'Розетки могут быть подняты «от детей» — зафиксируй фактическую высоту',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Санузел': {
icon: '🪠',
params: [
{label:'D1 / D1\' — Слив + фановая',hint:'Горизонталь от угла-базы + вертикаль от пола', critical:true},
{label:'Wc1 / Wh1 — Трубы', hint:'Вынос от стены + горизонталь от базы', critical:true},
{label:'Rs1 — Силовая под стиралку', hint:'Есть ли, позиция — если совмещённый санузел', critical:true},
{label:'Зона под тумбу / машину', hint:'Ширина + глубина свободной зоны', critical:true},
{label:'Высота потолка', hint:'Для зеркала-шкафа, навесных элементов', critical:false},
{label:'R1 / Sw1 — Розетка, выкл.', hint:'Влагозащищённые зоны — позиции', critical:false},
],
tips: [
'D1\' (фановая) определяет высоту тумбы — зафиксируй без допущений',
'Стиральная машина: Rs1 обязательна, уточни зону заранее у клиента',
'В плитке — учитывай её толщину ~1012 мм при измерении ниш и стен',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
'Столовая': {
icon: '🍽️',
params: [
{label:'Зона стол + стулья', hint:'Мин. 800 мм прохода с каждой стороны стола', critical:true},
{label:'Стена под сервант / буфет', hint:'Ширина + высота; угловой — обе стены', critical:true},
{label:'Высота потолка у стены', hint:'У стены установки мебели', critical:true},
{label:В1 / ОК1 — Проёмы', hint:'Ширина + высота + отступ от базы', critical:false},
{label:'Ld1 — Освещение над столом',hint:'Точка на потолке — горизонталь и вертикаль', critical:false},
{label:'R1 × N — Розетки', hint:'Под торшер, зарядки, подсветку', critical:false},
],
tips: [
'Ld1 (светильник над столом) определяет положение стола — фиксируй точно',
'Минимум 750 мм от края стола до стены или мебели — проверь реальный размер',
'Угловой сервант: замерь обе стены и угол в градусах',
],
source: ЕКЛИСТ_ЗАМЕРА v1.0'
},
};
// Signature canvas
function initSigCanvas() {
var cv = document.getElementById('sig-cv');
if (!cv) return;
var ctx = cv.getContext('2d');
cv.width = cv.offsetWidth;
ctx.strokeStyle = '#1e1b4b';
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
var drawing = false;
function pos(e) {
var r = cv.getBoundingClientRect();
var src = e.touches ? e.touches[0] : e;
return {x: (src.clientX - r.left) * (cv.width / r.width), y: (src.clientY - r.top) * (cv.height / r.height)};
}
function start(e) { e.preventDefault(); drawing = true; var p = pos(e); ctx.beginPath(); ctx.moveTo(p.x, p.y); }
function move(e) { e.preventDefault(); if (!drawing) return; var p = pos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); markSigned(); }
function end(e) { drawing = false; }
function markSigned() {
window._calcSigned = true;
var btn = document.getElementById('sig-confirm-btn');
if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
}
cv.addEventListener('touchstart', start, {passive:false});
cv.addEventListener('touchmove', move, {passive:false});
cv.addEventListener('touchend', end);
cv.addEventListener('mousedown', start);
cv.addEventListener('mousemove', move);
cv.addEventListener('mouseup', end);
}
// Init
navigate('home');
</script>
</body>
</html>