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 = '';
+
+ 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'