mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 18:04:45 +00:00
feat: expedition — route detail, ETA, traveled path, e-signature, delivery close
This commit is contained in:
parent
0d9c452432
commit
c9e04a22c6
@ -236,6 +236,9 @@ const SCREENS = {
|
|||||||
staff_edit: 'Редактирование сотрудника',
|
staff_edit: 'Редактирование сотрудника',
|
||||||
staff_new: 'Новый сотрудник',
|
staff_new: 'Новый сотрудник',
|
||||||
staff_cat_confirm: 'Подтверждение категории',
|
staff_cat_confirm: 'Подтверждение категории',
|
||||||
|
expedition: 'Экспедиция',
|
||||||
|
expedition_detail: 'Маршрут экспедитора',
|
||||||
|
delivery_sign: 'Подпись акта',
|
||||||
settings: 'Настройки'
|
settings: 'Настройки'
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -344,6 +347,9 @@ function renderScreen(id) {
|
|||||||
case 'staff_edit': return screenStaffEdit();
|
case 'staff_edit': return screenStaffEdit();
|
||||||
case 'staff_new': return screenStaffNew();
|
case 'staff_new': return screenStaffNew();
|
||||||
case 'staff_cat_confirm': return screenStaffCatConfirm();
|
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 'assemblies': return screenAssemblies();
|
||||||
case 'assembly_detail': return screenAssemblyDetail();
|
case 'assembly_detail': return screenAssemblyDetail();
|
||||||
case 'pricelist': return screenPricelist();
|
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:'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: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:'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)
|
// 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';});
|
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)';
|
var bg = cat==='A' ? 'rgba(99,102,241,.1)' : 'rgba(245,158,11,.1)';
|
||||||
return '<span style="font-size:10px;font-weight:800;padding:2px 7px;border-radius:20px;background:'+bg+';color:'+col+'">Кат.'+cat+'</span>';
|
return '<span style="font-size:10px;font-weight:800;padding:2px 7px;border-radius:20px;background:'+bg+';color:'+col+'">Кат.'+cat+'</span>';
|
||||||
}
|
}
|
||||||
|
function routeBar(r) {
|
||||||
|
if(!r) return '';
|
||||||
|
var pct = Math.round(r.done/r.total*100);
|
||||||
|
return '<div style="margin-top:7px"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px"><span style="font-size:10px;color:var(--muted);font-weight:500">Маршрут</span><span style="font-size:11px;font-weight:800;color:var(--accent)">'+r.done+'/'+r.total+' точек · '+r.pkg+' уп.</span></div><div style="height:4px;background:rgba(0,0,0,.07);border-radius:2px;overflow:hidden"><div style="height:100%;width:'+pct+'%;background:var(--accent);border-radius:2px"></div></div></div>';
|
||||||
|
}
|
||||||
function loadBar(pct, active) {
|
function loadBar(pct, active) {
|
||||||
if (pct===null) return '';
|
if (pct===null) return '';
|
||||||
var col = pct>=75 ? 'var(--success)' : pct>=50 ? 'var(--accent)' : 'var(--warn)';
|
var col = pct>=75 ? 'var(--success)' : pct>=50 ? 'var(--accent)' : 'var(--warn)';
|
||||||
@ -930,7 +944,7 @@ function screenStaff() {
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
var cards = emps.map(function(e) {
|
var cards = emps.map(function(e) {
|
||||||
return '<div class="card" onclick="navigate(\'staff_detail\')" style="cursor:pointer;padding:12px 14px;margin-bottom:8px">'
|
return '<div class="card" onclick="navigate(e.role===\'Экспедитор\'?\'expedition_detail\':\'staff_detail\')" style="cursor:pointer;padding:12px 14px;margin-bottom:8px">'
|
||||||
+'<div style="display:flex;align-items:flex-start;gap:12px">'
|
+'<div style="display:flex;align-items:flex-start;gap:12px">'
|
||||||
+'<div class="avatar" style="background:var(--accent);flex-shrink:0">' + e.init + '</div>'
|
+'<div class="avatar" style="background:var(--accent);flex-shrink:0">' + e.init + '</div>'
|
||||||
+'<div style="flex:1;min-width:0">'
|
+'<div style="flex:1;min-width:0">'
|
||||||
@ -943,7 +957,7 @@ function screenStaff() {
|
|||||||
+'<div style="width:7px;height:7px;border-radius:50%;background:' + (e.active?'var(--success)':'var(--muted)') + ';flex-shrink:0"></div>'
|
+'<div style="width:7px;height:7px;border-radius:50%;background:' + (e.active?'var(--success)':'var(--muted)') + ';flex-shrink:0"></div>'
|
||||||
+'<span style="font-size:12px;color:var(--muted)">' + (e.active?'Онлайн':'Не активен') + ' · ' + e.jobs + (e.role==='Менеджер'?' клиентов':' заказ.' ) + ' сегодня · ⭐ ' + e.rating + '</span>'
|
+'<span style="font-size:12px;color:var(--muted)">' + (e.active?'Онлайн':'Не активен') + ' · ' + e.jobs + (e.role==='Менеджер'?' клиентов':' заказ.' ) + ' сегодня · ⭐ ' + e.rating + '</span>'
|
||||||
+'</div>'
|
+'</div>'
|
||||||
+ loadBar(e.load, e.active)
|
+ (e.role==='Экспедитор' ? routeBar(e.routes) : loadBar(e.load, e.active))
|
||||||
+'</div>'
|
+'</div>'
|
||||||
+'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color:var(--muted);flex-shrink:0;margin-top:3px"><polyline points="9 18 15 12 9 6"/></svg>'
|
+'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color:var(--muted);flex-shrink:0;margin-top:3px"><polyline points="9 18 15 12 9 6"/></svg>'
|
||||||
+'</div>'
|
+'</div>'
|
||||||
@ -961,7 +975,7 @@ function screenStaff() {
|
|||||||
+'<div class="chip-row" style="padding:0 16px;margin-bottom:10px">'
|
+'<div class="chip-row" style="padding:0 16px;margin-bottom:10px">'
|
||||||
+'<div class="chip active">Все</div>'
|
+'<div class="chip active">Все</div>'
|
||||||
+'<div class="chip">Сборщики</div>'
|
+'<div class="chip">Сборщики</div>'
|
||||||
+'<div class="chip">Замерщики</div>'
|
+'<div class="chip">Замерщики</div><div class="chip">Экспедиторы</div>'
|
||||||
+''
|
+''
|
||||||
+'</div>'
|
+'</div>'
|
||||||
+ warnBanner
|
+ warnBanner
|
||||||
@ -1513,6 +1527,291 @@ function screenStaffCatConfirm() {
|
|||||||
+'</div>';
|
+'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── 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 '<span style="font-size:10px;font-weight:700;padding:2px 8px;border-radius:20px;background:'+col+'18;color:'+col+'">'+d.slbl+'</span>';
|
||||||
|
};
|
||||||
|
|
||||||
|
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 ? '<div style="font-size:11px;color:var(--accent);font-weight:600;margin-top:4px">🕐 До клиента: '+cur.eta+'</div>' : '';
|
||||||
|
var prob = d.stops.find(function(s){return s.problem;});
|
||||||
|
var probLine = prob ? '<div style="font-size:11px;color:var(--danger);font-weight:600;margin-top:4px">⚠ '+prob.problem+'</div>' : '';
|
||||||
|
|
||||||
|
return '<div class="card" onclick="(function(){window._expDriverId=\''+d.id+'\';navigate(\'expedition_detail\')})()" style="cursor:pointer;padding:12px 14px;margin-bottom:8px">'
|
||||||
|
+'<div style="display:flex;align-items:flex-start;gap:10px">'
|
||||||
|
+'<div class="avatar" style="background:var(--accent);flex-shrink:0">'+d.init+'</div>'
|
||||||
|
+'<div style="flex:1;min-width:0">'
|
||||||
|
+'<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:4px">'
|
||||||
|
+'<span style="font-size:14px;font-weight:700;color:var(--ink)">'+d.name+'</span>'
|
||||||
|
+ statusChip(d)
|
||||||
|
+'</div>'
|
||||||
|
+'<div style="font-size:12px;color:var(--muted)">'+doneCnt+'/'+d.stops.length+' точек · '+d.pkg+' упак. · '+(d.traveled||0)+' км пройдено</div>'
|
||||||
|
+ etaLine + probLine
|
||||||
|
+'<div style="margin-top:8px"><div style="height:4px;background:rgba(0,0,0,.07);border-radius:2px;overflow:hidden"><div style="height:100%;width:'+pct+'%;background:'+(d.status==='problem'?'var(--danger)':d.status==='done'?'var(--success)':'var(--accent)')+';border-radius:2px;transition:width .4s"></div></div></div>'
|
||||||
|
+'</div>'
|
||||||
|
+'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color:var(--muted);flex-shrink:0;margin-top:4px"><polyline points="9 18 15 12 9 6"/></svg>'
|
||||||
|
+'</div>'
|
||||||
|
+'</div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
var summary = '<div style="display:flex;gap:8px;padding:0 16px 10px">'
|
||||||
|
+'<div style="flex:1;background:var(--card);border-radius:12px;padding:10px;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.06)">'
|
||||||
|
+'<div style="font-size:18px;font-weight:900;color:var(--accent)">42</div><div style="font-size:10px;color:var(--muted);margin-top:2px">уп. всего</div></div>'
|
||||||
|
+'<div style="flex:1;background:var(--card);border-radius:12px;padding:10px;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.06)">'
|
||||||
|
+'<div style="font-size:18px;font-weight:900;color:var(--success)">21</div><div style="font-size:10px;color:var(--muted);margin-top:2px">доставлено</div></div>'
|
||||||
|
+'<div style="flex:1;background:var(--card);border-radius:12px;padding:10px;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.06)">'
|
||||||
|
+'<div style="font-size:18px;font-weight:900;color:var(--danger)">1</div><div style="font-size:10px;color:var(--muted);margin-top:2px">проблема</div></div>'
|
||||||
|
+'</div>';
|
||||||
|
|
||||||
|
return '<div class="page">'
|
||||||
|
+'<div class="page-header">'
|
||||||
|
+'<h2>Экспедиция</h2>'
|
||||||
|
+'<button class="header-action" onclick="navigate(\'staff_new\')">'
|
||||||
|
+'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>'
|
||||||
|
+'</button>'
|
||||||
|
+'</div>'
|
||||||
|
+ summary
|
||||||
|
+'<div style="padding:0 16px">'+cards+'</div>'
|
||||||
|
+ navBar('staff')
|
||||||
|
+'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SCREEN EXP-2: EXPEDITION DETAIL ───────────────────────────
|
||||||
|
function screenExpeditionDetail() {
|
||||||
|
var id = window._expDriverId || 'zah';
|
||||||
|
var drivers = window._expDrivers;
|
||||||
|
if (!drivers) { navigate('expedition'); return '<div></div>'; }
|
||||||
|
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 '<div style="display:flex;flex-direction:column;align-items:center;gap:0">'
|
||||||
|
+'<div style="width:'+size+'px;height:'+size+'px;border-radius:50%;background:'+col+';flex-shrink:0;box-shadow:0 0 0 2px var(--card),0 0 0 3px '+col+'"></div>'
|
||||||
|
+'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route timeline visual
|
||||||
|
var totalStops = d.stops.length;
|
||||||
|
var routeLine = '<div style="padding:14px 16px">'
|
||||||
|
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px">Маршрут · '+d.stops.filter(function(s){return s.done;}).length+' из '+totalStops+' точек</div>'
|
||||||
|
+'<div style="position:relative;padding-left:28px">';
|
||||||
|
|
||||||
|
for (var i=0; i<d.stops.length; i++) {
|
||||||
|
var s = d.stops[i];
|
||||||
|
var isLast = i===d.stops.length-1;
|
||||||
|
var dotCol = s.done ? 'var(--success)' : s.current ? 'var(--accent)' : s.problem ? 'var(--danger)' : 'rgba(0,0,0,.18)';
|
||||||
|
var lineCol = s.done ? 'var(--success)' : 'rgba(0,0,0,.1)';
|
||||||
|
|
||||||
|
var timeDisp = s.done ? s.time : (s.eta || '—');
|
||||||
|
var timeLbl = s.done ? '' : (s.eta ? 'ETA ' : '');
|
||||||
|
var timeCol = s.done ? 'var(--success)' : (s.current ? 'var(--accent)' : 'var(--muted)');
|
||||||
|
|
||||||
|
var signBtn = '';
|
||||||
|
if (s.done && !s.signed) {
|
||||||
|
signBtn = '<button onclick="(function(){window._signStopIdx='+i+';navigate(\'delivery_sign\')})()" style="font-size:11px;font-weight:700;color:var(--accent);background:rgba(0,62,126,.08);border:1px solid rgba(0,62,126,.2);border-radius:6px;padding:3px 8px;cursor:pointer;margin-top:4px">Подписать акт</button>';
|
||||||
|
} else if (s.done && s.signed) {
|
||||||
|
signBtn = '<div style="font-size:11px;color:var(--success);font-weight:600;margin-top:3px">✓ Акт подписан</div>';
|
||||||
|
} else if (s.current) {
|
||||||
|
signBtn = '<button onclick="(function(){window._signStopIdx='+i+';navigate(\'delivery_sign\')})()" style="font-size:11px;font-weight:700;color:var(--card);background:var(--accent);border:none;border-radius:6px;padding:4px 10px;cursor:pointer;margin-top:4px">Закрыть доставку</button>';
|
||||||
|
}
|
||||||
|
if (s.problem) {
|
||||||
|
signBtn = '<div style="font-size:11px;color:var(--danger);font-weight:600;margin-top:3px;background:rgba(239,68,68,.07);border-radius:6px;padding:3px 8px">⚠ '+s.problem+'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
routeLine += '<div style="position:relative;padding-bottom:'+(isLast?'0':'18')+'px">'
|
||||||
|
// dot
|
||||||
|
+'<div style="position:absolute;left:-28px;top:2px;width:16px;height:16px;border-radius:50%;background:'+dotCol+';border:2px solid var(--card);box-shadow:0 0 0 2px '+dotCol+'"></div>'
|
||||||
|
// line to next
|
||||||
|
+ (isLast ? '' : '<div style="position:absolute;left:-21px;top:18px;width:2px;bottom:0;background:'+lineCol+'"></div>')
|
||||||
|
// content
|
||||||
|
+'<div style="background:'+(s.current?'rgba(0,62,126,.04)':'transparent')+';border-radius:10px;padding:'+(s.current?'8px 10px':'0 0')+'">'
|
||||||
|
+'<div style="display:flex;align-items:flex-start;justify-content:space-between">'
|
||||||
|
+'<div style="flex:1;min-width:0">'
|
||||||
|
+'<div style="font-size:13px;font-weight:700;color:var(--ink)">'+s.client+'</div>'
|
||||||
|
+'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+s.addr+'</div>'
|
||||||
|
+'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+s.pkg+' упак.</div>'
|
||||||
|
+ signBtn
|
||||||
|
+'</div>'
|
||||||
|
+'<div style="text-align:right;flex-shrink:0;margin-left:8px">'
|
||||||
|
+'<div style="font-size:12px;font-weight:800;color:'+timeCol+'">'+timeLbl+timeDisp+'</div>'
|
||||||
|
+(s.current ? '<div style="font-size:10px;color:var(--accent);font-weight:600;margin-top:2px">Сейчас</div>' : '')
|
||||||
|
+'</div>'
|
||||||
|
+'</div>'
|
||||||
|
+'</div>'
|
||||||
|
+'</div>';
|
||||||
|
}
|
||||||
|
routeLine += '</div></div>';
|
||||||
|
|
||||||
|
// 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 = '<div style="padding:0 16px 14px">'
|
||||||
|
+'<div style="background:var(--card);border-radius:14px;padding:12px 14px;box-shadow:0 2px 8px rgba(0,0,0,.06)">'
|
||||||
|
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Пройденный путь</div>'
|
||||||
|
+'<div style="display:flex;gap:8px;margin-bottom:10px">'
|
||||||
|
+'<div style="flex:1;background:var(--bg);border-radius:8px;padding:8px;text-align:center">'
|
||||||
|
+'<div style="font-size:17px;font-weight:900;color:var(--accent)">'+traveled+' км</div>'
|
||||||
|
+'<div style="font-size:10px;color:var(--muted)">пройдено</div></div>'
|
||||||
|
+(remaining>0?'<div style="flex:1;background:var(--bg);border-radius:8px;padding:8px;text-align:center">'
|
||||||
|
+'<div style="font-size:17px;font-weight:900;color:var(--muted)">'+remaining+' км</div>'
|
||||||
|
+'<div style="font-size:10px;color:var(--muted)">осталось</div></div>':'')
|
||||||
|
+'<div style="flex:1;background:var(--bg);border-radius:8px;padding:8px;text-align:center">'
|
||||||
|
+'<div style="font-size:17px;font-weight:900;color:var(--accent)">'+(d.startTime||'—')+'</div>'
|
||||||
|
+'<div style="font-size:10px;color:var(--muted)">выезд</div></div>'
|
||||||
|
+'</div>'
|
||||||
|
+ 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 '<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-top:1px solid rgba(0,0,0,.05);background:'+bg+';border-radius:6px;padding:6px 4px">'
|
||||||
|
+'<div style="font-size:11px;color:var(--ink);flex:1">'+(l.current?'▶ ':'✓ ')+'<span style="color:var(--muted)">'+l.from+'</span> → '+l.to+'</div>'
|
||||||
|
+'<div style="font-size:11px;font-weight:700;color:'+col+';white-space:nowrap">'+l.km+' км · '+l.min+' мин</div>'
|
||||||
|
+'</div>';
|
||||||
|
}).join('')
|
||||||
|
+'</div></div>';
|
||||||
|
|
||||||
|
// ETA block for active driver
|
||||||
|
var etaBlock = '';
|
||||||
|
var curStop = d.stops.find(function(s){return s.current;});
|
||||||
|
if (curStop) {
|
||||||
|
etaBlock = '<div style="margin:0 16px 12px;background:rgba(0,62,126,.06);border:1.5px solid rgba(0,62,126,.18);border-radius:12px;padding:12px 14px">'
|
||||||
|
+'<div style="display:flex;align-items:center;justify-content:space-between">'
|
||||||
|
+'<div>'
|
||||||
|
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">Прибытие к клиенту</div>'
|
||||||
|
+'<div style="font-size:28px;font-weight:900;color:var(--accent);margin-top:2px">'+curStop.eta+'</div>'
|
||||||
|
+'<div style="font-size:12px;color:var(--muted);margin-top:1px">'+curStop.client+' · '+curStop.addr+'</div>'
|
||||||
|
+'</div>'
|
||||||
|
+'<button style="background:var(--accent);color:#fff;border:none;border-radius:10px;padding:10px 12px;font-size:11px;font-weight:700;cursor:pointer;text-align:center;line-height:1.3">📨 Отправить<br>клиенту</button>'
|
||||||
|
+'</div>'
|
||||||
|
+'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="page">'
|
||||||
|
+'<div class="page-header">'
|
||||||
|
+'<button class="back-btn" onclick="navigate(\'expedition\')">'
|
||||||
|
+'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>'
|
||||||
|
+'</button>'
|
||||||
|
+'<h2>'+d.name.replace('ИП ','')+'</h2>'
|
||||||
|
+'<span style="margin-left:auto;font-size:10px;font-weight:700;padding:3px 10px;border-radius:20px;background:'+d.scolor+'18;color:'+d.scolor+'">'+d.slbl+'</span>'
|
||||||
|
+'</div>'
|
||||||
|
+ etaBlock
|
||||||
|
+ pathHtml
|
||||||
|
+ routeLine
|
||||||
|
+ navBar('staff')
|
||||||
|
+'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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 '<div></div>'; }
|
||||||
|
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 '<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(0,0,0,.05)">'
|
||||||
|
+'<span style="font-size:13px;color:var(--ink)">'+it.name+'</span>'
|
||||||
|
+'<span style="font-size:13px;font-weight:700;color:var(--ink)">'+it.qty+' шт.</span>'
|
||||||
|
+'</div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
var sigArea = signed
|
||||||
|
? '<div style="height:100px;background:rgba(16,185,129,.06);border:1.5px solid var(--success);border-radius:10px;display:flex;align-items:center;justify-content:center;margin-bottom:12px">'
|
||||||
|
+'<div style="text-align:center"><div style="font-size:22px">✓</div><div style="font-size:12px;font-weight:700;color:var(--success);margin-top:4px">Подписано</div></div>'
|
||||||
|
+'</div>'
|
||||||
|
: '<div style="height:100px;background:rgba(0,62,126,.03);border:1.5px dashed rgba(0,62,126,.25);border-radius:10px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;margin-bottom:12px;cursor:pointer" onclick="(function(){window._signDone=true;document.getElementById(\'screen\').innerHTML=renderScreen(\'delivery_sign\')})()">'
|
||||||
|
+'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--muted)" stroke-width="1.5"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>'
|
||||||
|
+'<div style="font-size:12px;color:var(--muted)">Нажмите — клиент ставит подпись</div>'
|
||||||
|
+'</div>';
|
||||||
|
|
||||||
|
var actionBtn = signed
|
||||||
|
? '<button onclick="(function(){window._signDone=false;navigate(\'expedition_detail\')})()" style="width:100%;padding:14px;background:var(--success);color:#fff;border:none;border-radius:12px;font-size:15px;font-weight:700;cursor:pointer">✓ Акт принят · Доставка закрыта</button>'
|
||||||
|
: '<button onclick="(function(){window._signDone=true;document.getElementById(\'screen\').innerHTML=renderScreen(\'delivery_sign\')})()" style="width:100%;padding:14px;background:var(--accent);color:#fff;border:none;border-radius:12px;font-size:15px;font-weight:700;cursor:pointer">Подписать акт</button>';
|
||||||
|
|
||||||
|
return '<div class="page">'
|
||||||
|
+'<div class="page-header">'
|
||||||
|
+'<button class="back-btn" onclick="navigate(\'expedition_detail\')">'
|
||||||
|
+'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>'
|
||||||
|
+'</button>'
|
||||||
|
+'<h2>Акт доставки</h2>'
|
||||||
|
+'</div>'
|
||||||
|
+'<div style="padding:16px">'
|
||||||
|
+'<div class="card">'
|
||||||
|
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px">Получатель</div>'
|
||||||
|
+'<div style="font-size:16px;font-weight:800;color:var(--ink);margin-bottom:4px">'+s.client+'</div>'
|
||||||
|
+'<div style="font-size:13px;color:var(--muted);margin-bottom:4px">'+s.addr+'</div>'
|
||||||
|
+'<div style="font-size:12px;color:var(--muted)">Доставка: '+d.name+' · '+(new Date()).toLocaleDateString('ru')+'</div>'
|
||||||
|
+'</div>'
|
||||||
|
+'<div class="card">'
|
||||||
|
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Состав доставки</div>'
|
||||||
|
+ itemRows
|
||||||
|
+'<div style="display:flex;justify-content:space-between;margin-top:10px;padding-top:8px;border-top:2px solid rgba(0,0,0,.06)">'
|
||||||
|
+'<span style="font-size:14px;font-weight:700;color:var(--ink)">Итого</span>'
|
||||||
|
+'<span style="font-size:14px;font-weight:800;color:var(--accent)">'+s.pkg+' упак.</span>'
|
||||||
|
+'</div>'
|
||||||
|
+'</div>'
|
||||||
|
+'<div class="card">'
|
||||||
|
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Электронная подпись клиента</div>'
|
||||||
|
+ sigArea
|
||||||
|
+'<div style="font-size:11px;color:var(--muted);text-align:center;margin-bottom:12px">'+(signed?'Подпись получена · '+new Date().toLocaleTimeString('ru',{hour:\'2-digit\',minute:\'2-digit\'}):'Передайте телефон клиенту для подписи')+'</div>'
|
||||||
|
+ actionBtn
|
||||||
|
+'</div>'
|
||||||
|
+'</div>'
|
||||||
|
+ navBar('staff')
|
||||||
|
+'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
// ── SCREEN 4: STAFF BLOCK CONFIRM ─────────────────────────────
|
// ── SCREEN 4: STAFF BLOCK CONFIRM ─────────────────────────────
|
||||||
function screenStaffBlockConfirm() {
|
function screenStaffBlockConfirm() {
|
||||||
// Роль сотрудника — в реале берётся из профиля, здесь симулируем: 'asm'|'meas'|'both'
|
// Роль сотрудника — в реале берётся из профиля, здесь симулируем: 'asm'|'meas'|'both'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user