mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 21:04:47 +00:00
1823 lines
128 KiB
HTML
1823 lines
128 KiB
HTML
<!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="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="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>
|
||
// ── ДАННЫЕ: ЗАЯВКИ МЕНЕДЖЕРОВ ─────────────────────────────────────────────────
|
||
window._MGR_REQUESTS = window._MGR_REQUESTS || [
|
||
{id:'r1', mgr:'Анна К.', color:'#7C3AED', type:'supply', prio:'high', title:'Закончились образцы ткани', body:'Нет образцов искусственной замши — теряем клиентов. Нужно срочно дозаказать у Мебельтекстиля.', created:'сегодня 09:14', status:'new'},
|
||
{id:'r2', mgr:'Мария С.', color:'#0891B2', type:'escalate', prio:'high', title:'Конфликт с клиентом Козлов Р.', body:'Клиент требует возврат 25 000 ₽ из-за задержки поставки. Говорит, что подаст жалобу. Прошу вмешаться.', created:'сегодня 10:30', status:'new'},
|
||
{id:'r3', mgr:'Пётр В.', color:'#059669', type:'visit', prio:'normal', title:'Нужна машина для выезда к клиенту', body:'Клиент Сидорова на Васильевском — хочет видеть образцы на дому. Нужен транспорт на 15:00.', created:'сегодня 11:05', status:'new'},
|
||
{id:'r4', mgr:'Анна К.', color:'#7C3AED', type:'supply', prio:'normal', title:'Каталоги кухонь Hettich закончились', body:'Остался 1 экземпляр. Клиенты берут домой, не возвращают. Надо заказать 10 шт.', created:'вчера 17:22', status:'done'},
|
||
{id:'r5', mgr:'Мария С.', color:'#0891B2', type:'schedule', prio:'normal', title:'Запрос на замену смены — 31 мая', body:'Прошу разрешить обменяться сменой с Петром: я работаю 31 мая вместо него, он — 1 июня вместо меня.', created:'вчера 14:10', status:'new'},
|
||
];
|
||
window._staffSubTab = window._staffSubTab || 'chess';
|
||
|
||
// ── ДАННЫЕ: ШАХМАТКА (расписание менеджеров × слоты) ─────────────────────────
|
||
var _CHESS_HOURS = ['10:00','11:00','12:00','13:00','14:00','15:00','16:00','17:00','18:00','19:00','20:00'];
|
||
var _CHESS_MGRS = [
|
||
{id:'ak', name:'Анна К.', short:'АК', color:'#7C3AED'},
|
||
{id:'ms', name:'Мария С.', short:'МС', color:'#0891B2'},
|
||
{id:'pv', name:'Пётр В.', short:'ПВ', color:'#059669'},
|
||
{id:'iv', name:'Иван В.', short:'ИВ', color:'#D97706'},
|
||
];
|
||
// ячейка: {client, type, status} status: free|busy|done|noshow
|
||
// type: consult|measure|follow|tech
|
||
var _CHESS_DATA = {
|
||
'ak':{'10:00':{client:'Орлова М.', type:'consult', status:'done'},
|
||
'11:00':{client:'Соколов А.', type:'follow', status:'done'},
|
||
'12:00':{client:'', type:'', status:'free'},
|
||
'13:00':{client:'', type:'', status:'free'},
|
||
'14:00':{client:'Ким Л.', type:'consult', status:'busy'},
|
||
'15:00':{client:'Ким Л.', type:'consult', status:'busy'},
|
||
'16:00':{client:'Захаров П.', type:'measure', status:'busy'},
|
||
'17:00':{client:'Новикова С.', type:'consult', status:'free'},
|
||
'18:00':{client:'', type:'', status:'free'},
|
||
'19:00':{client:'Громов И.', type:'follow', status:'free'},
|
||
'20:00':{client:'', type:'', status:'free'}},
|
||
'ms':{'10:00':{client:'Козлов Р.', type:'follow', status:'done'},
|
||
'11:00':{client:'', type:'', status:'free'},
|
||
'12:00':{client:'Лебедев С.', type:'consult', status:'done'},
|
||
'13:00':{client:'', type:'', status:'free'},
|
||
'14:00':{client:'Петрова А.', type:'tech', status:'busy'},
|
||
'15:00':{client:'Петрова А.', type:'tech', status:'busy'},
|
||
'16:00':{client:'', type:'', status:'free'},
|
||
'17:00':{client:'Морозов В.', type:'consult', status:'busy'},
|
||
'18:00':{client:'', type:'', status:'free'},
|
||
'19:00':{client:'', type:'', status:'free'},
|
||
'20:00':{client:'', type:'', status:'free'}},
|
||
'pv':{'10:00':{client:'', type:'', status:'free'},
|
||
'11:00':{client:'Сидорова Н.', type:'consult', status:'done'},
|
||
'12:00':{client:'Фёдоров К.', type:'follow', status:'noshow'},
|
||
'13:00':{client:'', type:'', status:'free'},
|
||
'14:00':{client:'', type:'', status:'free'},
|
||
'15:00':{client:'Баринова Т.', type:'consult', status:'busy'},
|
||
'16:00':{client:'Баринова Т.', type:'consult', status:'busy'},
|
||
'17:00':{client:'', type:'', status:'free'},
|
||
'18:00':{client:'Яковлев М.', type:'measure', status:'busy'},
|
||
'19:00':{client:'Яковлев М.', type:'measure', status:'busy'},
|
||
'20:00':{client:'', type:'', status:'free'}},
|
||
'iv':{'10:00':{client:'Тихонова Р.', type:'consult', status:'done'},
|
||
'11:00':{client:'Тихонова Р.', type:'consult', status:'done'},
|
||
'12:00':{client:'', type:'', status:'free'},
|
||
'13:00':{client:'Волков Е.', type:'follow', status:'done'},
|
||
'14:00':{client:'', type:'', status:'free'},
|
||
'15:00':{client:'Кузьмин О.', type:'consult', status:'busy'},
|
||
'16:00':{client:'', type:'', status:'free'},
|
||
'17:00':{client:'Рыбаков Д.', type:'tech', status:'free'},
|
||
'18:00':{client:'Рыбаков Д.', type:'tech', status:'free'},
|
||
'19:00':{client:'', type:'', status:'free'},
|
||
'20:00':{client:'', type:'', status:'free'}},
|
||
};
|
||
|
||
// ── ДАННЫЕ: ИТОГИ ПО МЕНЕДЖЕРАМ (по месяцам) ──────────────────────────────
|
||
var _MONTHLY_STATS = [
|
||
{id:'ak', name:'Анна К.', color:'#7C3AED', months:{
|
||
'Май':{visits:42, deals:14, revenue:847000, conversion:33, avg:60500, rating:4.8},
|
||
'Апр':{visits:38, deals:11, revenue:712000, conversion:29, avg:64700, rating:4.7},
|
||
'Мар':{visits:45, deals:16, revenue:920000, conversion:36, avg:57500, rating:4.9},
|
||
}},
|
||
{id:'ms', name:'Мария С.', color:'#0891B2', months:{
|
||
'Май':{visits:35, deals:9, revenue:610000, conversion:26, avg:67800, rating:4.5},
|
||
'Апр':{visits:41, deals:13, revenue:780000, conversion:32, avg:60000, rating:4.6},
|
||
'Мар':{visits:39, deals:12, revenue:695000, conversion:31, avg:57900, rating:4.4},
|
||
}},
|
||
{id:'pv', name:'Пётр В.', color:'#059669', months:{
|
||
'Май':{visits:28, deals:7, revenue:490000, conversion:25, avg:70000, rating:4.3},
|
||
'Апр':{visits:32, deals:9, revenue:540000, conversion:28, avg:60000, rating:4.2},
|
||
'Мар':{visits:30, deals:8, revenue:520000, conversion:27, avg:65000, rating:4.4},
|
||
}},
|
||
{id:'iv', name:'Иван В.', color:'#D97706', months:{
|
||
'Май':{visits:31, deals:8, revenue:530000, conversion:26, avg:66250, rating:4.1},
|
||
'Апр':{visits:29, deals:7, revenue:480000, conversion:24, avg:68600, rating:4.0},
|
||
'Мар':{visits:33, deals:10, revenue:610000, conversion:30, avg:61000, rating:4.3},
|
||
}},
|
||
];
|
||
window._monthlyPeriod = window._monthlyPeriod || 'Май';
|
||
|
||
// ── СОГЛАСОВАНИЕ СМЕН: запросы ─────────────────────────────────────────────
|
||
window._SHIFT_REQS = window._SHIFT_REQS || [
|
||
{id:'sr1', mgr:'Мария С.', color:'#0891B2', type:'swap', dates:'31 мая ↔ 1 июня', with:'Пётр В.', status:'pending'},
|
||
{id:'sr2', mgr:'Пётр В.', color:'#059669', type:'off', dates:'2 июня (отгул)', with:null, status:'pending'},
|
||
];
|
||
|
||
// ── 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 _STAGES = {
|
||
measure: {label:'Замер', color:'#8B5CF6', bg:'rgba(139,92,246,.1)'},
|
||
design: {label:'Проект', color:'#3B82F6', bg:'rgba(59,130,246,.1)'},
|
||
production: {label:'На фабрике', color:'#F59E0B', bg:'rgba(245,158,11,.1)'},
|
||
assembly: {label:'Монтаж', color:'#10B981', bg:'rgba(16,185,129,.1)'},
|
||
ready: {label:'Готов к сдаче', color:'#003E7E', bg:'rgba(0,62,126,.1)'},
|
||
waiting_pay: {label:'Ожид. оплату', color:'#EF4444', bg:'rgba(239,68,68,.1)'},
|
||
done: {label:'Закрыт', color:'#76BD22', bg:'rgba(118,189,34,.1)'},
|
||
};
|
||
|
||
window._ORDERS = window._ORDERS || [
|
||
// Просроченный — оплата не пришла
|
||
{id:'o1', num:'№ 2026-0214', client:'Морозова Е.', manager:'Анна К.', type:'Шкаф-купе', amount:98500, paid:0, stage:'waiting_pay', overdue:true, daysLeft:-12, note:'Ожидает финальный платёж с 10.05'},
|
||
// Готов — ожидает сдачи
|
||
{id:'o2', num:'№ 2026-0225', client:'Лебедев К.', manager:'Пётр В.', type:'Кухня', amount:189000, paid:94500, stage:'ready', overdue:false, daysLeft:1, note:'Требует согласования даты сдачи'},
|
||
// Монтаж
|
||
{id:'o3', num:'№ 2026-0228', client:'Козлов Р.', manager:'Пётр В.', type:'Кухня', amount:215000, paid:107500, stage:'assembly', overdue:false, daysLeft:3, note:''},
|
||
{id:'o4', num:'№ 2026-0231', client:'Иванова О.', manager:'Анна К.', type:'Гардероб', amount:87000, paid:43500, stage:'assembly', overdue:false, daysLeft:5, note:''},
|
||
// На фабрике
|
||
{id:'o5', num:'№ 2026-0235', client:'Петров С.', manager:'Пётр В.', type:'Кухня', amount:178000, paid:89000, stage:'production', overdue:false, daysLeft:14, note:''},
|
||
{id:'o6', num:'№ 2026-0236', client:'Соколов Д.', manager:'Анна К.', type:'Прихожая', amount:54000, paid:27000, stage:'production', overdue:false, daysLeft:18, note:''},
|
||
// Проект
|
||
{id:'o7', num:'№ 2026-0238', client:'Фёдорова Н.', manager:'Анна К.', type:'Кухня', amount:195000, paid:97500, stage:'design', overdue:false, daysLeft:21, note:''},
|
||
// Замер
|
||
{id:'o8', num:'№ 2026-0239', client:'Новиков А.', manager:'Пётр В.', type:'Шкаф-купе', amount:112000, paid:56000, stage:'measure', overdue:false, daysLeft:25, note:''},
|
||
{id:'o9', num:'№ 2026-0240', client:'Смирнова Т.', manager:'Анна К.', type:'Гардероб', amount:76000, paid:38000, stage:'measure', overdue:false, daysLeft:28, note:''},
|
||
// Закрытые (май)
|
||
{id:'o10', num:'№ 2026-0210', client:'Попова Г.', manager:'Анна К.', type:'Прихожая', amount:45000, paid:45000, stage:'done', overdue:false, daysLeft:0, note:''},
|
||
{id:'o11', num:'№ 2026-0212', client:'Захаров М.', manager:'Пётр В.', type:'Кухня', amount:231000, paid:231000, stage:'done', overdue:false, daysLeft:0, note:''},
|
||
];
|
||
window._ordersTab = window._ordersTab || 'active'; // 'active' | 'overdue' | 'done'
|
||
window._orderSel = window._orderSel || null; // id выбранного заказа (детали)
|
||
|
||
// ── ДАННЫЕ: ПЕРСОНАЛ ──────────────────────────────────────────────────────────
|
||
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:'orders', icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-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"/></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:'Касса'},
|
||
{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:'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:'Менеджеры'},
|
||
];
|
||
// Бейджи
|
||
var needBuy = _SUPPLIES.filter(function(s){ return _stockLevel(s)==='danger'&&!s.ordered; }).length;
|
||
var ordOverdue = (window._ORDERS||[]).filter(function(o){ return o.overdue; }).length;
|
||
var mgrReqNew = (window._MGR_REQUESTS||[]).filter(function(r){ return r.status==='new'; }).length;
|
||
var shiftReqPend = (window._SHIFT_REQS||[]).filter(function(r){ return r.status==='pending'; }).length;
|
||
var staffAlert = mgrReqNew + shiftReqPend;
|
||
// 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==='orders'&&ordOverdue>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">'+ordOverdue+'</div>'
|
||
: (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==='staff'&&(staffAlert+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">'+(staffAlert+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==='orders') return _screenOrders();
|
||
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 overdueOrd = (window._ORDERS||[]).filter(function(o){ return o.overdue; });
|
||
var activeOrd = (window._ORDERS||[]).filter(function(o){ return !o.overdue && o.stage!=='done'; });
|
||
var pendCash = (window._PENDING||[]);
|
||
|
||
// ── KPI-строка дня ──
|
||
var c = _cashCalc();
|
||
var revenue = c.cashIn + c.cardIn;
|
||
var revPlan = 55000; // план на день
|
||
var revPct = Math.round((revenue/revPlan)*100);
|
||
var kpiHtml = '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;padding:12px 16px 0">'
|
||
+'<div style="background:var(--card);border-radius:14px;padding:12px 10px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.05);cursor:pointer" onclick="_nav(\'orders\')">'
|
||
+'<div style="font-size:22px;font-weight:900;color:'+(overdueOrd.length?'var(--danger)':'var(--ink)')+'">'+((window._ORDERS||[]).filter(function(o){return o.stage!=='done';}).length)+'</div>'
|
||
+'<div style="font-size:9px;color:var(--muted);margin-top:1px">заказов актив.</div>'
|
||
+(overdueOrd.length?'<div style="font-size:9px;color:var(--danger);font-weight:700;margin-top:2px">'+overdueOrd.length+' просроч.</div>':'')
|
||
+'</div>'
|
||
+'<div style="background:var(--card);border-radius:14px;padding:12px 10px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.05);cursor:pointer" onclick="_nav(\'cash\')">'
|
||
+'<div style="font-size:14px;font-weight:900;color:'+(revPct>=80?'var(--success)':revPct>=50?'var(--warn)':'var(--danger)')+'">'+_fmtMoney(revenue)+'</div>'
|
||
+'<div style="font-size:9px;color:var(--muted);margin-top:1px">выручка сегодня</div>'
|
||
+'<div style="font-size:9px;color:var(--muted);margin-top:1px">план: '+_fmtMoney(revPlan)+'</div>'
|
||
+'</div>'
|
||
+'<div style="background:var(--card);border-radius:14px;padding:12px 10px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.05);cursor:pointer" onclick="_nav(\'cash\')">'
|
||
+'<div style="font-size:14px;font-weight:900;color:var(--ink)">'+_fmtMoney(c.cashBal)+'</div>'
|
||
+'<div style="font-size:9px;color:var(--muted);margin-top:1px">в кассе</div>'
|
||
+(pendCash.length?'<div style="font-size:9px;color:var(--warn);font-weight:700;margin-top:2px">'+pendCash.length+' ожид. оплат</div>':'')
|
||
+'</div>'
|
||
+'</div>';
|
||
|
||
// ── Прогресс выручки ──
|
||
var barPct = Math.min(100,revPct);
|
||
var barCol = revPct>=80?'var(--success)':revPct>=50?'var(--warn)':'var(--danger)';
|
||
var barHtml = '<div style="padding:8px 16px 0">'
|
||
+'<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--muted);margin-bottom:4px">'
|
||
+'<span>Выручка / план дня</span><span style="font-weight:700;color:'+barCol+'">'+revPct+'%</span></div>'
|
||
+'<div style="height:5px;background:var(--line);border-radius:3px;overflow:hidden">'
|
||
+'<div style="height:100%;background:'+barCol+';border-radius:3px;width:'+barPct+'%;transition:width .4s"></div>'
|
||
+'</div></div>';
|
||
|
||
// ── Алерты ──
|
||
var alertsHtml = '<div style="height:10px"></div>';
|
||
if(overdueOrd.length){
|
||
alertsHtml += '<div style="margin:0 16px 8px;background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.2);border-radius:14px;padding:12px 14px;cursor:pointer" onclick="_nav(\'orders\')">'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--danger);margin-bottom:3px">🚨 Просроченные заказы — '+overdueOrd.length+'</div>'
|
||
+'<div style="font-size:12px;color:var(--muted)">'+overdueOrd.map(function(o){return o.client+' · '+_fmtMoney(o.amount-o.paid)+' ₽ не оплачено';}).join('<br>')+'</div>'
|
||
+'</div>';
|
||
}
|
||
if(pendCash.length){
|
||
alertsHtml += '<div style="margin:0 16px 8px;background:rgba(245,158,11,.07);border:1px solid rgba(245,158,11,.2);border-radius:14px;padding:12px 14px;cursor:pointer" onclick="_nav(\'cash\')">'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--warn);margin-bottom:3px">⏳ Ожидают приёма в кассу — '+pendCash.length+'</div>'
|
||
+'<div style="font-size:12px;color:var(--muted)">'+pendCash.map(function(p){return p.client+' · '+_fmtMoney(p.amount);}).join(', ')+'</div>'
|
||
+'</div>';
|
||
}
|
||
if(needBuy.length){
|
||
alertsHtml += '<div style="margin:0 16px 8px;background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.15);border-radius:14px;padding:12px 14px;cursor:pointer" onclick="_nav(\'supplies\')">'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--danger);margin-bottom:3px">🛒 Нужно закупить — '+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(invBad.length){
|
||
alertsHtml += '<div style="margin:0 16px 8px;background:rgba(245,158,11,.06);border:1px solid rgba(245,158,11,.15);border-radius:14px;padding:12px 14px;cursor:pointer" onclick="_nav(\'inventory\')">'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--warn);margin-bottom:3px">⚠️ Инвентарь: требует внимания — '+invBad.length+'</div>'
|
||
+'<div style="font-size:12px;color:var(--muted)">'+invBad.map(function(i){return i.name;}).join(', ')+'</div>'
|
||
+'</div>';
|
||
}
|
||
// Заявки менеджеров
|
||
var mgrHigh = (window._MGR_REQUESTS||[]).filter(function(r){return r.status==='new'&&r.prio==='high';});
|
||
var mgrAll = (window._MGR_REQUESTS||[]).filter(function(r){return r.status==='new';});
|
||
if(mgrHigh.length){
|
||
alertsHtml += '<div style="margin:0 16px 8px;background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.2);border-radius:14px;padding:12px 14px;cursor:pointer" onclick="window._staffSubTab=\'requests\';window._reqFilter=\'all\';_nav(\'staff\')">'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--danger);margin-bottom:3px">🚨 Срочные заявки менеджеров — '+mgrHigh.length+'</div>'
|
||
+'<div style="font-size:12px;color:var(--muted)">'+mgrHigh.map(function(r){return r.mgr+': '+r.title;}).join('<br>')+'</div>'
|
||
+'</div>';
|
||
} else if(mgrAll.length){
|
||
alertsHtml += '<div style="margin:0 16px 8px;background:rgba(0,62,126,.05);border:1px solid rgba(0,62,126,.15);border-radius:14px;padding:12px 14px;cursor:pointer" onclick="window._staffSubTab=\'requests\';window._reqFilter=\'all\';_nav(\'staff\')">'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--accent);margin-bottom:3px">📋 Заявки менеджеров — '+mgrAll.length+'</div>'
|
||
+'<div style="font-size:12px;color:var(--muted)">'+mgrAll.slice(0,2).map(function(r){return r.mgr+': '+r.title;}).join('<br>')+'</div>'
|
||
+'</div>';
|
||
}
|
||
if(!overdueOrd.length&&!pendCash.length&&!needBuy.length&&!invBad.length&&!mgrAll.length){
|
||
alertsHtml += '<div style="margin:0 16px 8px;background:var(--s-success-bg);border:1px solid rgba(16,185,129,.2);border-radius:14px;padding:12px 14px;text-align:center">'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--success)">✅ Всё в порядке</div>'
|
||
+'</div>';
|
||
}
|
||
|
||
// ── Менеджеры сейчас ──
|
||
var mgrNowHtml = '<div style="padding:0 16px;margin-bottom:4px">'
|
||
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">👥 Менеджеры сейчас</div>'
|
||
+'<div style="background:var(--card);border-radius:14px;box-shadow:0 2px 8px rgba(0,0,0,.06);overflow:hidden">'
|
||
+_CHESS_MGRS.map(function(mgr){
|
||
var rd=_CHESS_DATA[mgr.id]||{};
|
||
var curSlot=rd['15:00']||{};
|
||
var nextSlot=rd['16:00']||{};
|
||
var statusText=curSlot.client?'С клиентом: '+curSlot.client:'Свободен';
|
||
var statusColor=curSlot.client?'var(--success)':'var(--muted)';
|
||
var nextText=nextSlot.client?'→ '+nextSlot.client:'→ Свободен';
|
||
return '<div style="display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid rgba(0,0,0,.05);cursor:pointer" onclick="window._staffSubTab=\'chess\';_nav(\'staff\')">'
|
||
+'<div style="width:34px;height:34px;border-radius:50%;background:'+mgr.color+';display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:#fff;flex-shrink:0">'+mgr.short+'</div>'
|
||
+'<div style="flex:1;min-width:0">'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--ink)">'+mgr.name+'</div>'
|
||
+'<div style="font-size:11px;color:'+statusColor+';font-weight:600">'+statusText+'</div>'
|
||
+'</div>'
|
||
+'<div style="font-size:10px;color:var(--muted);text-align:right;flex-shrink:0">'+nextText+'</div>'
|
||
+'</div>';
|
||
}).join('')
|
||
+'<div style="padding:8px 12px;background:rgba(0,62,126,.04);cursor:pointer;text-align:center;font-size:12px;font-weight:700;color:var(--accent)" onclick="window._staffSubTab=\'chess\';_nav(\'staff\')">🔲 Открыть шахматку →</div>'
|
||
+'</div></div>';
|
||
|
||
// ── Быстрые действия ──
|
||
var quickHtml = '<div style="padding:0 16px;display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:4px">'
|
||
+'<button onclick="_nav(\'sup_new\')" style="padding:11px;font-size:13px;font-weight:700;border-radius:14px;border:1.5px solid rgba(0,62,126,.2);background:rgba(0,62,126,.06);color:var(--accent);cursor:pointer">🛒 Создать заявку</button>'
|
||
+'<button onclick="_startInvCheck()" style="padding:11px;font-size:13px;font-weight:700;border-radius:14px;border:1.5px solid rgba(0,62,126,.15);background:rgba(0,62,126,.05);color:var(--accent);cursor:pointer">📋 Осмотр инвентаря</button>'
|
||
+'</div>';
|
||
|
||
return '<div class="page anim">'
|
||
+'<div style="padding:16px 16px 12px;background:var(--card);border-bottom:1px solid var(--line)">'
|
||
+'<div style="font-size:12px;color:var(--muted);margin-bottom:2px">Чт, 29 мая 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>'
|
||
+kpiHtml
|
||
+barHtml
|
||
+alertsHtml
|
||
+mgrNowHtml
|
||
+quickHtml
|
||
+'</div>';
|
||
}
|
||
|
||
// ── ЗАКАЗЫ ────────────────────────────────────────────────────────────────────
|
||
function _screenOrders(){
|
||
var tab = window._ordersTab;
|
||
var all = window._ORDERS||[];
|
||
var overdueList = all.filter(function(o){ return o.overdue; });
|
||
var activeList = all.filter(function(o){ return !o.overdue && o.stage!=='done'; });
|
||
var doneList = all.filter(function(o){ return o.stage==='done'; });
|
||
|
||
var list = tab==='active' ? activeList : tab==='overdue' ? overdueList : doneList;
|
||
|
||
var tabsHtml = '<div style="display:flex;gap:6px;padding:10px 16px;overflow-x:auto;scrollbar-width:none">'
|
||
+[['active','Активные',activeList.length],['overdue','Просроченные',overdueList.length],['done','Закрытые',doneList.length]].map(function(t){
|
||
var isAct = tab===t[0];
|
||
var col = t[0]==='overdue'&&t[2]>0?'var(--danger)':t[0]==='active'?'var(--accent)':'var(--success)';
|
||
return '<div style="padding:6px 14px;border-radius:20px;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;flex-shrink:0;'
|
||
+(isAct?'background:var(--accent);color:#fff;':'background:var(--card);color:var(--muted);border:1.5px solid var(--line);')
|
||
+'" onclick="window._ordersTab=\''+t[0]+'\';_render()">'+t[1]+(t[2]>0?' <b style="opacity:.8">'+t[2]+'</b>':'')+'</div>';
|
||
}).join('')
|
||
+'</div>';
|
||
|
||
// Суммарная строка по заказам
|
||
var totalActive = activeList.length + overdueList.length;
|
||
var totalRevenue = all.filter(function(o){return o.stage!=='done'||true;}).reduce(function(s,o){return s+o.amount;},0);
|
||
var totalPaid = all.reduce(function(s,o){return s+o.paid;},0);
|
||
var totalDebt = totalRevenue - totalPaid;
|
||
|
||
var summaryHtml = '<div style="margin:0 16px 10px;background:var(--accent);border-radius:16px;padding:14px 16px;color:#fff">'
|
||
+'<div style="display:flex;justify-content:space-between;margin-bottom:8px">'
|
||
+'<div style="font-size:11px;opacity:.7">Всего заказов · Салон Ленина</div>'
|
||
+'<div style="font-size:11px;opacity:.7">Май 2026</div>'
|
||
+'</div>'
|
||
+'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px">'
|
||
+'<div><div style="font-size:20px;font-weight:900">'+totalActive+'</div><div style="font-size:9px;opacity:.7;margin-top:1px">активных</div></div>'
|
||
+'<div><div style="font-size:20px;font-weight:900">'+_fmtMoney(totalPaid)+'</div><div style="font-size:9px;opacity:.7;margin-top:1px">оплачено</div></div>'
|
||
+'<div><div style="font-size:20px;font-weight:900;color:rgba(255,255,255,.75)">'+_fmtMoney(totalDebt)+'</div><div style="font-size:9px;opacity:.7;margin-top:1px">к получению</div></div>'
|
||
+'</div>'
|
||
+'</div>';
|
||
|
||
var ordersHtml = list.length ? list.map(function(o){
|
||
var st = _STAGES[o.stage] || {label:o.stage, color:'#888', bg:'rgba(0,0,0,.06)'};
|
||
var debtAmt = o.amount - o.paid;
|
||
var border = o.overdue ? 'var(--danger)' : o.daysLeft<=3&&o.daysLeft>=0 ? 'var(--warn)' : 'var(--line)';
|
||
var paidPct = Math.round((o.paid/o.amount)*100);
|
||
return '<div style="background:var(--card);margin:0 16px 8px;border-radius:14px;padding:13px 14px;border-left:3px solid '+border+'">'
|
||
// Шапка карточки
|
||
+'<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:8px">'
|
||
+'<div>'
|
||
+'<div style="font-size:14px;font-weight:700;color:var(--ink)">'+o.client+'</div>'
|
||
+'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+o.num+' · '+o.type+' · '+o.manager+'</div>'
|
||
+'</div>'
|
||
+'<span style="padding:3px 9px;border-radius:12px;font-size:11px;font-weight:700;background:'+st.bg+';color:'+st.color+';white-space:nowrap;flex-shrink:0">'+st.label+'</span>'
|
||
+'</div>'
|
||
// Сумма + оплата
|
||
+'<div style="display:flex;gap:8px;margin-bottom:8px">'
|
||
+'<div style="flex:1;background:var(--bg);border-radius:9px;padding:8px 10px">'
|
||
+'<div style="font-size:10px;color:var(--muted);margin-bottom:2px">Сумма заказа</div>'
|
||
+'<div style="font-size:14px;font-weight:800;color:var(--ink)">'+_fmtMoney(o.amount)+'</div>'
|
||
+'</div>'
|
||
+'<div style="flex:1;background:var(--bg);border-radius:9px;padding:8px 10px">'
|
||
+'<div style="font-size:10px;color:var(--muted);margin-bottom:2px">Оплачено ('+paidPct+'%)</div>'
|
||
+'<div style="font-size:14px;font-weight:800;color:'+(o.paid>=o.amount?'var(--success)':o.overdue?'var(--danger)':'var(--ink)')+'">'+_fmtMoney(o.paid)+'</div>'
|
||
+'</div>'
|
||
+'</div>'
|
||
// Прогресс оплаты
|
||
+'<div style="height:4px;background:var(--line);border-radius:2px;overflow:hidden;margin-bottom:8px">'
|
||
+'<div style="height:100%;border-radius:2px;background:'+(o.paid>=o.amount?'var(--success)':o.overdue?'var(--danger)':'var(--accent)')+';width:'+Math.min(100,paidPct)+'%;transition:width .4s"></div>'
|
||
+'</div>'
|
||
// Статус / срок
|
||
+(o.overdue
|
||
? '<div style="font-size:12px;color:var(--danger);font-weight:700">🚨 Просрочено на '+Math.abs(o.daysLeft)+' дн. · к получению: '+_fmtMoney(debtAmt)+'</div>'
|
||
: o.note
|
||
? '<div style="font-size:12px;color:var(--warn)">⚠️ '+o.note+'</div>'
|
||
: o.daysLeft>0
|
||
? '<div style="font-size:12px;color:var(--muted)">Срок: через '+o.daysLeft+' дн.</div>'
|
||
: '')
|
||
+'</div>';
|
||
}).join('')
|
||
: '<div style="padding:32px 16px;text-align:center;color:var(--muted);font-size:13px">Заказов в этой категории нет</div>';
|
||
|
||
return '<div class="page anim">'
|
||
+'<div class="page-header">'
|
||
+'<h2>📋 Заказы</h2>'
|
||
+(overdueList.length?'<span class="pill danger">'+overdueList.length+' просроч.</span>':'<span class="pill ok">'+activeList.length+' актив.</span>')
|
||
+'</div>'
|
||
+summaryHtml
|
||
+tabsHtml
|
||
+ordersHtml
|
||
+'</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 sub = window._staffSubTab || 'schedule';
|
||
var mgrNew = (window._MGR_REQUESTS||[]).filter(function(r){return r.status==='new';}).length;
|
||
var shiftNew = (window._SHIFT_REQS||[]).filter(function(r){return r.status==='pending';}).length;
|
||
var _tabs=[
|
||
{key:'chess', label:'🔲 Шахматка'},
|
||
{key:'schedule', label:'📅 График'},
|
||
{key:'requests', label:'📋 Заявки', badge: mgrNew+shiftNew},
|
||
{key:'monthly', label:'📊 Итоги'},
|
||
];
|
||
var chipBar = '<div style="display:flex;gap:0;background:var(--card);border-bottom:1px solid var(--line);overflow-x:auto;scrollbar-width:none;padding:0 8px">'
|
||
+_tabs.map(function(t){
|
||
var act=sub===t.key;
|
||
return '<div onclick="window._staffSubTab=\''+t.key+'\';_render()" style="padding:10px 12px;font-size:12px;font-weight:700;cursor:pointer;border-bottom:2px solid '+(act?'var(--accent)':'transparent')+';color:'+(act?'var(--accent)':'var(--muted)')+';white-space:nowrap;position:relative;flex-shrink:0">'
|
||
+t.label
|
||
+(t.badge>0?'<span style="position:absolute;top:6px;right:2px;min-width:15px;height:15px;border-radius:8px;background:var(--danger);color:#fff;font-size:9px;font-weight:800;display:inline-flex;align-items:center;justify-content:center;padding:0 2px">'+t.badge+'</span>':'')
|
||
+'</div>';
|
||
}).join('')
|
||
+'</div>';
|
||
if(sub==='requests') return '<div class="page anim">'+chipBar+_screenRequests()+'</div>';
|
||
if(sub==='chess') return '<div class="page anim">'+chipBar+_screenChess()+'</div>';
|
||
if(sub==='monthly') return '<div class="page anim">'+chipBar+_screenMonthly()+'</div>';
|
||
|
||
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">19–25 мая</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) : '';
|
||
|
||
// Блок согласования запросов смен
|
||
var shiftReqs = (window._SHIFT_REQS||[]).filter(function(r){return r.status==='pending';});
|
||
var shiftReqBlock = shiftReqs.length ? (
|
||
'<div style="margin:10px 10px 0;padding:12px;background:rgba(245,158,11,.07);border:1.5px solid rgba(245,158,11,.3);border-radius:14px">'
|
||
+'<div style="font-size:12px;font-weight:700;color:var(--warn);margin-bottom:8px">⏳ Запросы на изменение смены — '+shiftReqs.length+'</div>'
|
||
+shiftReqs.map(function(r){
|
||
var typeLabel = r.type==='swap'?'Обмен сменами':r.type==='off'?'Отгул':'Перенос';
|
||
return '<div style="display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid rgba(245,158,11,.15)">'
|
||
+'<div style="width:30px;height:30px;border-radius:50%;background:'+r.color+';display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;flex-shrink:0">'+r.mgr.split(' ').map(function(w){return w[0];}).join('')+'</div>'
|
||
+'<div style="flex:1;min-width:0">'
|
||
+'<div style="font-size:12px;font-weight:700;color:var(--ink)">'+r.mgr+' · <span style="color:var(--warn)">'+typeLabel+'</span></div>'
|
||
+'<div style="font-size:11px;color:var(--muted)">'+r.dates+(r.with?' · с '+r.with:'')+'</div>'
|
||
+'</div>'
|
||
+'<div style="display:flex;gap:5px;flex-shrink:0">'
|
||
+'<button onclick="window._SHIFT_REQS.find(function(x){return x.id===\''+r.id+'\';}).status=\'approved\';_toast(\'Смена согласована\',\'#059669\');_render()" style="padding:5px 10px;border-radius:8px;border:none;background:#DCFCE7;color:#15803D;font-size:11px;font-weight:700;cursor:pointer">✓</button>'
|
||
+'<button onclick="window._SHIFT_REQS.find(function(x){return x.id===\''+r.id+'\';}).status=\'rejected\';_toast(\'Отклонено\',\'#DC2626\');_render()" style="padding:5px 10px;border-radius:8px;border:none;background:#FEE2E2;color:#DC2626;font-size:11px;font-weight:700;cursor:pointer">✕</button>'
|
||
+'</div>'
|
||
+'</div>';
|
||
}).join('')
|
||
+'</div>'
|
||
) : '';
|
||
|
||
return '<div class="page anim">'
|
||
+chipBar
|
||
+header
|
||
+modeBanner
|
||
+shiftReqBlock
|
||
+_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)">Менеджер · 19–25 мая</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');
|
||
}
|
||
|
||
// ── ШАХМАТКА ──────────────────────────────────────────────────────────────────
|
||
function _screenChess(){
|
||
var typeColor={consult:'#3B82F6',measure:'#8B5CF6',follow:'#059669',tech:'#F59E0B'};
|
||
var typeLabel={consult:'Консульт.',measure:'Замер',follow:'Повторный',tech:'Тех.вопрос'};
|
||
var statusBg={free:'transparent',busy:'',done:'',noshow:'rgba(239,68,68,.12)'};
|
||
var now = 15; // текущий час для демо
|
||
|
||
// Текущий час-индикатор
|
||
var nowIdx = _CHESS_HOURS.indexOf('15:00');
|
||
|
||
var CELL_W = 68; // px ширина ячейки
|
||
var NAME_W = 40; // px ширина имени менеджера
|
||
|
||
// Шапка часов
|
||
var headerRow = '<div style="display:flex;align-items:center;position:sticky;top:0;z-index:5;background:var(--card);border-bottom:1px solid var(--line)">'
|
||
+'<div style="width:'+NAME_W+'px;flex-shrink:0;padding:6px 4px;font-size:9px;font-weight:700;color:var(--muted)">Менеджер</div>'
|
||
+_CHESS_HOURS.map(function(h,i){
|
||
var isCur=i===nowIdx;
|
||
return '<div style="width:'+CELL_W+'px;flex-shrink:0;text-align:center;padding:6px 2px;font-size:10px;font-weight:'+(isCur?'800':'600')+';color:'+(isCur?'var(--accent)':'var(--muted)')+';border-left:1px solid rgba(0,0,0,.06);background:'+(isCur?'rgba(0,62,126,.05)':'')+'">'+h+'</div>';
|
||
}).join('')
|
||
+'</div>';
|
||
|
||
// Строки менеджеров
|
||
var rows = _CHESS_MGRS.map(function(mgr){
|
||
var rowData = _CHESS_DATA[mgr.id]||{};
|
||
return '<div style="display:flex;align-items:stretch;border-bottom:1px solid rgba(0,0,0,.05)">'
|
||
+'<div style="width:'+NAME_W+'px;flex-shrink:0;display:flex;align-items:center;justify-content:center;padding:4px 2px">'
|
||
+'<div style="width:30px;height:30px;border-radius:50%;background:'+mgr.color+';display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;color:#fff">'+mgr.short+'</div>'
|
||
+'</div>'
|
||
+_CHESS_HOURS.map(function(h,i){
|
||
var cell=rowData[h]||{status:'free'};
|
||
var isCur=i===nowIdx;
|
||
var bg=cell.status==='done'?'rgba(16,185,129,.1)':cell.status==='busy'?'rgba(59,130,246,.1)':cell.status==='noshow'?'rgba(239,68,68,.1)':'';
|
||
var brd=cell.status==='done'?'1px solid rgba(16,185,129,.25)':cell.status==='busy'?'1px solid rgba(59,130,246,.25)':cell.status==='noshow'?'1px solid rgba(239,68,68,.2)':'1px solid rgba(0,0,0,.05)';
|
||
var txt=cell.status==='done'?'var(--success)':cell.status==='busy'?'#1D4ED8':cell.status==='noshow'?'var(--danger)':'var(--muted)';
|
||
var icon=cell.status==='done'?'✓':cell.status==='noshow'?'✗':cell.status==='busy'?'●':'';
|
||
return '<div style="width:'+CELL_W+'px;flex-shrink:0;min-height:54px;border-left:'+brd+';background:'+(isCur?'rgba(0,62,126,.04)':bg||'transparent')+';padding:4px;display:flex;flex-direction:column;justify-content:center;cursor:'+(cell.client?'pointer':'default')+'" onclick="'+( cell.client?'alert(\''+mgr.name+' · '+h+' · '+cell.client+' · '+(typeLabel[cell.type]||'')+'\')':'')+'">'
|
||
+(cell.client
|
||
?'<div style="font-size:9px;font-weight:700;color:'+txt+';line-height:1.2;margin-bottom:2px">'+icon+' '+cell.client+'</div>'
|
||
+'<div style="font-size:8px;font-weight:600;background:'+(typeColor[cell.type]||'#94A3B8')+'20;color:'+(typeColor[cell.type]||'#94A3B8')+';padding:1px 4px;border-radius:4px;display:inline-block">'+typeLabel[cell.type]+'</div>'
|
||
:'<div style="font-size:11px;color:rgba(0,0,0,.1);text-align:center">—</div>')
|
||
+'</div>';
|
||
}).join('')
|
||
+'</div>';
|
||
}).join('');
|
||
|
||
// Легенда
|
||
var legend='<div style="display:flex;gap:10px;padding:8px 12px;flex-wrap:wrap">'
|
||
+'<span style="font-size:10px;color:var(--success);font-weight:700">✓ Завершено</span>'
|
||
+'<span style="font-size:10px;color:#1D4ED8;font-weight:700">● Занято</span>'
|
||
+'<span style="font-size:10px;color:var(--danger);font-weight:700">✗ Не пришёл</span>'
|
||
+'<span style="font-size:10px;color:var(--muted);font-weight:700">— Свободно</span>'
|
||
+'</div>';
|
||
|
||
// Итог строки: занятость
|
||
var summary='<div style="padding:8px 12px;border-top:1px solid var(--line)">'
|
||
+'<div style="font-size:11px;font-weight:700;color:var(--muted);margin-bottom:6px">ЗАНЯТОСТЬ СЕГОДНЯ</div>'
|
||
+'<div style="display:flex;flex-direction:column;gap:5px">'
|
||
+_CHESS_MGRS.map(function(mgr){
|
||
var rd=_CHESS_DATA[mgr.id]||{};
|
||
var total=_CHESS_HOURS.length;
|
||
var busy=_CHESS_HOURS.filter(function(h){return (rd[h]||{}).client;}).length;
|
||
var pct=Math.round(busy/total*100);
|
||
var col=pct>=70?'var(--success)':pct>=40?'var(--warn)':'var(--muted)';
|
||
return '<div style="display:flex;align-items:center;gap:8px">'
|
||
+'<div style="width:22px;height:22px;border-radius:50%;background:'+mgr.color+';display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:800;color:#fff;flex-shrink:0">'+mgr.short+'</div>'
|
||
+'<div style="flex:1"><div style="height:6px;background:var(--line);border-radius:3px;overflow:hidden"><div style="height:100%;background:'+col+';border-radius:3px;width:'+pct+'%"></div></div></div>'
|
||
+'<div style="font-size:11px;font-weight:700;color:'+col+';width:28px;text-align:right">'+pct+'%</div>'
|
||
+'<div style="font-size:10px;color:var(--muted);width:32px">'+busy+'/'+total+'сл</div>'
|
||
+'</div>';
|
||
}).join('')
|
||
+'</div></div>';
|
||
|
||
return '<div style="overflow-x:auto;scrollbar-width:thin;-webkit-overflow-scrolling:touch">'
|
||
+'<div style="min-width:'+(_CHESS_HOURS.length*CELL_W+NAME_W)+'px">'
|
||
+headerRow+rows
|
||
+'</div></div>'
|
||
+legend+summary;
|
||
}
|
||
|
||
// ── ПОМЕСЯЧНЫЕ ИТОГИ ───────────────────────────────────────────────────────────
|
||
function _screenMonthly(){
|
||
var months=['Май','Апр','Мар'];
|
||
var per=window._monthlyPeriod||'Май';
|
||
|
||
var periodTabs='<div style="display:flex;gap:6px;padding:10px 12px;overflow-x:auto;scrollbar-width:none">'
|
||
+months.map(function(m){
|
||
var act=m===per;
|
||
return '<div onclick="window._monthlyPeriod=\''+m+'\';_render()" style="padding:6px 16px;border-radius:20px;font-size:12px;font-weight:700;cursor:pointer;border:1.5px solid '+(act?'var(--accent)':'var(--line)')+';background:'+(act?'var(--accent)':'var(--card)')+';color:'+(act?'#fff':'var(--muted)')+';flex-shrink:0">'+m+'</div>';
|
||
}).join('')
|
||
+'</div>';
|
||
|
||
// Топ-менеджер
|
||
var sorted=_MONTHLY_STATS.slice().sort(function(a,b){return (b.months[per]||{revenue:0}).revenue-(a.months[per]||{revenue:0}).revenue;});
|
||
var top=sorted[0];
|
||
|
||
var topCard='<div style="margin:0 12px 12px;background:linear-gradient(135deg,#002450,#003E7E,#1560BD);border-radius:16px;padding:14px">'
|
||
+'<div style="font-size:10px;font-weight:700;color:rgba(255,255,255,.6);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">🏆 Лидер месяца · '+per+'</div>'
|
||
+'<div style="display:flex;align-items:center;gap:10px">'
|
||
+'<div style="width:40px;height:40px;border-radius:50%;background:'+top.color+';display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800;color:#fff">'+top.name.split(' ').map(function(w){return w[0];}).join('')+'</div>'
|
||
+'<div><div style="font-size:16px;font-weight:800;color:#fff">'+top.name+'</div>'
|
||
+'<div style="font-size:12px;color:rgba(255,255,255,.7)">'+_fmtMoney((top.months[per]||{revenue:0}).revenue)+' · '+((top.months[per]||{conversion:0}).conversion)+'% конверсия</div>'
|
||
+'</div></div></div>';
|
||
|
||
var cards=sorted.map(function(mgr,idx){
|
||
var s=mgr.months[per]||{};
|
||
var prev=mgr.months[months[1]]||{};
|
||
var revDelta=s.revenue&&prev.revenue?Math.round((s.revenue-prev.revenue)/prev.revenue*100):0;
|
||
var convDelta=s.conversion&&prev.conversion?s.conversion-prev.conversion:0;
|
||
return '<div style="background:var(--card);border-radius:14px;padding:14px;margin:0 12px 10px;box-shadow:0 2px 8px rgba(0,0,0,.06)">'
|
||
+'<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">'
|
||
+'<div style="width:36px;height:36px;border-radius:50%;background:'+mgr.color+';display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;color:#fff">'+mgr.name.split(' ').map(function(w){return w[0];}).join('')+'</div>'
|
||
+'<div style="flex:1"><div style="font-size:14px;font-weight:700;color:var(--ink)">'+mgr.name+'</div>'
|
||
+'<div style="font-size:11px;color:var(--muted)">Рейтинг: ★ '+s.rating+'</div></div>'
|
||
+'<div style="font-size:11px;font-weight:800;color:'+(idx===0?'#D97706':'var(--muted)')+'">#'+(idx+1)+'</div>'
|
||
+'</div>'
|
||
// Метрики 2×2
|
||
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">'
|
||
+[
|
||
{label:'Выручка', val:_fmtMoney(s.revenue||0), delta:revDelta+'%', up:revDelta>=0},
|
||
{label:'Конверсия', val:(s.conversion||0)+'%', delta:(convDelta>=0?'+':'')+convDelta+'%', up:convDelta>=0},
|
||
{label:'Визиты', val:(s.visits||0)+' кл.', delta:'', up:true},
|
||
{label:'Сделки', val:(s.deals||0)+' шт.', delta:'ср. '+_fmtMoney(s.avg||0), up:true},
|
||
].map(function(m){
|
||
return '<div style="background:var(--bg);border-radius:10px;padding:10px">'
|
||
+'<div style="font-size:10px;color:var(--muted);font-weight:600;margin-bottom:3px">'+m.label+'</div>'
|
||
+'<div style="font-size:15px;font-weight:800;color:var(--ink)">'+m.val+'</div>'
|
||
+(m.delta?'<div style="font-size:10px;font-weight:700;color:'+(m.up?'var(--success)':'var(--danger)')+'">'+m.delta+'</div>':'')
|
||
+'</div>';
|
||
}).join('')
|
||
+'</div></div>';
|
||
}).join('');
|
||
|
||
return periodTabs+topCard+cards;
|
||
}
|
||
|
||
// ── ЗАЯВКИ МЕНЕДЖЕРОВ ─────────────────────────────────────────────────────────
|
||
function _screenRequests(){
|
||
var filter = window._reqFilter || 'all';
|
||
var typeMap = {supply:'🛒 Расходники', visit:'🚗 Выезд', escalate:'🚨 Эскалация', schedule:'📅 График'};
|
||
var prioMap = {high:'Срочно', normal:'Обычная'};
|
||
|
||
var filtered = (window._MGR_REQUESTS||[]).filter(function(r){
|
||
return filter==='all' || r.type===filter;
|
||
}).sort(function(a,b){ return (a.prio==='high'?0:1)-(b.prio==='high'?0:1); });
|
||
|
||
var chips = ['all','supply','visit','escalate','schedule'].map(function(t){
|
||
var cnt = (window._MGR_REQUESTS||[]).filter(function(r){return (t==='all'||r.type===t)&&r.status==='new';}).length;
|
||
var label = t==='all'?'Все':typeMap[t];
|
||
return '<div onclick="window._reqFilter=\''+t+'\';_render()" style="padding:6px 13px;border-radius:20px;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;flex-shrink:0;border:1.5px solid '+(filter===t?'var(--accent)':'var(--line)')+';background:'+(filter===t?'var(--accent)':'var(--card)')+';color:'+(filter===t?'#fff':'var(--muted)')+'">'+label+(cnt>0&&t!=='all'?' ('+cnt+')':'')+'</div>';
|
||
}).join('');
|
||
|
||
var cards = filtered.length ? filtered.map(function(r){
|
||
var isDone = r.status==='done';
|
||
var prioColor = r.prio==='high'?'var(--danger)':'var(--muted)';
|
||
var bg = isDone?'rgba(0,0,0,.03)':r.prio==='high'?'rgba(239,68,68,.04)':'var(--card)';
|
||
var border = isDone?'var(--line)':r.prio==='high'?'rgba(239,68,68,.25)':'var(--line)';
|
||
return '<div style="background:'+bg+';border:1.5px solid '+border+';border-radius:14px;padding:13px 14px;margin-bottom:10px;opacity:'+(isDone?.55:1)+'">'
|
||
+'<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">'
|
||
+'<div style="width:32px;height:32px;border-radius:50%;background:'+r.color+';display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;flex-shrink:0">'+r.mgr.split(' ').map(function(w){return w[0];}).join('')+'</div>'
|
||
+'<div style="flex:1">'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--ink)">'+r.mgr+'</div>'
|
||
+'<div style="font-size:11px;color:var(--muted)">'+r.created+'</div>'
|
||
+'</div>'
|
||
+'<div style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">'
|
||
+'<span style="font-size:10px;font-weight:700;background:'+(r.type==='escalate'?'#FEE2E2':r.type==='supply'?'#DBEAFE':r.type==='visit'?'#DCFCE7':'#F3E8FF')+';color:'+(r.type==='escalate'?'#DC2626':r.type==='supply'?'#1D4ED8':r.type==='visit'?'#15803D':'#7C3AED')+';padding:2px 8px;border-radius:20px">'+typeMap[r.type]+'</span>'
|
||
+(r.prio==='high'&&!isDone?'<span style="font-size:10px;font-weight:700;color:var(--danger)">● Срочно</span>':'')
|
||
+'</div>'
|
||
+'</div>'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--ink);margin-bottom:4px">'+r.title+'</div>'
|
||
+'<div style="font-size:12px;color:var(--muted);line-height:1.45;margin-bottom:'+(isDone?'0':'10px')+'">'+r.body+'</div>'
|
||
+(!isDone?'<div style="display:flex;gap:7px">'
|
||
+'<button onclick="window._MGR_REQUESTS.find(function(x){return x.id===\''+r.id+'\';}).status=\'done\';_toast(\'Принято\',\'#059669\');_render()" style="flex:1;padding:8px;border-radius:10px;border:none;background:#DCFCE7;color:#15803D;font-size:12px;font-weight:700;cursor:pointer">✓ Принять</button>'
|
||
+'<button onclick="_toast(\'Отклонено — уведомление отправлено\',\'#DC2626\');window._MGR_REQUESTS.find(function(x){return x.id===\''+r.id+'\';}).status=\'done\';_render()" style="flex:1;padding:8px;border-radius:10px;border:none;background:#FEE2E2;color:#DC2626;font-size:12px;font-weight:700;cursor:pointer">✕ Отклонить</button>'
|
||
+'<button onclick="alert(\'Открыть чат с '+r.mgr+'…\')" style="padding:8px 12px;border-radius:10px;border:1.5px solid var(--line);background:var(--bg);color:var(--muted);font-size:12px;font-weight:700;cursor:pointer">💬</button>'
|
||
+'</div>':'')
|
||
+'</div>';
|
||
}).join('')
|
||
: '<div style="text-align:center;padding:40px 20px;color:var(--muted)">'
|
||
+'<div style="font-size:32px;margin-bottom:10px">🎉</div>'
|
||
+'<div style="font-size:14px;font-weight:600">Нет активных заявок</div>'
|
||
+'</div>';
|
||
|
||
return '<div style="padding:10px 16px 0">'
|
||
+'<div style="display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;padding-bottom:10px">'+chips+'</div>'
|
||
+cards
|
||
+'</div>';
|
||
}
|
||
|
||
// ── 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','#orders':'orders','#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>
|
||
|