diff --git a/docs/mockup_director.html b/docs/mockup_director.html index 22248ef..cbb2070 100644 --- a/docs/mockup_director.html +++ b/docs/mockup_director.html @@ -236,6 +236,9 @@ const SCREENS = { staff_edit: 'Редактирование сотрудника', staff_new: 'Новый сотрудник', staff_cat_confirm: 'Подтверждение категории', + expedition: 'Экспедиция', + expedition_detail: 'Маршрут экспедитора', + delivery_sign: 'Подпись акта', settings: 'Настройки' }; @@ -344,6 +347,9 @@ function renderScreen(id) { case 'staff_edit': return screenStaffEdit(); case 'staff_new': return screenStaffNew(); case 'staff_cat_confirm': return screenStaffCatConfirm(); + case 'expedition': return screenExpedition(); + case 'expedition_detail': return screenExpeditionDetail(); + case 'delivery_sign': return screenDeliverySign(); case 'assemblies': return screenAssemblies(); case 'assembly_detail': return screenAssemblyDetail(); case 'pricelist': return screenPricelist(); @@ -894,6 +900,9 @@ function screenStaff() { {init:'ФМ',name:'Фёдорова М.Р.',role:'Замерщик', cat:'A', load:79, jobs:2, rating:4.8, badge:'purple', active:true}, {init:'СО',name:'Смирнов О.К.', role:'Сборщик', cat:'B', load:68, jobs:2, rating:4.7, badge:'blue', active:true}, {init:'НП',name:'Николаев П.В.',role:'Сборщик', cat:'B', load:40, jobs:1, rating:4.2, badge:'yellow', active:false}, + {init:'ФЕ',name:'ИП Фёдоров Е.А.', role:'Экспедитор',cat:null,load:null,jobs:3, rating:4.9, badge:'green', active:true, routes:{done:3,total:6,pkg:18}}, + {init:'ЗА',name:'ИП Захаров А.И.', role:'Экспедитор',cat:null,load:null,jobs:2, rating:4.7, badge:'blue', active:true, routes:{done:1,total:5,pkg:14}}, + {init:'МС',name:'ИП Морозов С.В.', role:'Экспедитор',cat:null,load:null,jobs:0, rating:4.5, badge:'yellow', active:false,routes:{done:0,total:4,pkg:10}}, ]; // Business rule: avg load Cat A >= avg load Cat B (for assemblers only) var asmA = emps.filter(function(e){return e.role==='Сборщик'&&e.cat==='A';}); @@ -908,6 +917,11 @@ function screenStaff() { var bg = cat==='A' ? 'rgba(99,102,241,.1)' : 'rgba(245,158,11,.1)'; return 'Кат.'+cat+''; } + function routeBar(r) { + if(!r) return ''; + var pct = Math.round(r.done/r.total*100); + return '
Маршрут'+r.done+'/'+r.total+' точек · '+r.pkg+' уп.
'; + } function loadBar(pct, active) { if (pct===null) return ''; var col = pct>=75 ? 'var(--success)' : pct>=50 ? 'var(--accent)' : 'var(--warn)'; @@ -930,7 +944,7 @@ function screenStaff() { : ''; var cards = emps.map(function(e) { - return '
' + return '
' +'
' +'
' + e.init + '
' +'
' @@ -943,7 +957,7 @@ function screenStaff() { +'
' +'' + (e.active?'Онлайн':'Не активен') + ' · ' + e.jobs + (e.role==='Менеджер'?' клиентов':' заказ.' ) + ' сегодня · ⭐ ' + e.rating + '' +'
' - + loadBar(e.load, e.active) + + (e.role==='Экспедитор' ? routeBar(e.routes) : loadBar(e.load, e.active)) +'
' +'' +'
' @@ -961,7 +975,7 @@ function screenStaff() { +'
' +'
Все
' +'
Сборщики
' - +'
Замерщики
' + +'
Замерщики
Экспедиторы
' +'' +'
' + warnBanner @@ -1513,6 +1527,291 @@ function screenStaffCatConfirm() { +'
'; } + +// ── SCREEN EXP-1: EXPEDITION LIST ───────────────────────────── +function screenExpedition() { + if (window._expSel === undefined) window._expSel = null; + + var drivers = [ + {id:'fed', init:'ФЕ', name:'ИП Фёдоров Е.А.', phone:'+7 921 100-11-22', tg:'@fedorov_exp', + status:'done', slbl:'Завершено', scolor:'var(--success)', + stops:[ + {addr:'Мира 7, кв.18', client:'Краснова И.', pkg:6, done:true, time:'09:45', eta:null, signed:true}, + {addr:'Ленина 45, кв.3', client:'Орлов Д.', pkg:8, done:true, time:'11:20', eta:null, signed:true}, + {addr:'Советская 12', client:'Тихонов Р.', pkg:4, done:true, time:'13:10', eta:null, signed:true}, + ], pkg:18, startTime:'08:30', endTime:'13:40', dist:47}, + {id:'zah', init:'ЗА', name:'ИП Захаров А.И.', phone:'+7 921 200-33-44', tg:'@zahkarov_exp', + status:'active', slbl:'В пути', scolor:'var(--accent)', + stops:[ + {addr:'Космонавтов 44, кв.8', client:'Николаев П.', pkg:4, done:true, time:'10:15', eta:null, signed:true}, + {addr:'Просвещения 18', client:'Фомина С.', pkg:6, done:false, time:null, eta:'~13:20', signed:false, current:true}, + {addr:'Ветеранов 55, кв.12', client:'Зайцев М.', pkg:2, done:false, time:null, eta:'~14:40', signed:false}, + {addr:'Московский пр. 101', client:'Иванова Т.', pkg:2, done:false, time:null, eta:'~16:00', signed:false}, + ], pkg:14, startTime:'09:00', endTime:null, dist:28, traveled:12}, + {id:'mor', init:'МС', name:'ИП Морозов С.В.', phone:'+7 921 300-55-66', tg:'@morozov_exp', + status:'problem', slbl:'Проблема', scolor:'var(--danger)', + stops:[ + {addr:'Гагарина 3, кв.5', client:'Соколов А.', pkg:3, done:false, time:null, eta:null, signed:false, problem:'Не открывает дверь'}, + {addr:'Пушкина 20', client:'Белов К.', pkg:4, done:false, time:null, eta:null, signed:false}, + {addr:'Садовая 7, кв.14', client:'Новикова Е.',pkg:3, done:false, time:null, eta:null, signed:false}, + ], pkg:10, startTime:'09:30', endTime:null, dist:0, traveled:8}, + ]; + window._expDrivers = drivers; + + var statusChip = function(d) { + var col = d.scolor; + return ''+d.slbl+''; + }; + + var cards = drivers.map(function(d) { + var doneCnt = d.stops.filter(function(s){return s.done;}).length; + var pct = Math.round(doneCnt/d.stops.length*100); + var cur = d.stops.find(function(s){return s.current;}); + var etaLine = cur ? '
🕐 До клиента: '+cur.eta+'
' : ''; + var prob = d.stops.find(function(s){return s.problem;}); + var probLine = prob ? '
⚠ '+prob.problem+'
' : ''; + + return '
' + +'
' + +'
'+d.init+'
' + +'
' + +'
' + +''+d.name+'' + + statusChip(d) + +'
' + +'
'+doneCnt+'/'+d.stops.length+' точек · '+d.pkg+' упак. · '+(d.traveled||0)+' км пройдено
' + + etaLine + probLine + +'
' + +'
' + +'' + +'
' + +'
'; + }).join(''); + + var summary = '
' + +'
' + +'
42
уп. всего
' + +'
' + +'
21
доставлено
' + +'
' + +'
1
проблема
' + +'
'; + + return '
' + +'' + + summary + +'
'+cards+'
' + + navBar('staff') + +'
'; +} + +// ── SCREEN EXP-2: EXPEDITION DETAIL ─────────────────────────── +function screenExpeditionDetail() { + var id = window._expDriverId || 'zah'; + var drivers = window._expDrivers; + if (!drivers) { navigate('expedition'); return '
'; } + var d = drivers.find(function(x){return x.id===id;}) || drivers[1]; + + if (window._signStopIdx === undefined) window._signStopIdx = null; + + function stopDot(s, i) { + var col = s.done ? 'var(--success)' : s.current ? 'var(--accent)' : s.problem ? 'var(--danger)' : 'rgba(0,0,0,.15)'; + var size = s.current ? 14 : 10; + return '
' + +'
' + +'
'; + } + + // Route timeline visual + var totalStops = d.stops.length; + var routeLine = '
' + +'
Маршрут · '+d.stops.filter(function(s){return s.done;}).length+' из '+totalStops+' точек
' + +'
'; + + for (var i=0; iПодписать акт'; + } else if (s.done && s.signed) { + signBtn = '
✓ Акт подписан
'; + } else if (s.current) { + signBtn = ''; + } + if (s.problem) { + signBtn = '
⚠ '+s.problem+'
'; + } + + routeLine += '
' + // dot + +'
' + // line to next + + (isLast ? '' : '
') + // content + +'
' + +'
' + +'
' + +'
'+s.client+'
' + +'
'+s.addr+'
' + +'
'+s.pkg+' упак.
' + + signBtn + +'
' + +'
' + +'
'+timeLbl+timeDisp+'
' + +(s.current ? '
Сейчас
' : '') + +'
' + +'
' + +'
' + +'
'; + } + routeLine += '
'; + + // Traveled path section + var traveled = d.traveled || 0; + var remaining = (d.dist||0) - traveled; + var legs = []; + if (d.stops[0] && d.stops[0].done) legs.push({from:'Склад',to:d.stops[0].addr,km:7,min:18,done:true}); + if (d.stops[1] && d.stops[1].done) legs.push({from:d.stops[0].addr,to:d.stops[1].addr,km:5,min:14,done:true}); + if (d.stops[1] && d.stops[1].current) legs.push({from:d.stops[0].addr,to:d.stops[1].addr,km:5,min:25,done:false,current:true}); + + var pathHtml = '
' + +'
' + +'
Пройденный путь
' + +'
' + +'
' + +'
'+traveled+' км
' + +'
пройдено
' + +(remaining>0?'
' + +'
'+remaining+' км
' + +'
осталось
':'') + +'
' + +'
'+(d.startTime||'—')+'
' + +'
выезд
' + +'
' + + legs.map(function(l){ + var col = l.current ? 'var(--accent)' : l.done ? 'var(--success)' : 'var(--muted)'; + var bg = l.current ? 'rgba(0,62,126,.05)' : 'transparent'; + return '
' + +'
'+(l.current?'▶ ':'✓ ')+''+l.from+' → '+l.to+'
' + +'
'+l.km+' км · '+l.min+' мин
' + +'
'; + }).join('') + +'
'; + + // ETA block for active driver + var etaBlock = ''; + var curStop = d.stops.find(function(s){return s.current;}); + if (curStop) { + etaBlock = '
' + +'
' + +'
' + +'
Прибытие к клиенту
' + +'
'+curStop.eta+'
' + +'
'+curStop.client+' · '+curStop.addr+'
' + +'
' + +'' + +'
' + +'
'; + } + + return '
' + +'' + + etaBlock + + pathHtml + + routeLine + + navBar('staff') + +'
'; +} + +// ── SCREEN EXP-3: DELIVERY SIGN (E-SIGNATURE) ───────────────── +function screenDeliverySign() { + var id = window._expDriverId || 'zah'; + var idx = window._signStopIdx !== null ? window._signStopIdx : 1; + var drivers = window._expDrivers; + if (!drivers) { navigate('expedition'); return '
'; } + var d = drivers.find(function(x){return x.id===id;}) || drivers[1]; + var s = d.stops[idx] || d.stops[0]; + + if (window._signDone === undefined) window._signDone = false; + var signed = window._signDone; + + var items = [ + {name:'Шкаф-купе 2-дв.',qty:1}, + {name:'Комод тумба', qty:2}, + {name:'Система хранения',qty:1}, + ]; + var itemRows = items.map(function(it){ + return '
' + +''+it.name+'' + +''+it.qty+' шт.' + +'
'; + }).join(''); + + var sigArea = signed + ? '
' + +'
Подписано
' + +'
' + : '
' + +'' + +'
Нажмите — клиент ставит подпись
' + +'
'; + + var actionBtn = signed + ? '' + : ''; + + return '
' + +'' + +'
' + +'
' + +'
Получатель
' + +'
'+s.client+'
' + +'
'+s.addr+'
' + +'
Доставка: '+d.name+' · '+(new Date()).toLocaleDateString('ru')+'
' + +'
' + +'
' + +'
Состав доставки
' + + itemRows + +'
' + +'Итого' + +''+s.pkg+' упак.' + +'
' + +'
' + +'
' + +'
Электронная подпись клиента
' + + sigArea + +'
'+(signed?'Подпись получена · '+new Date().toLocaleTimeString('ru',{hour:\'2-digit\',minute:\'2-digit\'}):'Передайте телефон клиенту для подписи')+'
' + + actionBtn + +'
' + +'
' + + navBar('staff') + +'
'; +} + // ── SCREEN 4: STAFF BLOCK CONFIRM ───────────────────────────── function screenStaffBlockConfirm() { // Роль сотрудника — в реале берётся из профиля, здесь симулируем: 'asm'|'meas'|'both'