wasrusgen1-crm/docs/mockup_admin.html

1290 lines
85 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{--accent:#003E7E;--accent2:#76BD22;--bg:#F5F6F8;--card:#FFFFFF;--ink:#1A1A2E;--muted:#8A94A6;--danger:#EF4444;--warn:#F59E0B;--success:#10B981;--line:#E5E7EB;--s-success-bg:#ECFDF5;--s-warning-bg:#FFFBEB;--s-danger-bg:#FEF2F2;--s-info:#3B82F6;--s-info-bg:#EFF6FF}
body[data-theme="radar"]{--accent:#4338CA;--accent2:#6366F1;--bg:#F9FAFB;--card:#FFFFFF;--ink:#111827;--muted:#6B7280}
body[data-theme="dark"]{--accent:#4338CA;--accent2:#6366F1;--bg:#111827;--card:#1F2937;--ink:#F9FAFB;--muted:#9CA3AF}
#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}
#screen{flex:1;overflow-y:auto;overflow-x:hidden;scrollbar-width:none;background:var(--bg)}
#screen::-webkit-scrollbar{display:none}
.bottom-nav{height:60px;background:rgba(255,255,255,.95);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;z-index:100}
.nav-item{display:flex;flex-direction:column;align-items:center;gap:2px;cursor:pointer;padding:6px 10px;border-radius:10px;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,.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 var(--line);position:sticky;top:0;z-index:50}
.page-header h2{font-size:17px;font-weight:700;color:var(--ink);flex:1}
.pill{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:20px;font-size:11px;font-weight:700}
.pill.ok{background:rgba(16,185,129,.1);color:var(--success);border:1px solid rgba(16,185,129,.25)}
.pill.warn{background:rgba(245,158,11,.1);color:var(--warn);border:1px solid rgba(245,158,11,.25)}
.pill.danger{background:rgba(239,68,68,.08);color:var(--danger);border:1px solid rgba(239,68,68,.2)}
.pill.muted{background:var(--bg);color:var(--muted);border:1px solid var(--line)}
.pill.accent{background:rgba(0,62,126,.08);color:var(--accent);border:1px solid rgba(0,62,126,.2)}
.btn-primary{width:100%;background:var(--accent);color:#fff;border:none;border-radius:14px;padding:14px 20px;font-size:15px;font-weight:700;cursor:pointer}
.btn-ghost{background:var(--bg);color:var(--muted);border:1.5px solid var(--line);border-radius:12px;padding:10px 16px;font-size:13px;font-weight:600;cursor:pointer}
.chip-tabs{display:flex;gap:6px;padding:12px 16px;overflow-x:auto;scrollbar-width:none}
.chip-tabs::-webkit-scrollbar{display:none}
.chip-tab{padding:6px 14px;border-radius:20px;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;border:1.5px solid var(--line);background:var(--card);color:var(--muted);flex-shrink:0}
.chip-tab.active{background:var(--accent);color:#fff;border-color:var(--accent)}
@keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.anim{animation:fadeIn .2s ease}
/* Stock bar */
.stock-bar{height:4px;border-radius:2px;background:var(--line);overflow:hidden;margin-top:6px}
.stock-bar-fill{height:100%;border-radius:2px;transition:width .4s}
</style>
</head>
<body>
<div id="phoneFrame">
<div id="statusBar">
<span>9:41</span>
<div style="display:flex;align-items:center;gap:6px">
<svg width="16" height="12" viewBox="0 0 16 12" fill="currentColor"><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="0" width="2.5" height="12" rx="1" opacity=".3"/></svg>
<svg width="16" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="18" height="11" rx="2"/><path d="M20 11h2v4h-2"/></svg>
</div>
</div>
<div id="screen"></div>
<div class="bottom-nav" id="nav"></div>
</div>
<script>
// ── CONFIG (регулируется директором) ─────────────────────────────────────────
window._CONFIG = window._CONFIG || {
supplyApproval: false, // true = заявка уходит на согласование директору
inventoryPeriod: 30, // дней между плановыми инвентаризациями (0 = без расписания)
};
// ── ДАННЫЕ: ЗАКУПКИ ───────────────────────────────────────────────────────────
var _CAT = {
drink: {label:'Напитки', icon:'☕'},
office: {label:'Канцелярия', icon:'📎'},
clean: {label:'Хозяйство', icon:'🧹'},
misc: {label:'Расходники', icon:'📦'},
};
window._SUPPLIES = window._SUPPLIES || [
// Напитки
{id:'tea', cat:'drink', name:'Чай ассорти', unit:'пачка', cur:2, min:4, ordered:false},
{id:'coffee', cat:'drink', name:'Кофе молотый', unit:'пачка', cur:0, min:2, ordered:true, orderedDate:'20.05'},
{id:'sugar', cat:'drink', name:'Сахар', unit:'кг', cur:0.5,min:2, ordered:false},
{id:'candy', cat:'drink', name:'Конфеты', unit:'кг', cur:1, min:1.5,ordered:false},
{id:'cups', cat:'drink', name:'Одноразовые стаканы',unit:'уп', cur:3, min:2, ordered:false},
// Канцелярия
{id:'pens', cat:'office', name:'Ручки', unit:'шт', cur:4, min:10, ordered:false},
{id:'paper', cat:'office', name:'Бумага A4', unit:'пачка', cur:1, min:3, ordered:false},
{id:'folder', cat:'office', name:'Папки-скоросшиватели',unit:'шт', cur:6, min:5, ordered:false},
{id:'tape', cat:'office', name:'Скотч', unit:'рул', cur:2, min:2, ordered:false},
// Хозяйство
{id:'soap', cat:'clean', name:'Жидкое мыло', unit:'л', cur:0.3,min:1, ordered:false},
{id:'bags', cat:'clean', name:'Мусорные пакеты', unit:'рул', cur:1, min:3, ordered:false},
{id:'wet', cat:'clean', name:'Влажные салфетки', unit:'уп', cur:2, min:3, ordered:false},
{id:'cloth', cat:'clean', name:'Тряпки для уборки', unit:'шт', cur:4, min:4, ordered:false},
// Расходники
{id:'cart', cat:'misc', name:'Картридж принтера', unit:'шт', cur:0, min:1, ordered:true, orderedDate:'18.05'},
{id:'paper2', cat:'misc', name:'Термобумага', unit:'рул', cur:5, min:3, ordered:false},
{id:'bag2', cat:'misc', name:'Пакеты для клиентов',unit:'уп', cur:2, min:5, ordered:false},
];
// ── ДАННЫЕ: ИНВЕНТАРИЗАЦИЯ ───────────────────────────────────────────────────
var _ICAT = {tech:'Техника', furn:'Мебель'};
window._INVENTORY = window._INVENTORY || [
// Техника
{id:'tv1', icat:'tech', name:'Телевизор Samsung 55"', sn:'SN-2024-001', qty:1, cond:'ok', note:''},
{id:'pc1', icat:'tech', name:'Компьютер + монитор', sn:'PC-2023-042', qty:1, cond:'ok', note:''},
{id:'prn1', icat:'tech', name:'Принтер HP LaserJet', sn:'PR-2022-017', qty:1, cond:'warn', note:'Требует заправки'},
{id:'phone1', icat:'tech', name:'Телефон офисный', sn:'—', qty:2, cond:'ok', note:''},
{id:'cam1', icat:'tech', name:'IP-камера', sn:'CAM-001', qty:1, cond:'ok', note:''},
// Мебель
{id:'desk1', icat:'furn', name:'Стол менеджера', sn:'—', qty:3, cond:'ok', note:''},
{id:'chair1', icat:'furn', name:'Кресло (офис)', sn:'—', qty:4, cond:'warn', note:'У 1 кресла поврежден подлокотник'},
{id:'sofa1', icat:'furn', name:'Диван клиентский', sn:'—', qty:1, cond:'ok', note:''},
{id:'shelf1', icat:'furn', name:'Стеллаж образцов', sn:'—', qty:2, cond:'ok', note:''},
{id:'table1', icat:'furn', name:'Стол переговорный', sn:'—', qty:1, cond:'bad', note:'Скол на углу, нужна замена'},
];
window._LAST_INVENTORY = window._LAST_INVENTORY || '15.04.2026';
window._INV_SESSION = window._INV_SESSION || null;
// ── ДАННЫЕ: ПЕРСОНАЛ ──────────────────────────────────────────────────────────
var _DOW = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
var _DATES = [19,20,21,22,23,24,25]; // май 2026, неделя
var _TODAY = 22; // четверг
var _STAFF = [
{id:'anna', name:'Анна К.', short:'АК', color:'#3B82F6'},
{id:'petr', name:'Пётр В.', short:'ПВ', color:'#10B981'},
{id:'masha', name:'Мария С.', short:'МС', color:'#F59E0B'},
{id:'ivan', name:'Иван Д.', short:'ИД', color:'#8B5CF6'},
];
// ПЛАН: staffId → массив day-индексов (0=Пн…6=Вс)
window._PLAN = window._PLAN || {
anna: [0,1,4], // Пн Вт Пт
petr: [1,2,4], // Вт Ср Пт
masha: [0,3,4], // Пн Чт Пт
ivan: [2,3], // Ср Чт
};
// ФАКТ: staffId → {dayIndex: {in, out, gps:{ok,dist,forced}}}
window._FACT = window._FACT || {
anna: {
0:{in:'10:03',out:'19:12',gps:{ok:true,dist:47}},
1:{in:'10:18',out:null, gps:{ok:true,dist:89}},
},
petr: {
1:{in:'10:52',out:'19:05',gps:{ok:false,dist:1200,forced:true}},
2:{in:'09:58',out:'18:45',gps:{ok:true,dist:63}},
},
masha: {
0:{in:'10:01',out:'19:20',gps:{ok:true,dist:65}},
},
ivan: {},
};
window._staffView = window._staffView || 'week'; // 'week' | 'list'
window._staffSel = window._staffSel || null; // выбранный сотрудник для деталей
window._staffEditMode = window._staffEditMode !== undefined ? window._staffEditMode : false; // false = просмотр (как у ком.директора)
window._staffAddSheet = window._staffAddSheet || null; // {staffId} — быстрое добавление смены
// ── ДАННЫЕ: КАССА ─────────────────────────────────────────────────────────────
window._CASH_SHIFT = window._CASH_SHIFT || {
open: true,
openTime: '10:00',
openBalance: 12000, // начальный остаток (введён при открытии)
inkass: 0, // инкассация за смену
opener: 'Анна К.', // кто открыл смену
};
// История закрытых смен
window._CASH_HISTORY = window._CASH_HISTORY || [
{date:'16.05', dow:'Пт', open:'10:00', close:'19:50', opener:'Анна К.', openBal:10300, cashIn:22000, cardIn:41500, expense:800, inkass:20000, closeBal:11500},
{date:'19.05', dow:'Пн', open:'09:58', close:'19:30', opener:'Пётр В.', openBal:11500, cashIn:8500, cardIn:12000, expense:500, inkass:0, closeBal:19500},
{date:'20.05', dow:'Вт', open:'10:02', close:'20:05', opener:'Анна К.', openBal:19500, cashIn:31000, cardIn:28500, expense:1200, inkass:30000, closeBal:19300},
{date:'21.05', dow:'Ср', open:'10:05', close:'19:45', opener:'Мария С.',openBal:19300, cashIn:15000, cardIn:19000, expense:300, inkass:15000, closeBal:19000},
{date:'22.05', dow:'Чт', open:'10:00', close:null, opener:'Анна К.', openBal:12000, cashIn:null, cardIn:null, expense:null, inkass:null, closeBal:null}, // текущая
];
window._cashTab = window._cashTab || 'shift'; // 'shift' | 'history'
window._cashClosing = window._cashClosing || false; // экран подтверждения закрытия
window._cashOpening = window._cashOpening || false; // экран открытия смены
// Проведённые операции дня
window._TRANSACTIONS = window._TRANSACTIONS || [
{id:'t1', time:'10:23', type:'in', method:'card', amount:12000, manager:'Анна К.', client:'Иванов А.', note:'Оплата замера'},
{id:'t2', time:'11:47', type:'in', method:'cash', amount:8500, manager:'Пётр В.', client:'Сидорова М.', note:'Предоплата 50%'},
{id:'t3', time:'13:10', type:'out', method:'cash', amount:500, manager:null, client:null, note:'Закупка кофе и сахара'},
{id:'t4', time:'14:35', type:'in', method:'card', amount:25000, manager:'Мария С.', client:'Козлов Р.', note:'Финальный расчёт'},
];
// Ожидают подтверждения администратора (менеджер зафиксировал — касса ещё не приняла)
window._PENDING = window._PENDING || [
{id:'p1', managerId:'ivan', manager:'Иван Д.', client:'Морозова Е.', amount:15000, method:'cash', note:'Предоплата 50% · замер 25.05'},
{id:'p2', managerId:'anna', manager:'Анна К.', client:'Петрова С.', amount:8500, method:'card', note:'Доплата по договору № 117'},
];
window._cashOutScreen = window._cashOutScreen || false; // форма расхода
// ── ROUTER ────────────────────────────────────────────────────────────────────
window._screen = window._screen || 'cash';
window._supCat = window._supCat || 'all';
window._invCat = window._invCat || 'all';
window._supEdit = window._supEdit || null; // id редактируемой позиции
function _nav(s){ window._screen=s; _render(); }
function _render(){
document.getElementById('screen').innerHTML = _renderScreen();
document.getElementById('nav').innerHTML = _navBar();
}
// ── NAV BAR ───────────────────────────────────────────────────────────────────
function _navBar(){
var items=[
{id:'home', icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>', label:'Главная'},
{id:'supplies', icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>', label:'Закупки'},
{id:'inventory', icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/><path d="M7 8h10M7 12h6"/></svg>', label:'Инвентарь'},
{id:'staff', icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>', label:'Персонал'},
{id:'cash', icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M6 12h.01M18 12h.01"/></svg>', label:'Касса'},
];
// Бейджи
var needBuy = _SUPPLIES.filter(function(s){ return _stockLevel(s)==='danger'&&!s.ordered; }).length;
var invBad = _INVENTORY.filter(function(i){ return i.cond==='bad'; }).length;
// GPS нарушения: плановые смены прошлых дней без факта
var gpsIssue = 0;
_STAFF.forEach(function(st){
var plan = window._PLAN[st.id]||[];
var fact = window._FACT[st.id]||{};
plan.forEach(function(di){
if(di>=5) return;
var d = _DATES[di];
if(d < _TODAY && !fact[di]) gpsIssue++;
});
});
return items.map(function(it){
var badge = (it.id==='supplies'&&needBuy>0)
? '<div style="position:absolute;top:4px;right:6px;width:16px;height:16px;border-radius:50%;background:var(--danger);color:#fff;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center">'+needBuy+'</div>'
: (it.id==='inventory'&&invBad>0)
? '<div style="position:absolute;top:4px;right:6px;width:16px;height:16px;border-radius:50%;background:var(--warn);color:#fff;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center">'+invBad+'</div>'
: (it.id==='staff'&&gpsIssue>0)
? '<div style="position:absolute;top:4px;right:6px;width:16px;height:16px;border-radius:50%;background:var(--danger);color:#fff;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center">'+gpsIssue+'</div>'
: (it.id==='cash'&&(window._PENDING||[]).length>0)
? '<div style="position:absolute;top:4px;right:6px;width:16px;height:16px;border-radius:50%;background:var(--warn);color:#fff;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center">'+(window._PENDING||[]).length+'</div>'
: '';
return '<div class="nav-item'+(window._screen===it.id?' active':'')+'" onclick="_nav(\''+it.id+'\')" style="position:relative">'
+it.icon+badge
+'<span>'+it.label+'</span>'
+'</div>';
}).join('');
}
// ── STOCK HELPERS ──────────────────────────────────────────────────────────────
function _stockLevel(s){
if(s.cur<=0) return 'danger';
if(s.cur < s.min*0.6) return 'warn';
return 'ok';
}
function _stockPct(s){ return Math.min(100, Math.round((s.cur/s.min)*100)); }
function _stockColor(lvl){ return lvl==='danger'?'var(--danger)':lvl==='warn'?'var(--warn)':'var(--success)'; }
function _stockLabel(s){
var lvl=_stockLevel(s);
if(lvl==='danger') return s.cur<=0?'Нет':'Заканчивается';
if(lvl==='warn') return 'Мало';
return 'В норме';
}
// ── SCREEN ROUTER ─────────────────────────────────────────────────────────────
function _renderScreen(){
if(window._screen==='home') return _screenHome();
if(window._screen==='supplies') return _screenSupplies();
if(window._screen==='sup_new') return _screenSupplyNew();
if(window._screen==='inventory') return _screenInventory();
if(window._screen==='inv_check') return _screenInvCheck();
if(window._screen==='staff') return _screenStaff();
if(window._screen==='cash') return _screenCash();
return _screenHome();
}
// ── HOME ──────────────────────────────────────────────────────────────────────
function _screenHome(){
var needBuy = _SUPPLIES.filter(function(s){ return _stockLevel(s)!=='ok'&&!s.ordered; });
var ordered = _SUPPLIES.filter(function(s){ return s.ordered; });
var invBad = _INVENTORY.filter(function(i){ return i.cond==='bad'; });
var invWarn = _INVENTORY.filter(function(i){ return i.cond==='warn'; });
var alertsHtml = '';
if(needBuy.length){
alertsHtml += '<div style="margin:0 16px 10px;background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.2);border-radius:14px;padding:13px 14px;cursor:pointer" onclick="_nav(\'supplies\')">'
+'<div style="font-size:13px;font-weight:700;color:var(--danger);margin-bottom:4px">🛒 Нужно закупить — '+needBuy.length+' позиций</div>'
+'<div style="font-size:12px;color:var(--muted)">'+needBuy.slice(0,3).map(function(s){return s.name;}).join(', ')+(needBuy.length>3?' и ещё '+(needBuy.length-3):'')+'</div>'
+'</div>';
}
if(ordered.length){
alertsHtml += '<div style="margin:0 16px 10px;background:rgba(245,158,11,.07);border:1px solid rgba(245,158,11,.2);border-radius:14px;padding:13px 14px">'
+'<div style="font-size:13px;font-weight:700;color:var(--warn);margin-bottom:4px">📦 Ожидается доставка — '+ordered.length+' позиции</div>'
+'<div style="font-size:12px;color:var(--muted)">'+ordered.map(function(s){return s.name+(s.orderedDate?' ('+s.orderedDate+')':'');}).join(', ')+'</div>'
+'</div>';
}
if(invBad.length){
alertsHtml += '<div style="margin:0 16px 10px;background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.2);border-radius:14px;padding:13px 14px;cursor:pointer" onclick="_nav(\'inventory\')">'
+'<div style="font-size:13px;font-weight:700;color:var(--danger);margin-bottom:4px">⚠️ Требует внимания — '+invBad.length+' позиции</div>'
+'<div style="font-size:12px;color:var(--muted)">'+invBad.map(function(i){return i.name;}).join(', ')+'</div>'
+'</div>';
}
// Быстрая статистика
var okCount = _SUPPLIES.filter(function(s){ return _stockLevel(s)==='ok'; }).length;
var totalSup = _SUPPLIES.length;
var okInv = _INVENTORY.filter(function(i){ return i.cond==='ok'; }).length;
var totalInv = _INVENTORY.length;
var statsHtml =
'<div style="display:flex;gap:10px;padding:0 16px 14px">'
+'<div style="flex:1;background:var(--card);border-radius:14px;padding:13px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.06)" onclick="_nav(\'supplies\')">'
+'<div style="font-size:22px;font-weight:900;color:'+(needBuy.length?'var(--danger)':'var(--success)')+'">'+okCount+'/'+totalSup+'</div>'
+'<div style="font-size:11px;color:var(--muted);margin-top:2px">позиций OK</div>'
+'</div>'
+'<div style="flex:1;background:var(--card);border-radius:14px;padding:13px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.06)" onclick="_nav(\'inventory\')">'
+'<div style="font-size:22px;font-weight:900;color:'+(invBad.length?'var(--danger)':invWarn.length?'var(--warn)':'var(--success)')+'">'+okInv+'/'+totalInv+'</div>'
+'<div style="font-size:11px;color:var(--muted);margin-top:2px">инвентарь OK</div>'
+'</div>'
+'<div style="flex:1;background:var(--card);border-radius:14px;padding:13px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.06)">'
+'<div style="font-size:13px;font-weight:900;color:var(--muted)">'+window._LAST_INVENTORY+'</div>'
+'<div style="font-size:11px;color:var(--muted);margin-top:2px">последний осмотр</div>'
+'</div>'
+'</div>';
var quickHtml =
'<div style="padding:0 16px;display:flex;gap:10px;margin-bottom:4px">'
+'<button onclick="_nav(\'sup_new\')" class="btn-primary" style="flex:1;padding:12px;font-size:14px">🛒 Создать заявку</button>'
+'<button onclick="_startInvCheck()" style="flex:1;padding:12px;font-size:14px;background:rgba(0,62,126,.08);color:var(--accent);border:1.5px solid rgba(0,62,126,.2);border-radius:14px;font-weight:700;cursor:pointer">📋 Провести осмотр</button>'
+'</div>';
return '<div class="page anim">'
+'<div style="padding:20px 16px 8px;background:var(--card);border-bottom:1px solid var(--line)">'
+'<div style="font-size:12px;color:var(--muted);margin-bottom:2px">Пн, 19 мая 2026</div>'
+'<div style="font-size:22px;font-weight:800;color:var(--ink)">Добрый день, Анна 👋</div>'
+'<div style="font-size:13px;color:var(--muted);margin-top:2px">Администратор салона</div>'
+'</div>'
+'<div style="height:14px"></div>'
+statsHtml
+(alertsHtml||'<div style="margin:0 16px 10px;font-size:13px;color:var(--muted);text-align:center;padding:8px">✅ Всё в порядке</div>')
+'<div style="height:6px"></div>'
+quickHtml
+'</div>';
}
// ── ЗАКУПКИ ───────────────────────────────────────────────────────────────────
function _screenSupplies(){
var cat = window._supCat;
var cats = [['all','Все']].concat(Object.keys(_CAT).map(function(k){ return [k, _CAT[k].icon+' '+_CAT[k].label]; }));
var filtered = cat==='all' ? _SUPPLIES : _SUPPLIES.filter(function(s){ return s.cat===cat; });
var tabsHtml = '<div class="chip-tabs">'
+cats.map(function(c){
var cnt = (c[0]==='all'?_SUPPLIES:_SUPPLIES.filter(function(s){return s.cat===c[0];})).filter(function(s){return _stockLevel(s)!=='ok';}).length;
return '<div class="chip-tab'+(cat===c[0]?' active':'')+'" onclick="window._supCat=\''+c[0]+'\';_render()">'+c[1]+(cnt>0?' <b style="color:'+(cat===c[0]?'rgba(255,255,255,.8)':'var(--danger)')+'">'+cnt+'</b>':'')+'</div>';
}).join('')
+'</div>';
var listHtml = filtered.map(function(s){
var lvl = _stockLevel(s);
var pct = _stockPct(s);
var col = _stockColor(lvl);
var badge = s.ordered
? '<span class="pill warn">📦 заказано'+(s.orderedDate?' '+s.orderedDate:'')+'</span>'
: lvl==='danger' ? '<span class="pill danger">'+_stockLabel(s)+'</span>'
: lvl==='warn' ? '<span class="pill warn">'+_stockLabel(s)+'</span>'
: '<span class="pill ok">В норме</span>';
return '<div style="background:var(--card);margin:0 16px 8px;border-radius:14px;padding:13px 14px;border-left:3px solid '+col+'">'
+'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">'
+'<div>'
+'<div style="font-size:14px;font-weight:600;color:var(--ink)">'+s.name+'</div>'
+'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+s.cur+' '+s.unit+' · мин. '+s.min+' '+s.unit+'</div>'
+'</div>'
+badge
+'</div>'
+'<div class="stock-bar"><div class="stock-bar-fill" style="width:'+pct+'%;background:'+col+'"></div></div>'
+'<div style="display:flex;gap:8px;margin-top:10px">'
+(s.ordered
? '<button onclick="_markReceived(\''+s.id+'\')" style="flex:1;padding:7px;border-radius:10px;border:none;background:rgba(16,185,129,.1);color:var(--success);font-size:12px;font-weight:700;cursor:pointer">✓ Получено</button>'
: '<button onclick="_addToOrder(\''+s.id+'\')" style="flex:1;padding:7px;border-radius:10px;border:none;background:rgba(0,62,126,.08);color:var(--accent);font-size:12px;font-weight:700;cursor:pointer">+ В заявку</button>')
+'<button onclick="_editStock(\''+s.id+'\')" style="padding:7px 12px;border-radius:10px;border:1px solid var(--line);background:var(--bg);color:var(--muted);font-size:12px;font-weight:600;cursor:pointer">Ред.</button>'
+'</div>'
+'</div>';
}).join('');
var needCnt = _SUPPLIES.filter(function(s){ return _stockLevel(s)!=='ok'&&!s.ordered; }).length;
return '<div class="page anim">'
+'<div class="page-header">'
+'<h2>📦 Закупки</h2>'
+(needCnt>0?'<span class="pill danger">'+needCnt+' нужно</span>':'<span class="pill ok">всё ок</span>')
+'</div>'
+tabsHtml
+listHtml
+'<div style="padding:12px 16px">'
+'<button onclick="_nav(\'sup_new\')" class="btn-primary">🛒 Создать заявку на закупку</button>'
+'</div>'
+'</div>';
}
// ── ЗАЯВКА НА ЗАКУПКУ ─────────────────────────────────────────────────────────
function _screenSupplyNew(){
var needItems = _SUPPLIES.filter(function(s){ return _stockLevel(s)!=='ok'&&!s.ordered; });
window._orderSel = window._orderSel || {};
// Инициализируем выбор
needItems.forEach(function(s){ if(!(s.id in window._orderSel)) window._orderSel[s.id]=true; });
var itemsHtml = needItems.length ? needItems.map(function(s){
var sel = window._orderSel[s.id];
return '<div style="display:flex;align-items:center;gap:12px;padding:11px 14px;border-bottom:1px solid rgba(0,0,0,.04);cursor:pointer" onclick="window._orderSel[\''+s.id+'\']=!window._orderSel[\''+s.id+'\'];_render()">'
+'<div style="width:22px;height:22px;border-radius:6px;border:2px solid '+(sel?'var(--accent)':'#CBD5E1')+';background:'+(sel?'var(--accent)':'transparent')+';display:flex;align-items:center;justify-content:center;flex-shrink:0">'
+(sel?'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>':'')
+'</div>'
+'<div style="flex:1">'
+'<div style="font-size:14px;font-weight:600;color:var(--ink)">'+s.name+'</div>'
+'<div style="font-size:11px;color:var(--muted)">Есть: '+s.cur+' '+s.unit+' · Нужно: '+s.min+' '+s.unit+'</div>'
+'</div>'
+'<span class="pill '+(s.cur<=0?'danger':'warn')+'">'+_stockLabel(s)+'</span>'
+'</div>';
}).join('')
: '<div style="padding:20px 14px;color:var(--muted);font-size:13px;text-align:center">Все позиции в норме или уже заказаны</div>';
var selCnt = Object.keys(window._orderSel||{}).filter(function(k){return window._orderSel[k];}).length;
return '<div class="page anim">'
+'<div class="page-header">'
+'<button class="back-btn" onclick="_nav(\'supplies\')" style="background:var(--bg);border:none;cursor:pointer;border-radius:50%;width:32px;height:32px;display:flex;align-items:center;justify-content:center"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg></button>'
+'<h2>Новая заявка</h2>'
+'<span class="pill accent">'+selCnt+' позиций</span>'
+'</div>'
+'<div style="margin:12px 16px 8px;font-size:12px;color:var(--muted)">Выберите позиции для заказа:</div>'
+'<div style="background:var(--card);margin:0 16px;border-radius:14px;overflow:hidden">'
+itemsHtml
+'</div>'
+(selCnt>0
? '<div style="padding:14px 16px">'
+'<button onclick="_submitOrder()" class="btn-primary">✓ Оформить заявку ('+selCnt+' поз.)</button>'
+'<div style="font-size:11px;color:var(--muted);text-align:center;margin-top:8px">'
+(window._CONFIG.supplyApproval?'Заявка уйдёт на согласование директору':'Заявка фиксируется как «заказано»')
+'</div>'
+'</div>'
: '')
+'</div>';
}
// ── ИНВЕНТАРИЗАЦИЯ ────────────────────────────────────────────────────────────
function _screenInventory(){
var cat = window._invCat;
var cats = [['all','Все'],['tech','🖥 Техника'],['furn','🪑 Мебель']];
var filtered = cat==='all' ? _INVENTORY : _INVENTORY.filter(function(i){ return i.icat===cat; });
var tabsHtml = '<div class="chip-tabs">'
+cats.map(function(c){
var bad = (c[0]==='all'?_INVENTORY:_INVENTORY.filter(function(i){return i.icat===c[0];})).filter(function(i){return i.cond!=='ok';}).length;
return '<div class="chip-tab'+(cat===c[0]?' active':'')+'" onclick="window._invCat=\''+c[0]+'\';_render()">'+c[1]+(bad>0?' <b style="color:'+(cat===c[0]?'rgba(255,255,255,.8)':'var(--danger)')+'">'+bad+'</b>':'')+'</div>';
}).join('')
+'</div>';
var condIcon = {ok:'✅', warn:'⚠️', bad:'❌'};
var condLabel = {ok:'В норме', warn:'Нужен осмотр', bad:'Требует замены'};
var condPill = {ok:'ok', warn:'warn', bad:'danger'};
var listHtml = filtered.map(function(item){
return '<div style="background:var(--card);margin:0 16px 8px;border-radius:14px;padding:13px 14px;border-left:3px solid '+(item.cond==='ok'?'var(--success)':item.cond==='warn'?'var(--warn)':'var(--danger)')+'">'
+'<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px">'
+'<div style="flex:1">'
+'<div style="font-size:14px;font-weight:600;color:var(--ink)">'+item.name+'</div>'
+'<div style="font-size:11px;color:var(--muted);margin-top:2px">'
+(item.sn&&item.sn!=='—'?'SN: '+item.sn+' · ':'')
+'Кол-во: '+item.qty+' шт.'
+'</div>'
+(item.note?'<div style="font-size:12px;color:var(--warn);margin-top:4px">'+item.note+'</div>':'')
+'</div>'
+'<span class="pill '+condPill[item.cond]+'">'+condIcon[item.cond]+' '+condLabel[item.cond]+'</span>'
+'</div>'
+'<div style="display:flex;gap:8px;margin-top:10px">'
+(item.cond!=='ok'?'<button onclick="_markFixed(\''+item.id+'\')" style="flex:1;padding:7px;border-radius:10px;border:none;background:rgba(16,185,129,.1);color:var(--success);font-size:12px;font-weight:700;cursor:pointer">✓ Устранено</button>':'')
+'<button onclick="_changeCond(\''+item.id+'\')" style="'+(item.cond!=='ok'?'':'flex:1;')+'padding:7px 12px;border-radius:10px;border:1px solid var(--line);background:var(--bg);color:var(--muted);font-size:12px;font-weight:600;cursor:pointer">Изменить</button>'
+'</div>'
+'</div>';
}).join('');
var badCnt = _INVENTORY.filter(function(i){ return i.cond!=='ok'; }).length;
return '<div class="page anim">'
+'<div class="page-header">'
+'<h2>📋 Инвентаризация</h2>'
+(badCnt>0?'<span class="pill danger">'+badCnt+' проблем</span>':'<span class="pill ok">всё ок</span>')
+'</div>'
+'<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 16px 0">'
+'<div style="font-size:12px;color:var(--muted)">Последний осмотр: <b style="color:var(--ink)">'+window._LAST_INVENTORY+'</b></div>'
+'<button onclick="_startInvCheck()" style="padding:6px 12px;border-radius:20px;border:1.5px solid rgba(0,62,126,.2);background:rgba(0,62,126,.08);color:var(--accent);font-size:12px;font-weight:700;cursor:pointer">+ Провести осмотр</button>'
+'</div>'
+tabsHtml
+listHtml
+'</div>';
}
// ── ОСМОТР (чек-лист) ────────────────────────────────────────────────────────
function _screenInvCheck(){
window._checkResults = window._checkResults || {};
var items = _INVENTORY;
var total = items.length;
var done = Object.keys(window._checkResults).length;
var listHtml = items.map(function(item, idx){
var r = window._checkResults[item.id];
var checked = r!==undefined;
return '<div style="background:var(--card);margin:0 16px 6px;border-radius:12px;padding:12px 14px;opacity:'+(checked?.7:1)+'">'
+'<div style="display:flex;align-items:center;gap:10px">'
+'<div style="width:24px;height:24px;border-radius:50%;border:2px solid '+(checked?'var(--success)':'#CBD5E1')+';background:'+(checked?'var(--success)':'transparent')+';display:flex;align-items:center;justify-content:center;flex-shrink:0">'
+(checked?'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>':'')
+'</div>'
+'<div style="flex:1">'
+'<div style="font-size:13px;font-weight:600;color:var(--ink)">'+item.name+'</div>'
+'<div style="font-size:11px;color:var(--muted)">'+item.qty+' шт.'+(item.sn&&item.sn!=='—'?' · '+item.sn:'')+'</div>'
+'</div>'
+(checked
? '<span class="pill '+(r==='ok'?'ok':r==='warn'?'warn':'danger')+'">'+(r==='ok'?'✅ ОК':r==='warn'?'⚠️ Замечание':'❌ Проблема')+'</span>'
: '<div style="display:flex;gap:5px">'
+'<button onclick="_checkItem(\''+item.id+'\',\'ok\')" style="padding:5px 9px;border-radius:8px;border:none;background:rgba(16,185,129,.12);color:var(--success);font-size:12px;font-weight:700;cursor:pointer">✅</button>'
+'<button onclick="_checkItem(\''+item.id+'\',\'warn\')" style="padding:5px 9px;border-radius:8px;border:none;background:rgba(245,158,11,.12);color:var(--warn);font-size:12px;font-weight:700;cursor:pointer">⚠️</button>'
+'<button onclick="_checkItem(\''+item.id+'\',\'bad\')" style="padding:5px 9px;border-radius:8px;border:none;background:rgba(239,68,68,.09);color:var(--danger);font-size:12px;font-weight:700;cursor:pointer">❌</button>'
+'</div>')
+'</div>'
+'</div>';
}).join('');
var progress = Math.round((done/total)*100);
return '<div class="page anim">'
+'<div class="page-header">'
+'<button onclick="_cancelCheck()" style="background:var(--bg);border:none;cursor:pointer;border-radius:50%;width:32px;height:32px;display:flex;align-items:center;justify-content:center"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg></button>'
+'<h2>Осмотр инвентаря</h2>'
+'<span class="pill accent">'+done+'/'+total+'</span>'
+'</div>'
+'<div style="padding:10px 16px 4px">'
+'<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:5px"><span>Прогресс</span><span>'+progress+'%</span></div>'
+'<div style="height:5px;background:var(--line);border-radius:3px;overflow:hidden"><div style="height:100%;background:var(--accent);border-radius:3px;width:'+progress+'%;transition:width .4s"></div></div>'
+'</div>'
+listHtml
+(done===total
? '<div style="padding:12px 16px"><button onclick="_finishCheck()" class="btn-primary">✅ Завершить осмотр и сохранить</button></div>'
: '<div style="padding:12px 16px;text-align:center;font-size:12px;color:var(--muted)">Отметьте все позиции для завершения</div>')
+'</div>';
}
// ── КАССА ────────────────────────────────────────────────────────────────────
function _fmtMoney(n){ return n.toLocaleString('ru-RU')+' ₽'; }
function _cashCalc(){
var sh = window._CASH_SHIFT;
var txns = window._TRANSACTIONS||[];
var cashIn = txns.filter(function(t){return t.type==='in' &&t.method==='cash';}).reduce(function(s,t){return s+t.amount;},0);
var cardIn = txns.filter(function(t){return t.type==='in' &&t.method==='card';}).reduce(function(s,t){return s+t.amount;},0);
var cashOut = txns.filter(function(t){return t.type==='out'&&t.method==='cash';}).reduce(function(s,t){return s+t.amount;},0);
var cardOut = txns.filter(function(t){return t.type==='out'&&t.method==='card';}).reduce(function(s,t){return s+t.amount;},0);
var inkass = sh.inkass||0;
var cashBal = sh.openBalance + cashIn - cashOut - inkass;
var total = sh.openBalance + cashIn + cardIn - cashOut - cardOut - inkass;
return {cashIn:cashIn, cardIn:cardIn, cashOut:cashOut, cardOut:cardOut, inkass:inkass, cashBal:cashBal, total:total};
}
function _screenCash(){
var sh = window._CASH_SHIFT;
var pend = window._PENDING||[];
var txns = window._TRANSACTIONS||[];
// ── Открытие смены ──
if(window._cashOpening){
return '<div class="page anim">'
+'<div class="page-header">'
+'<button onclick="window._cashOpening=false;_render()" style="background:var(--bg);border:none;cursor:pointer;border-radius:50%;width:32px;height:32px;display:flex;align-items:center;justify-content:center"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg></button>'
+'<h2>Открытие смены</h2>'
+'</div>'
+'<div style="padding:20px 16px;display:flex;flex-direction:column;gap:16px">'
+'<div style="background:rgba(0,62,126,.06);border-radius:14px;padding:14px;font-size:13px;color:var(--muted);line-height:1.5">'
+'Введите сумму наличных в кассе на начало смены. Эта сумма будет зафиксирована как начальный остаток.'
+'</div>'
+'<div>'
+'<div style="font-size:12px;font-weight:600;color:var(--muted);margin-bottom:6px">Начальный остаток (наличные), ₽</div>'
+'<input id="openBal" type="number" placeholder="0" value="12000" style="width:100%;border:1.5px solid var(--accent);border-radius:12px;padding:14px;font-size:24px;font-weight:800;color:var(--ink);outline:none;background:var(--card);text-align:center">'
+'</div>'
+'<button onclick="_confirmOpenShift()" class="btn-primary">🔓 Открыть смену</button>'
+'</div></div>';
}
// ── Закрытие смены (сводка) ──
if(window._cashClosing){
var c = _cashCalc();
var rows = [
['Начальный остаток (нал)', c.cashBal - c.cashIn + c.cashOut + c.inkass, 'var(--muted)'],
['+ Приход наличными', c.cashIn, 'var(--success)'],
['+ Приход картой', c.cardIn, 'var(--success)'],
[' Расходы (нал)', c.cashOut, 'var(--danger)'],
[' Расходы (карта)', c.cardOut, 'var(--danger)'],
[' Инкассация', c.inkass, 'var(--warn)'],
].filter(function(r){return r[1]>0;});
var tableHtml = rows.map(function(r){
return '<div style="display:flex;justify-content:space-between;padding:9px 0;border-bottom:1px solid rgba(0,0,0,.05)">'
+'<span style="font-size:13px;color:var(--muted)">'+r[0]+'</span>'
+'<span style="font-size:13px;font-weight:700;color:'+r[2]+'">'+_fmtMoney(r[1])+'</span>'
+'</div>';
}).join('');
return '<div class="page anim">'
+'<div class="page-header">'
+'<button onclick="window._cashClosing=false;_render()" style="background:var(--bg);border:none;cursor:pointer;border-radius:50%;width:32px;height:32px;display:flex;align-items:center;justify-content:center"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg></button>'
+'<h2>Закрытие смены</h2>'
+'</div>'
+'<div style="padding:16px">'
+'<div style="background:var(--card);border-radius:16px;padding:16px;margin-bottom:14px">'
+'<div style="font-size:11px;color:var(--muted);margin-bottom:4px">Конечный остаток в кассе</div>'
+'<div style="font-size:32px;font-weight:900;color:var(--ink);margin-bottom:4px">'+_fmtMoney(c.cashBal)+'</div>'
+'<div style="font-size:11px;color:var(--muted)">наличных · карта не хранится в кассе</div>'
+'</div>'
+'<div style="background:var(--card);border-radius:16px;padding:14px 16px;margin-bottom:14px">'
+tableHtml
+'<div style="display:flex;justify-content:space-between;padding:10px 0 0">'
+'<span style="font-size:14px;font-weight:700;color:var(--ink)">Итого оборот</span>'
+'<span style="font-size:15px;font-weight:900;color:var(--accent)">'+_fmtMoney(c.cashIn+c.cardIn)+'</span>'
+'</div>'
+'</div>'
+'<div style="background:rgba(245,158,11,.07);border:1px solid rgba(245,158,11,.2);border-radius:14px;padding:13px 14px;margin-bottom:16px">'
+'<div style="font-size:12px;font-weight:700;color:var(--warn);margin-bottom:4px">Инкассация за смену</div>'
+'<div style="font-size:11px;color:var(--muted);margin-bottom:8px">Укажите сумму наличных, изъятых для сдачи в банк (может быть 0)</div>'
+'<input id="inkassAmt" type="number" placeholder="0" value="'+(sh.inkass||0)+'" style="width:100%;border:1.5px solid rgba(245,158,11,.4);border-radius:10px;padding:10px 12px;font-size:18px;font-weight:700;color:var(--ink);outline:none;background:#fff" oninput="window._CASH_SHIFT.inkass=parseFloat(this.value)||0;_render()">'
+'</div>'
+'<button onclick="_confirmCloseShift()" style="width:100%;padding:14px;border-radius:14px;border:none;background:var(--accent);color:#fff;font-size:15px;font-weight:700;cursor:pointer">🔒 Подтвердить закрытие</button>'
+'</div></div>';
}
// ── Смена закрыта — открыть ──
if(!sh.open){
return '<div class="page anim">'
+'<div class="page-header"><h2>💰 Касса</h2></div>'
+'<div style="display:flex;gap:8px;padding:0 16px 0;margin-bottom:0"><div class="chip-tab '+(window._cashTab==='shift'?'active':'')+'" onclick="window._cashTab=\'shift\';_render()">Смена</div><div class="chip-tab '+(window._cashTab==='history'?'active':'')+'" onclick="window._cashTab=\'history\';_render()">История</div></div>'
+(window._cashTab==='history' ? _cashHistory()
: '<div style="padding:32px 24px;text-align:center">'
+'<div style="font-size:48px;margin-bottom:12px">🔒</div>'
+'<div style="font-size:15px;font-weight:700;color:var(--ink);margin-bottom:6px">Смена не открыта</div>'
+'<div style="font-size:13px;color:var(--muted);margin-bottom:24px">Для работы с кассой откройте смену</div>'
+'<button onclick="window._cashOpening=true;_render()" class="btn-primary">🔓 Открыть смену</button>'
+'</div>')
+'</div>';
}
// ── Форма расхода ──
if(window._cashOutScreen){
return '<div class="page anim">'
+'<div class="page-header">'
+'<button onclick="window._cashOutScreen=false;_render()" style="background:var(--bg);border:none;cursor:pointer;border-radius:50%;width:32px;height:32px;display:flex;align-items:center;justify-content:center"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg></button>'
+'<h2>Расход из кассы</h2>'
+'</div>'
+'<div style="padding:20px 16px;display:flex;flex-direction:column;gap:14px">'
+'<div><div style="font-size:12px;font-weight:600;color:var(--muted);margin-bottom:6px">Сумма, ₽</div>'
+'<input id="outAmt" type="number" placeholder="0" style="width:100%;border:1.5px solid var(--line);border-radius:12px;padding:12px 14px;font-size:24px;font-weight:800;color:var(--ink);outline:none;background:var(--card);text-align:center" oninput="this.style.borderColor=\'var(--accent)\'"></div>'
+'<div><div style="font-size:12px;font-weight:600;color:var(--muted);margin-bottom:6px">Назначение</div>'
+'<input id="outNote" type="text" placeholder="Закупка, доставка, прочее…" style="width:100%;border:1.5px solid var(--line);border-radius:12px;padding:12px 14px;font-size:14px;color:var(--ink);outline:none;background:var(--card)" oninput="this.style.borderColor=\'var(--accent)\'"></div>'
+'<div><div style="font-size:12px;font-weight:600;color:var(--muted);margin-bottom:8px">Способ</div>'
+'<div style="display:flex;gap:8px">'
+'<div id="omCash" onclick="_setOutMethod(\'cash\')" style="flex:1;padding:11px;border-radius:12px;border:1.5px solid var(--accent);background:rgba(0,62,126,.07);text-align:center;cursor:pointer;font-size:13px;font-weight:700;color:var(--accent)">💵 Наличные</div>'
+'<div id="omCard" onclick="_setOutMethod(\'card\')" style="flex:1;padding:11px;border-radius:12px;border:1.5px solid var(--line);background:var(--card);text-align:center;cursor:pointer;font-size:13px;font-weight:700;color:var(--muted)">💳 Карта</div>'
+'</div></div>'
// Фото чека
+'<div><div style="font-size:12px;font-weight:600;color:var(--muted);margin-bottom:8px">Фото чека <span style="font-weight:400">(опционально)</span></div>'
+'<div id="receiptBlock" onclick="_attachReceipt()" style="border:1.5px dashed var(--line);border-radius:12px;padding:16px;text-align:center;cursor:pointer;background:var(--card)">'
+(window._receiptAttached
? '<div style="color:var(--success);font-size:13px;font-weight:700">📎 receipt_22_05.jpg · прикреплён</div><div style="font-size:11px;color:var(--muted);margin-top:4px">Нажми чтобы заменить</div>'
: '<div style="font-size:22px;margin-bottom:6px">📷</div><div style="font-size:13px;font-weight:600;color:var(--muted)">Прикрепить фото чека</div><div style="font-size:11px;color:var(--muted);margin-top:2px">jpg, png до 10 МБ</div>')
+'</div></div>'
+'<button onclick="_submitExpense()" class="btn-primary" style="margin-top:4px">Провести расход</button>'
+'</div></div>';
}
// ── Основной экран смены ──
var c = _cashCalc();
// Сводная карточка
var summaryHtml =
'<div style="margin:12px 16px 0;background:var(--accent);border-radius:18px;padding:16px;color:#fff">'
+'<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:4px">'
+'<div style="font-size:11px;opacity:.7">Касса · смена с '+sh.openTime+' · '+sh.opener+'</div>'
+'</div>'
+'<div style="font-size:28px;font-weight:900;margin-bottom:2px">'+_fmtMoney(c.total)+'</div>'
+'<div style="font-size:11px;opacity:.65;margin-bottom:12px">общий баланс · нал. в ящике: '+_fmtMoney(c.cashBal)+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1px;background:rgba(255,255,255,.15);border-radius:12px;overflow:hidden">'
+'<div style="background:rgba(0,0,0,.2);padding:9px 10px">'
+'<div style="font-size:9px;opacity:.7;margin-bottom:2px">Нач. остаток</div>'
+'<div style="font-size:13px;font-weight:800">'+_fmtMoney(sh.openBalance)+'</div>'
+'</div>'
+'<div style="background:rgba(0,0,0,.2);padding:9px 10px">'
+'<div style="font-size:9px;opacity:.7;margin-bottom:2px">Приход</div>'
+'<div style="font-size:13px;font-weight:800">+'+_fmtMoney(c.cashIn+c.cardIn)+'</div>'
+'</div>'
+'<div style="background:rgba(0,0,0,.2);padding:9px 10px">'
+'<div style="font-size:9px;opacity:.7;margin-bottom:2px">Расход</div>'
+'<div style="font-size:13px;font-weight:800">'+_fmtMoney(c.cashOut+c.cardOut)+'</div>'
+'</div>'
+'</div>'
+'</div>';
// Ожидают оплаты
var pendHtml = '';
if(pend.length){
pendHtml = '<div style="padding:10px 16px 0">'
+'<div style="font-size:12px;font-weight:700;color:var(--warn);margin-bottom:8px">⏳ Ожидают оплаты — '+pend.length+'</div>'
+pend.map(function(p){
var mColor=(_STAFF.find(function(s){return s.id===p.managerId;})||{color:'#888'}).color;
var mShort=(_STAFF.find(function(s){return s.id===p.managerId;})||{short:'?'}).short;
return '<div style="background:var(--card);border-radius:14px;padding:12px 14px;margin-bottom:8px;border-left:3px solid var(--warn)">'
+'<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">'
+'<div style="width:30px;height:30px;border-radius:50%;background:'+mColor+';display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;color:#fff;flex-shrink:0">'+mShort+'</div>'
+'<div style="flex:1;min-width:0">'
+'<div style="font-size:13px;font-weight:700;color:var(--ink)">'+p.client+'</div>'
+'<div style="font-size:11px;color:var(--muted)">'+p.manager+' · '+(p.method==='cash'?'💵 Нал':'💳 Карта')+'</div>'
+'</div>'
+'<div style="font-size:15px;font-weight:900;color:var(--ink)">'+_fmtMoney(p.amount)+'</div>'
+'</div>'
+'<div style="display:flex;gap:8px">'
+'<button onclick="_acceptPayment(\''+p.id+'\')" style="flex:1;padding:8px;border-radius:10px;border:none;background:var(--success);color:#fff;font-size:13px;font-weight:700;cursor:pointer">✓ Принять</button>'
+'<button onclick="_rejectPayment(\''+p.id+'\')" style="padding:8px 14px;border-radius:10px;border:1.5px solid rgba(239,68,68,.3);background:rgba(239,68,68,.06);color:var(--danger);font-size:13px;font-weight:700;cursor:pointer">✕</button>'
+'</div>'
+'</div>';
}).join('')
+'</div>';
}
// Операции + кнопки действий
var actHtml = '<div style="display:flex;gap:8px;padding:10px 16px 4px">'
+'<button onclick="window._cashOutScreen=true;window._receiptAttached=false;_render()" style="flex:1;padding:9px;border-radius:12px;border:1.5px solid rgba(0,62,126,.2);background:rgba(0,62,126,.07);color:var(--accent);font-size:12px;font-weight:700;cursor:pointer">↑ Расход</button>'
+'<button onclick="_doInkass()" style="flex:1;padding:9px;border-radius:12px;border:1.5px solid rgba(245,158,11,.3);background:rgba(245,158,11,.07);color:var(--warn);font-size:12px;font-weight:700;cursor:pointer">🏦 Инкассация</button>'
+'<button onclick="window._cashClosing=true;_render()" style="flex:1;padding:9px;border-radius:12px;border:1.5px solid rgba(239,68,68,.25);background:rgba(239,68,68,.05);color:var(--danger);font-size:12px;font-weight:700;cursor:pointer">🔒 Закрыть</button>'
+'</div>';
var txHtml = '<div style="padding:0 16px">'
+'<div style="font-size:12px;font-weight:700;color:var(--muted);margin-bottom:8px">Операции сегодня — '+txns.length+'</div>'
+(sh.inkass>0?'<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid rgba(0,0,0,.04)">'
+'<div style="width:32px;height:32px;border-radius:50%;background:rgba(245,158,11,.1);display:flex;align-items:center;justify-content:center;flex-shrink:0"><span style="font-size:13px">🏦</span></div>'
+'<div style="flex:1"><div style="font-size:13px;font-weight:600;color:var(--ink)">Инкассация</div>'
+'<div style="font-size:11px;color:var(--muted)">Изъятие наличных из кассы</div></div>'
+'<div style="font-size:15px;font-weight:800;color:var(--warn)">'+_fmtMoney(sh.inkass)+'</div>'
+'</div>':'')
+txns.slice().reverse().map(function(t){
var isIn=t.type==='in';
var mIcon=t.method==='cash'?'💵':'💳';
var hasReceipt=t.receipt;
return '<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid rgba(0,0,0,.04)">'
+'<div style="width:32px;height:32px;border-radius:50%;background:'+(isIn?'rgba(16,185,129,.1)':'rgba(239,68,68,.08)')+';display:flex;align-items:center;justify-content:center;flex-shrink:0">'
+'<span style="font-size:13px">'+(isIn?'↓':'↑')+'</span></div>'
+'<div style="flex:1;min-width:0">'
+'<div style="font-size:13px;font-weight:600;color:var(--ink)">'+(t.client||t.note)+'</div>'
+'<div style="font-size:11px;color:var(--muted)">'+t.time+(t.manager?' · '+t.manager:'')+' · '+mIcon+(hasReceipt?' · 📎':'')+'</div>'
+(t.client&&t.note?'<div style="font-size:11px;color:var(--muted)">'+t.note+'</div>':'')
+'</div>'
+'<div style="font-size:14px;font-weight:800;color:'+(isIn?'var(--success)':'var(--danger)')+';flex-shrink:0">'
+(isIn?'+':'')+_fmtMoney(t.amount)+'</div>'
+'</div>';
}).join('')
+'</div>';
return '<div class="page anim">'
+'<div class="page-header">'
+'<h2>💰 Касса</h2>'
+'<span class="pill ok" style="white-space:nowrap">Смена открыта</span>'
+'<button onclick="window._cashTab=\'history\';window._cashClosing=false;window._cashOutScreen=false;_nav(\'cash\')" style="border:none;background:var(--bg);border-radius:20px;padding:6px 10px;font-size:11px;font-weight:700;color:var(--muted);cursor:pointer">История</button>'
+'</div>'
+summaryHtml
+pendHtml
+actHtml
+txHtml
+'</div>';
}
function _cashHistory(){
var hist = (window._CASH_HISTORY||[]).slice().reverse();
return '<div style="padding:8px 16px">'
+hist.map(function(h){
var totalIn = (h.cashIn||0)+(h.cardIn||0);
var totalOut = (h.expense||0)+(h.inkass||0);
var isCur = !h.close;
return '<div style="background:var(--card);border-radius:14px;padding:13px 14px;margin-bottom:8px;'+(isCur?'border:1.5px solid rgba(0,62,126,.2)':'opacity:.85')+'">'
+'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">'
+'<div>'
+'<span style="font-size:14px;font-weight:800;color:var(--ink)">'+h.dow+', '+h.date+'</span>'
+'<span style="font-size:11px;color:var(--muted);margin-left:8px">'+h.opener+'</span>'
+(isCur?'<span class="pill ok" style="margin-left:8px">текущая</span>':'')
+'</div>'
+'<div style="text-align:right">'
+'<div style="font-size:11px;color:var(--muted)">'+(h.open||'—')+(h.close?' '+h.close:' …')+'</div>'
+'</div>'
+'</div>'
+'<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px">'
+_hCell('Нач.остаток', h.openBal, 'var(--muted)')
+_hCell('Приход', totalIn, 'var(--success)')
+_hCell('Расход', totalOut, 'var(--danger)')
+_hCell(isCur?'Тек.остаток':'Кон.остаток', isCur?_cashCalc().cashBal:(h.closeBal||0), 'var(--accent)')
+'</div>'
+(h.inkass?'<div style="margin-top:8px;font-size:11px;color:var(--warn)">🏦 Инкассация: '+_fmtMoney(h.inkass)+'</div>':'')
+'</div>';
}).join('')
+'</div>';
}
function _hCell(label, val, col){
return '<div style="background:var(--bg);border-radius:9px;padding:8px;text-align:center">'
+'<div style="font-size:12px;font-weight:800;color:'+col+'">'+_fmtMoney(val)+'</div>'
+'<div style="font-size:9px;color:var(--muted);margin-top:1px">'+label+'</div>'
+'</div>';
}
// ── КАССА: ACTIONS ────────────────────────────────────────────────────────────
window._outMethod = window._outMethod || 'cash';
window._receiptAttached = window._receiptAttached || false;
function _attachReceipt(){
window._receiptAttached = !window._receiptAttached;
_render();
}
function _setOutMethod(m){
window._outMethod=m;
var cash=document.getElementById('omCash'), card=document.getElementById('omCard');
if(!cash) return;
if(m==='cash'){
cash.style.cssText+='border-color:var(--accent);background:rgba(0,62,126,.07);color:var(--accent)';
card.style.cssText+='border-color:var(--line);background:var(--card);color:var(--muted)';
} else {
card.style.cssText+='border-color:var(--accent);background:rgba(0,62,126,.07);color:var(--accent)';
cash.style.cssText+='border-color:var(--line);background:var(--card);color:var(--muted)';
}
}
function _acceptPayment(id){
var pend=window._PENDING||[], idx=pend.findIndex(function(p){return p.id===id;});
if(idx<0) return;
var p=pend[idx];
var now=new Date(), hh=String(now.getHours()).padStart(2,'0'), mm=String(now.getMinutes()).padStart(2,'0');
window._TRANSACTIONS.push({id:'t'+Date.now(),time:hh+':'+mm,type:'in',method:p.method,amount:p.amount,manager:p.manager,client:p.client,note:p.note});
pend.splice(idx,1); _render(); _toast('✅ Принято · '+_fmtMoney(p.amount),'var(--success)');
}
function _rejectPayment(id){
var pend=window._PENDING||[], idx=pend.findIndex(function(p){return p.id===id;});
if(idx<0) return;
var name=pend[idx].client; pend.splice(idx,1); _render(); _toast('↩ Отклонено · '+name,'var(--muted)');
}
function _submitExpense(){
var amt=parseFloat((document.getElementById('outAmt')||{value:0}).value);
var note=((document.getElementById('outNote')||{value:''}).value||'').trim();
if(!amt||amt<=0){_toast('Введите сумму','var(--danger)');return;}
if(!note){_toast('Укажите назначение','var(--danger)');return;}
var now=new Date(), hh=String(now.getHours()).padStart(2,'0'), mm=String(now.getMinutes()).padStart(2,'0');
window._TRANSACTIONS.push({id:'t'+Date.now(),time:hh+':'+mm,type:'out',method:window._outMethod||'cash',amount:amt,manager:null,client:null,note:note,receipt:window._receiptAttached});
window._cashOutScreen=false; window._receiptAttached=false; _render();
_toast('↑ Расход '+_fmtMoney(amt)+(window._receiptAttached?' · 📎 чек':''),'var(--accent)');
}
function _doInkass(){
var v=prompt('Сумма инкассации (изъятие наличных из кассы), ₽:');
if(!v||isNaN(parseFloat(v))) return;
var amt=parseFloat(v);
window._CASH_SHIFT.inkass=(window._CASH_SHIFT.inkass||0)+amt;
_render(); _toast('🏦 Инкассация '+_fmtMoney(amt),'var(--warn)');
}
function _confirmOpenShift(){
var bal=parseFloat((document.getElementById('openBal')||{value:0}).value)||0;
var now=new Date(), hh=String(now.getHours()).padStart(2,'0'), mm=String(now.getMinutes()).padStart(2,'0');
window._CASH_SHIFT={open:true,openTime:hh+':'+mm,openBalance:bal,inkass:0,opener:'Анна К.'};
window._TRANSACTIONS=[]; window._cashOpening=false; _render();
_toast('🔓 Смена открыта · остаток '+_fmtMoney(bal),'var(--success)');
}
function _confirmCloseShift(){
var sh=window._CASH_SHIFT;
var c=_cashCalc();
var now=new Date();
var hh=String(now.getHours()).padStart(2,'0'), mm=String(now.getMinutes()).padStart(2,'0');
var today='22.05';
// Добавляем в историю (обновляем текущую запись)
var hist=window._CASH_HISTORY||[];
var curIdx=hist.findIndex(function(h){return !h.close;});
if(curIdx>=0){
hist[curIdx].close=hh+':'+mm;
hist[curIdx].cashIn=c.cashIn; hist[curIdx].cardIn=c.cardIn;
hist[curIdx].expense=c.cashOut+c.cardOut; hist[curIdx].inkass=c.inkass;
hist[curIdx].closeBal=c.cashBal;
}
window._CASH_SHIFT={open:false,openTime:null,openBalance:c.cashBal,inkass:0,opener:''};
window._TRANSACTIONS=[]; window._cashClosing=false; window._cashTab='history'; _render();
_toast('🔒 Смена закрыта · нал. '+_fmtMoney(c.cashBal),'var(--accent)');
}
// ── ПЕРСОНАЛ ─────────────────────────────────────────────────────────────────
function _screenStaff(){
var edit = window._staffEditMode;
// Шапка с переключателем режима
var header = '<div class="page-header" style="gap:8px">'
+'<h2>👥 Персонал</h2>'
+'<span class="pill accent" style="white-space:nowrap;font-size:10px">1925 мая</span>'
+'<button onclick="window._staffEditMode='+(!edit)+';window._staffSel=null;window._staffAddSheet=null;_render()" '
+'style="margin-left:auto;padding:6px 12px;border-radius:20px;font-size:12px;font-weight:700;cursor:pointer;border:1.5px solid '
+(edit?'var(--success)+\';background:rgba(16,185,129,.1);color:var(--success)">'
:'rgba(0,62,126,.25)+\';background:rgba(0,62,126,.07);color:var(--accent)">')
+(edit?'✓ Готово':'✏️ Изменить')
+'</button>'
+'</div>';
// Уведомление о режиме
var modeBanner = edit
? '<div style="background:rgba(0,62,126,.07);border-bottom:1px solid rgba(0,62,126,.12);padding:8px 14px;display:flex;align-items:center;gap:8px">'
+'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>'
+'<span style="font-size:12px;font-weight:600;color:var(--accent)">Режим редактирования — нажмите ячейку чтобы добавить или убрать смену</span>'
+'</div>'
: '';
// Панель добавления смены (bottom-sheet стиль)
var addSheet = (edit && window._staffAddSheet) ? _renderAddSheet(window._staffAddSheet) : '';
return '<div class="page anim">'
+header
+modeBanner
+_staffGrid()
+(addSheet || (window._staffSel ? _staffDetail(window._staffSel) : _staffLegend()))
+'</div>';
}
function _staffGrid(){
var edit = window._staffEditMode;
var hdr = '<div style="display:flex;align-items:center;padding:0 10px;gap:3px;margin-bottom:6px">'
+'<div style="width:52px;flex-shrink:0"></div>'
+_DOW.map(function(d,i){
var isToday = _DATES[i]===_TODAY;
var isWknd = i===5||i===6;
return '<div style="flex:1;text-align:center">'
+'<div style="font-size:8px;font-weight:700;color:'+(isWknd?'var(--danger)':'var(--muted)')+'">'+d+'</div>'
+'<div style="font-size:11px;font-weight:'+(isToday?'800':'600')+';color:'+(isWknd?'var(--danger)':isToday?'var(--accent)':'var(--ink)')
+';'+(isToday?'background:rgba(0,62,126,.1);border-radius:5px;':'')+'padding:1px 0">'+_DATES[i]+'</div>'
+'</div>';
}).join('')
+'</div>';
var rows = _STAFF.map(function(st){
var plan = window._PLAN[st.id]||[];
var fact = window._FACT[st.id]||{};
var isSel = window._staffSel===st.id;
var row = '<div style="display:flex;align-items:center;padding:0 10px;gap:3px;margin-bottom:5px">';
// Аватар — в режиме просмотра кликабелен для деталей, в режиме правки — открывает добавление
var avatarClick = edit
? 'window._staffAddSheet=\''+(window._staffAddSheet===st.id?'null':st.id)+'\';window._staffSel=null;_render()'
: 'window._staffSel=\''+(isSel?'null':st.id)+'\';_render()';
var avatarRing = edit
? (window._staffAddSheet===st.id?';box-shadow:0 0 0 3px var(--accent)':'')
: (isSel?';box-shadow:0 0 0 3px rgba(0,62,126,.35)':'');
row += '<div style="width:52px;flex-shrink:0;cursor:pointer" onclick="'+avatarClick+'">'
+'<div style="width:34px;height:34px;border-radius:50%;background:'+st.color
+';display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;margin:0 auto 2px'+avatarRing+'">'
+st.short+'</div>'
+'<div style="font-size:8px;font-weight:600;color:'+(edit?'var(--accent)':'var(--muted)')+';text-align:center;line-height:1.1">'
+(edit?'смены':st.name.split(' ')[0])+'</div>'
+'</div>';
// Ячейки дней
_DATES.forEach(function(d,i){
var isWknd = i===5||i===6;
var isToday = d===_TODAY;
var isPast = d<_TODAY;
var inPlan = plan.indexOf(i)>=0;
var f = fact[i];
var cell = '';
if(isWknd){
// Выходной — всегда серый, нельзя добавить
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(239,68,68,.05);display:flex;align-items:center;justify-content:center">'
+'<span style="font-size:8px;color:rgba(239,68,68,.3);font-weight:700">вых</span></div>';
} else if(inPlan){
// ЗАПЛАНИРОВАН
if(f&&f.in){
// Есть факт прихода
var late = f.in>'10:15';
var bg = late?'rgba(245,158,11,.13)':'rgba(16,185,129,.11)';
var col = late?'var(--warn)':'var(--success)';
var icon = late?'⏰':'✓';
// В режиме правки прошлые факт-ячейки — только просмотр (нельзя убрать отработанный день)
cell='<div style="flex:1;height:38px;border-radius:7px;background:'+bg
+';display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;cursor:pointer" onclick="window._staffSel=\''+st.id+'\';window._staffEditMode=false;_render()">'
+'<span style="font-size:12px;font-weight:800;color:'+col+'">'+icon+'</span>'
+'<span style="font-size:8px;color:var(--muted)">'+f.in+'</span>'
+(f.gps&&!f.gps.ok&&f.gps.forced?'<span style="font-size:7px;color:var(--warn)">GPS?</span>':'')
+'</div>';
} else if(isPast){
// Неявка — только просмотр
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(239,68,68,.09);display:flex;align-items:center;justify-content:center;cursor:pointer" onclick="window._staffSel=\''+st.id+'\';window._staffEditMode=false;_render()">'
+'<span style="font-size:13px;color:var(--danger)">✗</span></div>';
} else if(isToday){
// Сегодня ожидается
var todayStyle = 'flex:1;height:38px;border-radius:7px;background:rgba(0,62,126,.08);border:1.5px solid rgba(0,62,126,.2);display:flex;align-items:center;justify-content:center';
cell='<div style="'+todayStyle+'">'
+'<span style="font-size:8px;font-weight:700;color:var(--accent)">план</span></div>';
} else {
// Будущий запланированный — в режиме правки можно УБРАТЬ
if(edit){
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(0,62,126,.08);border:1.5px solid rgba(0,62,126,.35);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;cursor:pointer;position:relative" onclick="_togglePlan(\''+st.id+'\','+i+')">'
+'<span style="font-size:8px;font-weight:700;color:var(--accent)">план</span>'
+'<span style="font-size:9px;color:var(--danger);font-weight:700">✕</span>'
+'</div>';
} else {
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(0,62,126,.05);border:1px dashed rgba(0,62,126,.2);display:flex;align-items:center;justify-content:center">'
+'<span style="font-size:8px;font-weight:700;color:rgba(0,62,126,.4)">план</span></div>';
}
}
} else {
// НЕ ЗАПЛАНИРОВАН
if(isPast||isToday){
// Прошлое/сегодня — просто пусто
cell='<div style="flex:1;height:38px;border-radius:7px;background:var(--bg);display:flex;align-items:center;justify-content:center">'
+'<span style="font-size:11px;color:rgba(0,0,0,.12)">—</span></div>';
} else {
// Будущий незапланированный — в режиме правки можно ДОБАВИТЬ
if(edit){
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(0,62,126,.04);border:1.5px dashed rgba(0,62,126,.3);display:flex;align-items:center;justify-content:center;cursor:pointer" onclick="_togglePlan(\''+st.id+'\','+i+')">'
+'<span style="font-size:18px;font-weight:300;color:var(--accent);opacity:.5">+</span></div>';
} else {
cell='<div style="flex:1;height:38px;border-radius:7px;background:var(--bg);display:flex;align-items:center;justify-content:center">'
+'<span style="font-size:11px;color:rgba(0,0,0,.1)">—</span></div>';
}
}
}
row+=cell;
});
row+='</div>';
return row;
}).join('');
return '<div style="background:var(--card);padding:12px 0 6px">'
+hdr+rows
+'</div>';
}
// Панель быстрого добавления смен конкретному сотруднику
function _renderAddSheet(staffId){
var st = _STAFF.find(function(x){return x.id===staffId;});
if(!st) return '';
var plan = window._PLAN[st.id]||[];
// Показываем только будущие рабочие дни (не выходные, не прошлые)
var futureDays = [];
_DATES.forEach(function(d,i){
if(i>=5) return; // вых
if(d<=_TODAY) return; // прошлое/сегодня
futureDays.push({d:d, i:i, dow:_DOW[i], inPlan: plan.indexOf(i)>=0});
});
var daysHtml = futureDays.map(function(fd){
return '<div onclick="_togglePlan(\''+st.id+'\','+fd.i+')" '
+'style="display:flex;align-items:center;justify-content:space-between;padding:11px 0;border-bottom:1px solid rgba(0,0,0,.05);cursor:pointer">'
+'<div style="display:flex;align-items:center;gap:12px">'
+'<div style="width:34px;height:34px;border-radius:10px;background:'+(fd.inPlan?'var(--accent)':'var(--bg)')+';border:1.5px solid '+(fd.inPlan?'var(--accent)':'var(--line)')+';display:flex;flex-direction:column;align-items:center;justify-content:center">'
+'<span style="font-size:8px;font-weight:700;color:'+(fd.inPlan?'rgba(255,255,255,.8)':'var(--muted)')+'">'+fd.dow+'</span>'
+'<span style="font-size:13px;font-weight:800;color:'+(fd.inPlan?'#fff':'var(--ink)')+'">'+fd.d+'</span>'
+'</div>'
+'<span style="font-size:14px;font-weight:600;color:'+(fd.inPlan?'var(--accent)':'var(--muted)')+'">'+fd.dow+', '+fd.d+' мая</span>'
+'</div>'
+(fd.inPlan
? '<span style="font-size:12px;font-weight:700;color:var(--danger)">✕ Убрать</span>'
: '<span style="font-size:12px;font-weight:700;color:var(--accent)">+ Добавить</span>')
+'</div>';
}).join('');
if(!futureDays.length){
daysHtml='<div style="padding:16px 0;text-align:center;color:var(--muted);font-size:13px">Нет доступных дней для изменения</div>';
}
return '<div style="background:var(--card);margin:8px 0 0;border-top:3px solid '+st.color+'">'
+'<div style="padding:12px 14px 0;display:flex;align-items:center;justify-content:space-between">'
+'<div style="display:flex;align-items:center;gap:10px">'
+'<div style="width:36px;height:36px;border-radius:50%;background:'+st.color+';display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:#fff">'+st.short+'</div>'
+'<div>'
+'<div style="font-size:14px;font-weight:700;color:var(--ink)">'+st.name+'</div>'
+'<div style="font-size:11px;color:var(--muted)">Изменение расписания</div>'
+'</div>'
+'</div>'
+'<button onclick="window._staffAddSheet=null;_render()" style="border:none;background:var(--bg);border-radius:50%;width:30px;height:30px;cursor:pointer;font-size:18px;color:var(--muted)">×</button>'
+'</div>'
+'<div style="padding:0 14px 8px">'+daysHtml+'</div>'
+'</div>';
}
function _staffLegend(){
return '<div style="display:flex;gap:10px;padding:10px 14px;flex-wrap:wrap">'
+'<div style="display:flex;align-items:center;gap:5px"><div style="width:14px;height:14px;border-radius:4px;background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.3)"></div><span style="font-size:11px;color:var(--muted)">Вовремя</span></div>'
+'<div style="display:flex;align-items:center;gap:5px"><div style="width:14px;height:14px;border-radius:4px;background:rgba(245,158,11,.13);border:1px solid rgba(245,158,11,.3)"></div><span style="font-size:11px;color:var(--muted)">Опоздание</span></div>'
+'<div style="display:flex;align-items:center;gap:5px"><div style="width:14px;height:14px;border-radius:4px;background:rgba(239,68,68,.09);border:1px solid rgba(239,68,68,.3)"></div><span style="font-size:11px;color:var(--muted)">Неявка</span></div>'
+'<div style="display:flex;align-items:center;gap:5px"><div style="width:14px;height:14px;border-radius:4px;background:rgba(0,62,126,.05);border:1px dashed rgba(0,62,126,.2)"></div><span style="font-size:11px;color:var(--muted)">Запланировано</span></div>'
+'<div style="width:100%;padding-top:4px;font-size:11px;color:var(--muted)">Нажми аватар — детали сотрудника</div>'
+'</div>';
}
function _staffDetail(staffId){
var st = _STAFF.find(function(x){return x.id===staffId;});
if(!st) return '';
var plan = window._PLAN[st.id]||[];
var fact = window._FACT[st.id]||{};
// Stats (only past workdays)
var totalPlan=0, worked=0, late=0, noShow=0, gpsWarn=0;
plan.forEach(function(di){
if(di>=5) return;
var d=_DATES[di];
if(d>_TODAY) return;
totalPlan++;
var f=fact[di];
if(f&&f.in){
worked++;
if(f.in>'10:15') late++;
if(f.gps&&!f.gps.ok&&!f.gps.forced) gpsWarn++;
} else if(d<_TODAY){
noShow++;
}
});
var statsHtml='<div style="display:flex;gap:6px;margin-bottom:14px">'
+_mStat(worked+'/'+totalPlan,'Смен',worked<totalPlan&&totalPlan>0?'var(--warn)':'var(--success)')
+_mStat(late||'—','Опозд.',late?'var(--warn)':'var(--muted)')
+_mStat(noShow||'—','Неявок',noShow?'var(--danger)':'var(--muted)')
+_mStat(gpsWarn||'—','GPS⚠','gpsWarn'?gpsWarn?'var(--danger)':'var(--muted)':'var(--muted)')
+'</div>';
// Week rows (workdays only)
var daysHtml = _DOW.slice(0,5).map(function(dow,i){
var d=_DATES[i];
var inPlan=plan.indexOf(i)>=0;
var f=fact[i];
var isPast=d<_TODAY, isToday=d===_TODAY;
var status,statusCol,timeStr='',gpsStr='';
if(!inPlan){
status='Выходной'; statusCol='var(--muted)';
} else if(f&&f.in){
var isLate=f.in>'10:15';
status=isLate?'⏰ Опоздание':'✅ Вовремя';
statusCol=isLate?'var(--warn)':'var(--success)';
timeStr=f.in+(f.out?' '+f.out:' идёт смена');
if(f.gps){
var km=function(m){return m>=1000?Math.round(m/100)/10+'км':m+'м';};
gpsStr=f.gps.ok?'📍 '+f.gps.dist+'м · в салоне'
:f.gps.forced?'📍 '+km(f.gps.dist)+' · подтверждено вручную'
:'⚠️ '+km(f.gps.dist)+' от салона';
}
} else if(isPast){
status='❌ Неявка'; statusCol='var(--danger)';
} else if(isToday){
status='🕐 Ожидается'; statusCol='var(--accent)';
} else {
status='📅 Запланировано'; statusCol='rgba(0,62,126,.6)';
}
// Кнопки редактирования — ТОЛЬКО в режиме правки (у ком. директора не видны)
var btn='';
if(window._staffEditMode && !isPast && !isToday){
if(inPlan){
btn='<button onclick="_togglePlan(\''+st.id+'\','+i+')" style="padding:5px 10px;border-radius:8px;border:1px solid rgba(239,68,68,.3);background:rgba(239,68,68,.06);color:var(--danger);font-size:11px;font-weight:600;cursor:pointer;flex-shrink:0">Убрать</button>';
} else {
btn='<button onclick="_togglePlan(\''+st.id+'\','+i+')" style="padding:5px 10px;border-radius:8px;border:1px solid rgba(0,62,126,.2);background:rgba(0,62,126,.06);color:var(--accent);font-size:11px;font-weight:600;cursor:pointer;flex-shrink:0">+ В план</button>';
}
}
return '<div style="display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:1px solid rgba(0,0,0,.04)">'
+'<div style="width:34px;text-align:center;flex-shrink:0">'
+'<div style="font-size:10px;font-weight:700;color:var(--muted)">'+dow+'</div>'
+'<div style="font-size:13px;font-weight:700;color:var(--ink)">'+d+'</div>'
+'</div>'
+'<div style="flex:1;min-width:0">'
+'<div style="font-size:13px;font-weight:600;color:'+statusCol+'">'+status+'</div>'
+(timeStr?'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+timeStr+'</div>':'')
+(gpsStr?'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+gpsStr+'</div>':'')
+'</div>'
+btn
+'</div>';
}).join('');
return '<div style="background:var(--card);margin:8px 0 0;padding:14px 14px 4px">'
+'<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px">'
+'<div style="width:42px;height:42px;border-radius:50%;background:'+st.color
+';display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800;color:#fff">'+st.short+'</div>'
+'<div style="flex:1">'
+'<div style="font-size:15px;font-weight:700;color:var(--ink)">'+st.name+'</div>'
+'<div style="font-size:11px;color:var(--muted)">Менеджер · 1925 мая</div>'
+'</div>'
+'<button onclick="window._staffSel=null;_render()" style="border:none;background:var(--bg);border-radius:50%;width:30px;height:30px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:18px;color:var(--muted);flex-shrink:0">×</button>'
+'</div>'
+statsHtml
+daysHtml
+'</div>';
}
function _mStat(val, label, col){
return '<div style="flex:1;background:var(--bg);border-radius:11px;padding:9px 6px;text-align:center">'
+'<div style="font-size:16px;font-weight:900;color:'+col+'">'+val+'</div>'
+'<div style="font-size:9px;color:var(--muted);margin-top:1px">'+label+'</div>'
+'</div>';
}
function _togglePlan(staffId, dayIdx){
var plan = window._PLAN[staffId]||[];
var pos = plan.indexOf(dayIdx);
if(pos>=0){ plan.splice(pos,1); } else { plan.push(dayIdx); plan.sort(function(a,b){return a-b;}); }
window._PLAN[staffId]=plan;
_render();
_toast(pos>=0?'📅 День убран из плана':'📅 День добавлен в план','var(--accent)');
}
// ── ACTIONS ───────────────────────────────────────────────────────────────────
function _addToOrder(id){
var s = _SUPPLIES.find(function(x){return x.id===id;});
if(s){ s.ordered=true; s.orderedDate=''; _render(); _toast('Добавлено в заявку','var(--accent)'); }
}
function _markReceived(id){
var s = _SUPPLIES.find(function(x){return x.id===id;});
if(s){ s.ordered=false; s.cur=s.min; s.orderedDate=''; _render(); _toast('✅ '+s.name+' — получено','var(--success)'); }
}
function _editStock(id){
var s = _SUPPLIES.find(function(x){return x.id===id;});
if(!s) return;
var v = prompt('Текущий остаток «'+s.name+'» ('+s.unit+'):', s.cur);
if(v!==null&&!isNaN(parseFloat(v))){ s.cur=parseFloat(v); _render(); }
}
function _submitOrder(){
var sel = Object.keys(window._orderSel||{}).filter(function(k){return window._orderSel[k];});
sel.forEach(function(id){
var s = _SUPPLIES.find(function(x){return x.id===id;});
if(s){ s.ordered=true; s.orderedDate='сегодня'; }
});
window._orderSel={};
_nav('supplies');
_toast('📦 Заявка оформлена на '+sel.length+' позиций','var(--accent)');
}
function _markFixed(id){
var i = _INVENTORY.find(function(x){return x.id===id;});
if(i){ i.cond='ok'; i.note=''; _render(); _toast('✅ Отмечено как устранено','var(--success)'); }
}
function _changeCond(id){
var i = _INVENTORY.find(function(x){return x.id===id;});
if(!i) return;
var opts=['ok','warn','bad'];
var labels=['ok — в норме','warn — нужен осмотр','bad — требует замены'];
var v=prompt('Состояние «'+i.name+'»:\n0: '+labels[0]+'\n1: '+labels[1]+'\n2: '+labels[2]+'\nВведите 0, 1 или 2:');
if(v!==null&&opts[parseInt(v)]){ i.cond=opts[parseInt(v)]; _render(); }
}
function _startInvCheck(){
window._checkResults={};
_nav('inv_check');
}
function _checkItem(id, result){
window._checkResults[id]=result;
var i = _INVENTORY.find(function(x){return x.id===id;});
if(i) i.cond=result;
_render();
}
function _finishCheck(){
window._LAST_INVENTORY = '19.05.2026';
window._checkResults={};
_nav('inventory');
_toast('📋 Осмотр завершён и сохранён','var(--success)');
}
function _cancelCheck(){
window._checkResults={};
_nav('inventory');
}
// ── TOAST ─────────────────────────────────────────────────────────────────────
function _toast(msg, bg){
var el=document.createElement('div');
el.style.cssText='position:absolute;bottom:70px;left:16px;right:16px;background:'+(bg||'#1E293B')+';color:#fff;padding:12px 16px;border-radius:14px;font-size:13px;font-weight:600;z-index:999;box-shadow:0 8px 24px rgba(0,0,0,.25);transition:opacity .4s';
el.textContent=msg;
document.getElementById('phoneFrame').appendChild(el);
setTimeout(function(){el.style.opacity='0';setTimeout(function(){el.remove();},400);},2200);
}
// ── INIT — hash navigation (file:// friendly) ─────────────────────────────────
var _adminHashMap={'#home':'home','#supplies':'supplies','#inventory':'inventory','#staff':'staff','#cash':'cash'};
var _initScreen = _adminHashMap[location.hash] || 'cash';
window._screen = _initScreen;
_render();
window.addEventListener('hashchange',function(){ var s=_adminHashMap[location.hash]; if(s){window._screen=s;_render();} });
</script>
</body>
</html>