mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 20:04:46 +00:00
1450 lines
98 KiB
HTML
1450 lines
98 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">
|
||
<a href="./index.html" id="in-frame-back" style="display:flex;align-items:center;gap:6px;padding:5px 14px;background:rgba(0,62,126,.06);border-bottom:1px solid rgba(0,62,126,.09);font-family:Inter,system-ui,sans-serif;font-size:11px;font-weight:700;color:#003E7E;text-decoration:none;flex-shrink:0;z-index:200"><svg width="12" height="12" 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>
|
||
<div id="statusBar">
|
||
<span>9:41</span>
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<svg width="16" height="12" viewBox="0 0 16 12" fill="currentColor"><rect x="0" y="3" width="3" height="9" rx="1"/><rect x="4.5" y="2" width="3" height="10" rx="1"/><rect x="9" y="0" width="3" height="12" rx="1"/><rect x="13.5" y="0" width="2.5" height="12" rx="1" opacity=".3"/></svg>
|
||
<svg width="16" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="18" height="11" rx="2"/><path d="M20 11h2v4h-2"/></svg>
|
||
</div>
|
||
</div>
|
||
<div id="screen"></div>
|
||
<div class="bottom-nav" id="nav"></div>
|
||
</div>
|
||
|
||
<script>
|
||
// ── CONFIG (регулируется директором) ─────────────────────────────────────────
|
||
window._CONFIG = window._CONFIG || {
|
||
supplyApproval: false, // true = заявка уходит на согласование директору
|
||
inventoryPeriod: 30, // дней между плановыми инвентаризациями (0 = без расписания)
|
||
};
|
||
|
||
// ── ДАННЫЕ: ЗАКУПКИ ───────────────────────────────────────────────────────────
|
||
var _CAT = {
|
||
drink: {label:'Напитки', icon:'☕'},
|
||
office: {label:'Канцелярия', icon:'📎'},
|
||
clean: {label:'Хозяйство', icon:'🧹'},
|
||
misc: {label:'Расходники', icon:'📦'},
|
||
};
|
||
|
||
window._SUPPLIES = window._SUPPLIES || [
|
||
// Напитки
|
||
{id:'tea', cat:'drink', name:'Чай ассорти', unit:'пачка', cur:2, min:4, ordered:false},
|
||
{id:'coffee', cat:'drink', name:'Кофе молотый', unit:'пачка', cur:0, min:2, ordered:true, orderedDate:'20.05'},
|
||
{id:'sugar', cat:'drink', name:'Сахар', unit:'кг', cur:0.5,min:2, ordered:false},
|
||
{id:'candy', cat:'drink', name:'Конфеты', unit:'кг', cur:1, min:1.5,ordered:false},
|
||
{id:'cups', cat:'drink', name:'Одноразовые стаканы',unit:'уп', cur:3, min:2, ordered:false},
|
||
// Канцелярия
|
||
{id:'pens', cat:'office', name:'Ручки', unit:'шт', cur:4, min:10, ordered:false},
|
||
{id:'paper', cat:'office', name:'Бумага A4', unit:'пачка', cur:1, min:3, ordered:false},
|
||
{id:'folder', cat:'office', name:'Папки-скоросшиватели',unit:'шт', cur:6, min:5, ordered:false},
|
||
{id:'tape', cat:'office', name:'Скотч', unit:'рул', cur:2, min:2, ordered:false},
|
||
// Хозяйство
|
||
{id:'soap', cat:'clean', name:'Жидкое мыло', unit:'л', cur:0.3,min:1, ordered:false},
|
||
{id:'bags', cat:'clean', name:'Мусорные пакеты', unit:'рул', cur:1, min:3, ordered:false},
|
||
{id:'wet', cat:'clean', name:'Влажные салфетки', unit:'уп', cur:2, min:3, ordered:false},
|
||
{id:'cloth', cat:'clean', name:'Тряпки для уборки', unit:'шт', cur:4, min:4, ordered:false},
|
||
// Расходники
|
||
{id:'cart', cat:'misc', name:'Картридж принтера', unit:'шт', cur:0, min:1, ordered:true, orderedDate:'18.05'},
|
||
{id:'paper2', cat:'misc', name:'Термобумага', unit:'рул', cur:5, min:3, ordered:false},
|
||
{id:'bag2', cat:'misc', name:'Пакеты для клиентов',unit:'уп', cur:2, min:5, ordered:false},
|
||
];
|
||
|
||
// ── ДАННЫЕ: ИНВЕНТАРИЗАЦИЯ ───────────────────────────────────────────────────
|
||
var _ICAT = {tech:'Техника', furn:'Мебель'};
|
||
|
||
window._INVENTORY = window._INVENTORY || [
|
||
// Техника
|
||
{id:'tv1', icat:'tech', name:'Телевизор Samsung 55"', sn:'SN-2024-001', qty:1, cond:'ok', note:''},
|
||
{id:'pc1', icat:'tech', name:'Компьютер + монитор', sn:'PC-2023-042', qty:1, cond:'ok', note:''},
|
||
{id:'prn1', icat:'tech', name:'Принтер HP LaserJet', sn:'PR-2022-017', qty:1, cond:'warn', note:'Требует заправки'},
|
||
{id:'phone1', icat:'tech', name:'Телефон офисный', sn:'—', qty:2, cond:'ok', note:''},
|
||
{id:'cam1', icat:'tech', name:'IP-камера', sn:'CAM-001', qty:1, cond:'ok', note:''},
|
||
// Мебель
|
||
{id:'desk1', icat:'furn', name:'Стол менеджера', sn:'—', qty:3, cond:'ok', note:''},
|
||
{id:'chair1', icat:'furn', name:'Кресло (офис)', sn:'—', qty:4, cond:'warn', note:'У 1 кресла поврежден подлокотник'},
|
||
{id:'sofa1', icat:'furn', name:'Диван клиентский', sn:'—', qty:1, cond:'ok', note:''},
|
||
{id:'shelf1', icat:'furn', name:'Стеллаж образцов', sn:'—', qty:2, cond:'ok', note:''},
|
||
{id:'table1', icat:'furn', name:'Стол переговорный', sn:'—', qty:1, cond:'bad', note:'Скол на углу, нужна замена'},
|
||
];
|
||
|
||
window._LAST_INVENTORY = window._LAST_INVENTORY || '15.04.2026';
|
||
window._INV_SESSION = window._INV_SESSION || null;
|
||
|
||
// ── ДАННЫЕ: ЗАКАЗЫ ────────────────────────────────────────────────────────────
|
||
var _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;
|
||
// 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'&&gpsIssue>0)
|
||
? '<div style="position:absolute;top:4px;right:6px;width:16px;height:16px;border-radius:50%;background:var(--danger);color:#fff;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center">'+gpsIssue+'</div>'
|
||
: (it.id==='cash'&&(window._PENDING||[]).length>0)
|
||
? '<div style="position:absolute;top:4px;right:6px;width:16px;height:16px;border-radius:50%;background:var(--warn);color:#fff;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center">'+(window._PENDING||[]).length+'</div>'
|
||
: '';
|
||
return '<div class="nav-item'+(window._screen===it.id?' active':'')+'" onclick="_nav(\''+it.id+'\')" style="position:relative">'
|
||
+it.icon+badge
|
||
+'<span>'+it.label+'</span>'
|
||
+'</div>';
|
||
}).join('');
|
||
}
|
||
|
||
// ── STOCK HELPERS ──────────────────────────────────────────────────────────────
|
||
function _stockLevel(s){
|
||
if(s.cur<=0) return 'danger';
|
||
if(s.cur < s.min*0.6) return 'warn';
|
||
return 'ok';
|
||
}
|
||
function _stockPct(s){ return Math.min(100, Math.round((s.cur/s.min)*100)); }
|
||
function _stockColor(lvl){ return lvl==='danger'?'var(--danger)':lvl==='warn'?'var(--warn)':'var(--success)'; }
|
||
function _stockLabel(s){
|
||
var lvl=_stockLevel(s);
|
||
if(lvl==='danger') return s.cur<=0?'Нет':'Заканчивается';
|
||
if(lvl==='warn') return 'Мало';
|
||
return 'В норме';
|
||
}
|
||
|
||
// ── SCREEN ROUTER ─────────────────────────────────────────────────────────────
|
||
function _renderScreen(){
|
||
if(window._screen==='home') return _screenHome();
|
||
if(window._screen==='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>';
|
||
}
|
||
if(!overdueOrd.length&&!pendCash.length&&!needBuy.length&&!invBad.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 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">Чт, 22 мая 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
|
||
+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 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) : '';
|
||
|
||
return '<div class="page anim">'
|
||
+header
|
||
+modeBanner
|
||
+_staffGrid()
|
||
+(addSheet || (window._staffSel ? _staffDetail(window._staffSel) : _staffLegend()))
|
||
+'</div>';
|
||
}
|
||
|
||
function _staffGrid(){
|
||
var edit = window._staffEditMode;
|
||
|
||
var hdr = '<div style="display:flex;align-items:center;padding:0 10px;gap:3px;margin-bottom:6px">'
|
||
+'<div style="width:52px;flex-shrink:0"></div>'
|
||
+_DOW.map(function(d,i){
|
||
var isToday = _DATES[i]===_TODAY;
|
||
var isWknd = i===5||i===6;
|
||
return '<div style="flex:1;text-align:center">'
|
||
+'<div style="font-size:8px;font-weight:700;color:'+(isWknd?'var(--danger)':'var(--muted)')+'">'+d+'</div>'
|
||
+'<div style="font-size:11px;font-weight:'+(isToday?'800':'600')+';color:'+(isWknd?'var(--danger)':isToday?'var(--accent)':'var(--ink)')
|
||
+';'+(isToday?'background:rgba(0,62,126,.1);border-radius:5px;':'')+'padding:1px 0">'+_DATES[i]+'</div>'
|
||
+'</div>';
|
||
}).join('')
|
||
+'</div>';
|
||
|
||
var rows = _STAFF.map(function(st){
|
||
var plan = window._PLAN[st.id]||[];
|
||
var fact = window._FACT[st.id]||{};
|
||
var isSel = window._staffSel===st.id;
|
||
|
||
var row = '<div style="display:flex;align-items:center;padding:0 10px;gap:3px;margin-bottom:5px">';
|
||
|
||
// Аватар — в режиме просмотра кликабелен для деталей, в режиме правки — открывает добавление
|
||
var avatarClick = edit
|
||
? 'window._staffAddSheet=\''+(window._staffAddSheet===st.id?'null':st.id)+'\';window._staffSel=null;_render()'
|
||
: 'window._staffSel=\''+(isSel?'null':st.id)+'\';_render()';
|
||
var avatarRing = edit
|
||
? (window._staffAddSheet===st.id?';box-shadow:0 0 0 3px var(--accent)':'')
|
||
: (isSel?';box-shadow:0 0 0 3px rgba(0,62,126,.35)':'');
|
||
|
||
row += '<div style="width:52px;flex-shrink:0;cursor:pointer" onclick="'+avatarClick+'">'
|
||
+'<div style="width:34px;height:34px;border-radius:50%;background:'+st.color
|
||
+';display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;margin:0 auto 2px'+avatarRing+'">'
|
||
+st.short+'</div>'
|
||
+'<div style="font-size:8px;font-weight:600;color:'+(edit?'var(--accent)':'var(--muted)')+';text-align:center;line-height:1.1">'
|
||
+(edit?'смены':st.name.split(' ')[0])+'</div>'
|
||
+'</div>';
|
||
|
||
// Ячейки дней
|
||
_DATES.forEach(function(d,i){
|
||
var isWknd = i===5||i===6;
|
||
var isToday = d===_TODAY;
|
||
var isPast = d<_TODAY;
|
||
var inPlan = plan.indexOf(i)>=0;
|
||
var f = fact[i];
|
||
var cell = '';
|
||
|
||
if(isWknd){
|
||
// Выходной — всегда серый, нельзя добавить
|
||
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(239,68,68,.05);display:flex;align-items:center;justify-content:center">'
|
||
+'<span style="font-size:8px;color:rgba(239,68,68,.3);font-weight:700">вых</span></div>';
|
||
|
||
} else if(inPlan){
|
||
// ЗАПЛАНИРОВАН
|
||
if(f&&f.in){
|
||
// Есть факт прихода
|
||
var late = f.in>'10:15';
|
||
var bg = late?'rgba(245,158,11,.13)':'rgba(16,185,129,.11)';
|
||
var col = late?'var(--warn)':'var(--success)';
|
||
var icon = late?'⏰':'✓';
|
||
// В режиме правки прошлые факт-ячейки — только просмотр (нельзя убрать отработанный день)
|
||
cell='<div style="flex:1;height:38px;border-radius:7px;background:'+bg
|
||
+';display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;cursor:pointer" onclick="window._staffSel=\''+st.id+'\';window._staffEditMode=false;_render()">'
|
||
+'<span style="font-size:12px;font-weight:800;color:'+col+'">'+icon+'</span>'
|
||
+'<span style="font-size:8px;color:var(--muted)">'+f.in+'</span>'
|
||
+(f.gps&&!f.gps.ok&&f.gps.forced?'<span style="font-size:7px;color:var(--warn)">GPS?</span>':'')
|
||
+'</div>';
|
||
} else if(isPast){
|
||
// Неявка — только просмотр
|
||
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(239,68,68,.09);display:flex;align-items:center;justify-content:center;cursor:pointer" onclick="window._staffSel=\''+st.id+'\';window._staffEditMode=false;_render()">'
|
||
+'<span style="font-size:13px;color:var(--danger)">✗</span></div>';
|
||
} else if(isToday){
|
||
// Сегодня ожидается
|
||
var todayStyle = 'flex:1;height:38px;border-radius:7px;background:rgba(0,62,126,.08);border:1.5px solid rgba(0,62,126,.2);display:flex;align-items:center;justify-content:center';
|
||
cell='<div style="'+todayStyle+'">'
|
||
+'<span style="font-size:8px;font-weight:700;color:var(--accent)">план</span></div>';
|
||
} else {
|
||
// Будущий запланированный — в режиме правки можно УБРАТЬ
|
||
if(edit){
|
||
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(0,62,126,.08);border:1.5px solid rgba(0,62,126,.35);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;cursor:pointer;position:relative" onclick="_togglePlan(\''+st.id+'\','+i+')">'
|
||
+'<span style="font-size:8px;font-weight:700;color:var(--accent)">план</span>'
|
||
+'<span style="font-size:9px;color:var(--danger);font-weight:700">✕</span>'
|
||
+'</div>';
|
||
} else {
|
||
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(0,62,126,.05);border:1px dashed rgba(0,62,126,.2);display:flex;align-items:center;justify-content:center">'
|
||
+'<span style="font-size:8px;font-weight:700;color:rgba(0,62,126,.4)">план</span></div>';
|
||
}
|
||
}
|
||
} else {
|
||
// НЕ ЗАПЛАНИРОВАН
|
||
if(isPast||isToday){
|
||
// Прошлое/сегодня — просто пусто
|
||
cell='<div style="flex:1;height:38px;border-radius:7px;background:var(--bg);display:flex;align-items:center;justify-content:center">'
|
||
+'<span style="font-size:11px;color:rgba(0,0,0,.12)">—</span></div>';
|
||
} else {
|
||
// Будущий незапланированный — в режиме правки можно ДОБАВИТЬ
|
||
if(edit){
|
||
cell='<div style="flex:1;height:38px;border-radius:7px;background:rgba(0,62,126,.04);border:1.5px dashed rgba(0,62,126,.3);display:flex;align-items:center;justify-content:center;cursor:pointer" onclick="_togglePlan(\''+st.id+'\','+i+')">'
|
||
+'<span style="font-size:18px;font-weight:300;color:var(--accent);opacity:.5">+</span></div>';
|
||
} else {
|
||
cell='<div style="flex:1;height:38px;border-radius:7px;background:var(--bg);display:flex;align-items:center;justify-content:center">'
|
||
+'<span style="font-size:11px;color:rgba(0,0,0,.1)">—</span></div>';
|
||
}
|
||
}
|
||
}
|
||
row+=cell;
|
||
});
|
||
row+='</div>';
|
||
return row;
|
||
}).join('');
|
||
|
||
return '<div style="background:var(--card);padding:12px 0 6px">'
|
||
+hdr+rows
|
||
+'</div>';
|
||
}
|
||
|
||
// Панель быстрого добавления смен конкретному сотруднику
|
||
function _renderAddSheet(staffId){
|
||
var st = _STAFF.find(function(x){return x.id===staffId;});
|
||
if(!st) return '';
|
||
var plan = window._PLAN[st.id]||[];
|
||
// Показываем только будущие рабочие дни (не выходные, не прошлые)
|
||
var futureDays = [];
|
||
_DATES.forEach(function(d,i){
|
||
if(i>=5) return; // вых
|
||
if(d<=_TODAY) return; // прошлое/сегодня
|
||
futureDays.push({d:d, i:i, dow:_DOW[i], inPlan: plan.indexOf(i)>=0});
|
||
});
|
||
|
||
var daysHtml = futureDays.map(function(fd){
|
||
return '<div onclick="_togglePlan(\''+st.id+'\','+fd.i+')" '
|
||
+'style="display:flex;align-items:center;justify-content:space-between;padding:11px 0;border-bottom:1px solid rgba(0,0,0,.05);cursor:pointer">'
|
||
+'<div style="display:flex;align-items:center;gap:12px">'
|
||
+'<div style="width:34px;height:34px;border-radius:10px;background:'+(fd.inPlan?'var(--accent)':'var(--bg)')+';border:1.5px solid '+(fd.inPlan?'var(--accent)':'var(--line)')+';display:flex;flex-direction:column;align-items:center;justify-content:center">'
|
||
+'<span style="font-size:8px;font-weight:700;color:'+(fd.inPlan?'rgba(255,255,255,.8)':'var(--muted)')+'">'+fd.dow+'</span>'
|
||
+'<span style="font-size:13px;font-weight:800;color:'+(fd.inPlan?'#fff':'var(--ink)')+'">'+fd.d+'</span>'
|
||
+'</div>'
|
||
+'<span style="font-size:14px;font-weight:600;color:'+(fd.inPlan?'var(--accent)':'var(--muted)')+'">'+fd.dow+', '+fd.d+' мая</span>'
|
||
+'</div>'
|
||
+(fd.inPlan
|
||
? '<span style="font-size:12px;font-weight:700;color:var(--danger)">✕ Убрать</span>'
|
||
: '<span style="font-size:12px;font-weight:700;color:var(--accent)">+ Добавить</span>')
|
||
+'</div>';
|
||
}).join('');
|
||
|
||
if(!futureDays.length){
|
||
daysHtml='<div style="padding:16px 0;text-align:center;color:var(--muted);font-size:13px">Нет доступных дней для изменения</div>';
|
||
}
|
||
|
||
return '<div style="background:var(--card);margin:8px 0 0;border-top:3px solid '+st.color+'">'
|
||
+'<div style="padding:12px 14px 0;display:flex;align-items:center;justify-content:space-between">'
|
||
+'<div style="display:flex;align-items:center;gap:10px">'
|
||
+'<div style="width:36px;height:36px;border-radius:50%;background:'+st.color+';display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:#fff">'+st.short+'</div>'
|
||
+'<div>'
|
||
+'<div style="font-size:14px;font-weight:700;color:var(--ink)">'+st.name+'</div>'
|
||
+'<div style="font-size:11px;color:var(--muted)">Изменение расписания</div>'
|
||
+'</div>'
|
||
+'</div>'
|
||
+'<button onclick="window._staffAddSheet=null;_render()" style="border:none;background:var(--bg);border-radius:50%;width:30px;height:30px;cursor:pointer;font-size:18px;color:var(--muted)">×</button>'
|
||
+'</div>'
|
||
+'<div style="padding:0 14px 8px">'+daysHtml+'</div>'
|
||
+'</div>';
|
||
}
|
||
|
||
function _staffLegend(){
|
||
return '<div style="display:flex;gap:10px;padding:10px 14px;flex-wrap:wrap">'
|
||
+'<div style="display:flex;align-items:center;gap:5px"><div style="width:14px;height:14px;border-radius:4px;background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.3)"></div><span style="font-size:11px;color:var(--muted)">Вовремя</span></div>'
|
||
+'<div style="display:flex;align-items:center;gap:5px"><div style="width:14px;height:14px;border-radius:4px;background:rgba(245,158,11,.13);border:1px solid rgba(245,158,11,.3)"></div><span style="font-size:11px;color:var(--muted)">Опоздание</span></div>'
|
||
+'<div style="display:flex;align-items:center;gap:5px"><div style="width:14px;height:14px;border-radius:4px;background:rgba(239,68,68,.09);border:1px solid rgba(239,68,68,.3)"></div><span style="font-size:11px;color:var(--muted)">Неявка</span></div>'
|
||
+'<div style="display:flex;align-items:center;gap:5px"><div style="width:14px;height:14px;border-radius:4px;background:rgba(0,62,126,.05);border:1px dashed rgba(0,62,126,.2)"></div><span style="font-size:11px;color:var(--muted)">Запланировано</span></div>'
|
||
+'<div style="width:100%;padding-top:4px;font-size:11px;color:var(--muted)">Нажми аватар — детали сотрудника</div>'
|
||
+'</div>';
|
||
}
|
||
|
||
function _staffDetail(staffId){
|
||
var st = _STAFF.find(function(x){return x.id===staffId;});
|
||
if(!st) return '';
|
||
var plan = window._PLAN[st.id]||[];
|
||
var fact = window._FACT[st.id]||{};
|
||
|
||
// Stats (only past workdays)
|
||
var totalPlan=0, worked=0, late=0, noShow=0, gpsWarn=0;
|
||
plan.forEach(function(di){
|
||
if(di>=5) return;
|
||
var d=_DATES[di];
|
||
if(d>_TODAY) return;
|
||
totalPlan++;
|
||
var f=fact[di];
|
||
if(f&&f.in){
|
||
worked++;
|
||
if(f.in>'10:15') late++;
|
||
if(f.gps&&!f.gps.ok&&!f.gps.forced) gpsWarn++;
|
||
} else if(d<_TODAY){
|
||
noShow++;
|
||
}
|
||
});
|
||
|
||
var statsHtml='<div style="display:flex;gap:6px;margin-bottom:14px">'
|
||
+_mStat(worked+'/'+totalPlan,'Смен',worked<totalPlan&&totalPlan>0?'var(--warn)':'var(--success)')
|
||
+_mStat(late||'—','Опозд.',late?'var(--warn)':'var(--muted)')
|
||
+_mStat(noShow||'—','Неявок',noShow?'var(--danger)':'var(--muted)')
|
||
+_mStat(gpsWarn||'—','GPS⚠','gpsWarn'?gpsWarn?'var(--danger)':'var(--muted)':'var(--muted)')
|
||
+'</div>';
|
||
|
||
// Week rows (workdays only)
|
||
var daysHtml = _DOW.slice(0,5).map(function(dow,i){
|
||
var d=_DATES[i];
|
||
var inPlan=plan.indexOf(i)>=0;
|
||
var f=fact[i];
|
||
var isPast=d<_TODAY, isToday=d===_TODAY;
|
||
|
||
var status,statusCol,timeStr='',gpsStr='';
|
||
if(!inPlan){
|
||
status='Выходной'; statusCol='var(--muted)';
|
||
} else if(f&&f.in){
|
||
var isLate=f.in>'10:15';
|
||
status=isLate?'⏰ Опоздание':'✅ Вовремя';
|
||
statusCol=isLate?'var(--warn)':'var(--success)';
|
||
timeStr=f.in+(f.out?' – '+f.out:' – идёт смена');
|
||
if(f.gps){
|
||
var km=function(m){return m>=1000?Math.round(m/100)/10+'км':m+'м';};
|
||
gpsStr=f.gps.ok?'📍 '+f.gps.dist+'м · в салоне'
|
||
:f.gps.forced?'📍 '+km(f.gps.dist)+' · подтверждено вручную'
|
||
:'⚠️ '+km(f.gps.dist)+' от салона';
|
||
}
|
||
} else if(isPast){
|
||
status='❌ Неявка'; statusCol='var(--danger)';
|
||
} else if(isToday){
|
||
status='🕐 Ожидается'; statusCol='var(--accent)';
|
||
} else {
|
||
status='📅 Запланировано'; statusCol='rgba(0,62,126,.6)';
|
||
}
|
||
|
||
// Кнопки редактирования — ТОЛЬКО в режиме правки (у ком. директора не видны)
|
||
var btn='';
|
||
if(window._staffEditMode && !isPast && !isToday){
|
||
if(inPlan){
|
||
btn='<button onclick="_togglePlan(\''+st.id+'\','+i+')" style="padding:5px 10px;border-radius:8px;border:1px solid rgba(239,68,68,.3);background:rgba(239,68,68,.06);color:var(--danger);font-size:11px;font-weight:600;cursor:pointer;flex-shrink:0">Убрать</button>';
|
||
} else {
|
||
btn='<button onclick="_togglePlan(\''+st.id+'\','+i+')" style="padding:5px 10px;border-radius:8px;border:1px solid rgba(0,62,126,.2);background:rgba(0,62,126,.06);color:var(--accent);font-size:11px;font-weight:600;cursor:pointer;flex-shrink:0">+ В план</button>';
|
||
}
|
||
}
|
||
|
||
return '<div style="display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:1px solid rgba(0,0,0,.04)">'
|
||
+'<div style="width:34px;text-align:center;flex-shrink:0">'
|
||
+'<div style="font-size:10px;font-weight:700;color:var(--muted)">'+dow+'</div>'
|
||
+'<div style="font-size:13px;font-weight:700;color:var(--ink)">'+d+'</div>'
|
||
+'</div>'
|
||
+'<div style="flex:1;min-width:0">'
|
||
+'<div style="font-size:13px;font-weight:600;color:'+statusCol+'">'+status+'</div>'
|
||
+(timeStr?'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+timeStr+'</div>':'')
|
||
+(gpsStr?'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+gpsStr+'</div>':'')
|
||
+'</div>'
|
||
+btn
|
||
+'</div>';
|
||
}).join('');
|
||
|
||
return '<div style="background:var(--card);margin:8px 0 0;padding:14px 14px 4px">'
|
||
+'<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px">'
|
||
+'<div style="width:42px;height:42px;border-radius:50%;background:'+st.color
|
||
+';display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800;color:#fff">'+st.short+'</div>'
|
||
+'<div style="flex:1">'
|
||
+'<div style="font-size:15px;font-weight:700;color:var(--ink)">'+st.name+'</div>'
|
||
+'<div style="font-size:11px;color:var(--muted)">Менеджер · 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');
|
||
}
|
||
|
||
// ── 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>
|
||
|
||
|