waybill-app/index.html

692 lines
29 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Путевой лист</title>
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
*{box-sizing:border-box;margin:0;padding:0}
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--gray-100);min-height:100vh}
/* Header */
header{background:var(--ink);color:var(--white);padding:16px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;position:sticky;top:0;z-index:10}
header h1{font-size:22px;font-weight:800;display:flex;align-items:center;gap:8px}
header h1 span{color:var(--cyan)}
.role-toggle{display:flex;gap:4px;background:#1a1f2b;border-radius:8px;padding:3px}
.role-toggle button{background:transparent;border:none;color:#9aa3b2;padding:8px 20px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:600;transition:.15s}
.role-toggle button:hover{color:var(--white)}
.role-toggle button.active{background:var(--cyan);color:var(--ink)}
/* Container */
.container{max-width:800px;margin:0 auto;padding:40px 20px 80px}
/* Progress bar */
.progress{display:flex;align-items:center;justify-content:space-between;margin-bottom:40px;padding:0 4px}
.progress-step{display:flex;flex-direction:column;align-items:center;gap:6px;position:relative;z-index:1}
.progress-dot{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;background:var(--gray-100);color:var(--gray-500);border:2px solid #d0d5dd;transition:.3s}
.progress-step.done .progress-dot{background:var(--cyan);color:var(--ink);border-color:var(--cyan)}
.progress-step.active .progress-dot{background:var(--white);color:var(--ink);border-color:var(--cyan);box-shadow:0 0 0 3px var(--cyan-50)}
.progress-step.locked .progress-dot{background:var(--gray-100);color:var(--gray-500);border-color:#d0d5dd;opacity:.5}
.progress-label{font-size:11px;font-weight:600;color:var(--gray-500);text-align:center;max-width:80px;line-height:1.3;text-transform:uppercase;letter-spacing:.3px}
.progress-step.done .progress-label{color:var(--ink)}
.progress-step.active .progress-label{color:var(--ink);font-weight:700}
.progress-line{flex:1;height:2px;background:#d0d5dd;margin:0 -4px;position:relative;top:-18px}
.progress-line.done{background:var(--cyan)}
/* Step cards */
.step{display:none;background:var(--white);border-radius:16px;padding:32px;margin-bottom:24px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
.step.active{display:block}
.step-header{display:flex;align-items:center;gap:12px;margin-bottom:20px}
.step-number{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:18px;background:var(--cyan);color:var(--ink);flex-shrink:0}
.step-number.done{background:#10b981;color:var(--white)}
.step-number.locked{background:var(--gray-100);color:var(--gray-500)}
.step-title{font-size:20px;font-weight:700}
.step-badge{font-size:13px;padding:4px 12px;border-radius:20px;font-weight:600;margin-left:auto}
.badge-pending{background:#fef3c7;color:#92400e}
.badge-done{background:#d1fae5;color:#065f46}
.badge-locked{background:var(--gray-100);color:var(--gray-500)}
.badge-waiting{background:#dbeafe;color:#1e40af}
.step-content{min-height:60px}
.step-info{margin-bottom:16px;padding:16px;background:var(--gray-100);border-radius:12px;font-size:15px;line-height:1.7}
.step-info strong{font-weight:600}
.step-info .label{color:var(--gray-500);font-size:13px}
.form-group{margin-bottom:16px}
.form-group label{display:block;font-weight:600;margin-bottom:6px;font-size:14px;color:var(--gray-500)}
.form-row{display:flex;gap:16px;flex-wrap:wrap}
.form-row .form-group{flex:1;min-width:180px}
input[type="number"],input[type="text"]{width:100%;padding:12px 16px;border:2px solid #d0d5dd;border-radius:8px;font-size:17px;outline:none;transition:.2s}
input:focus{border-color:var(--cyan);box-shadow:0 0 0 3px var(--cyan-50)}
input:disabled{background:var(--gray-100);color:var(--gray-500);cursor:not-allowed}
/* Buttons */
.btn{display:inline-flex;align-items:center;gap:8px;padding:12px 24px;border-radius:8px;font-weight:700;font-size:15px;border:none;cursor:pointer;transition:.15s;text-decoration:none}
.btn-primary{background:var(--cyan);color:var(--ink)}
.btn-primary:hover{background:#1be5ff}
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
.btn-secondary{background:var(--ink);color:var(--white)}
.btn-secondary:hover{background:#1f2636}
.btn-outline{background:transparent;color:var(--ink);border:2px solid var(--ink)}
.btn-outline:hover{background:var(--ink);color:var(--white)}
.btn-success{background:#10b981;color:var(--white)}
.btn-success:hover{background:#059669}
.btn-small{padding:8px 16px;font-size:13px}
.btn-block{width:100%;justify-content:center}
/* Mechanic view */
#mechanic-view{display:none}
.tabs{display:flex;gap:4px;margin-bottom:24px;background:var(--white);border-radius:12px;padding:4px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
.tab{flex:1;padding:10px;border:none;background:transparent;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;color:var(--gray-500);transition:.15s;text-align:center}
.tab:hover{color:var(--ink)}
.tab.active{background:var(--ink);color:var(--white)}
.tab-count{display:inline-block;background:var(--cyan);color:var(--ink);border-radius:10px;padding:0 8px;font-size:12px;margin-left:6px;font-weight:700}
.tab.active .tab-count{background:var(--cyan);color:var(--ink)}
/* Waybill card */
.waybill-card{background:var(--white);border-radius:16px;padding:24px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.06);border-left:4px solid var(--cyan)}
.waybill-card.status-approved{border-left-color:#10b981}
.waybill-card-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px;flex-wrap:wrap;gap:8px}
.waybill-id{font-size:13px;color:var(--gray-500)}
.waybill-status{font-size:13px;font-weight:600;padding:4px 12px;border-radius:20px}
.waybill-status.pending{background:#fef3c7;color:#92400e}
.waybill-status.approved{background:#d1fae5;color:#065f46}
.waybill-info{display:grid;grid-template-columns:1fr 1fr;gap:8px 24px;font-size:15px;margin-bottom:16px}
.waybill-info .info-item{}
.waybill-info .info-label{color:var(--gray-500);font-size:13px}
.waybill-info .info-value{font-weight:600}
.waybill-actions{margin-top:12px;padding-top:16px;border-top:1px solid var(--gray-100)}
/* Empty state */
.empty-state{text-align:center;padding:60px 20px;color:var(--gray-500)}
.empty-state .emoji{font-size:48px;margin-bottom:16px}
.empty-state p{font-size:18px}
/* Loading spinner */
.spinner{display:inline-block;width:18px;height:18px;border:2px solid transparent;border-top-color:currentColor;border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* View header */
.view-header{margin-bottom:32px}
.view-header h2{font-size:28px;font-weight:800;margin-bottom:4px}
.view-header p{color:var(--gray-500);font-size:15px}
/* Notifications */
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--ink);color:var(--white);padding:14px 24px;border-radius:12px;font-weight:600;font-size:15px;z-index:100;box-shadow:0 8px 30px rgba(0,0,0,.2);animation:toastIn .3s ease;max-width:90vw;text-align:center}
.toast.success{background:#10b981}
@keyframes toastIn{from{opacity:0;transform:translateX(-50%) translateY(20px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
/* Responsive */
@media(max-width:640px){header{padding:12px 16px;flex-direction:column;align-items:stretch;gap:8px}header h1{font-size:18px}.container{padding:24px 16px 60px}.step{padding:24px}.step-title{font-size:17px}.progress-label{font-size:10px;max-width:60px}.progress-dot{width:30px;height:30px;font-size:12px}.form-row{flex-direction:column}.form-row .form-group{min-width:auto}.waybill-info{grid-template-columns:1fr}.view-header h2{font-size:24px}}
</style>
</head>
<body>
<header>
<h1><span></span> Путевой лист</h1>
<div class="role-toggle">
<button id="role-installer" class="active" onclick="switchRole('installer')">🔧 Инсталлятор</button>
<button id="role-mechanic" onclick="switchRole('mechanic')">🔩 Механик</button>
</div>
</header>
<div class="container">
<!-- ===== INSTALLER VIEW ===== -->
<div id="installer-view">
<div class="view-header">
<h2>Оформление путевого листа</h2>
<p>Заполни все этапы по порядку</p>
</div>
<!-- Progress -->
<div class="progress" id="progress-bar"></div>
<!-- Step 1 -->
<div class="step" id="step-1">
<div class="step-header">
<div class="step-number">1</div>
<div class="step-title">Апрув диспетчера</div>
<div class="step-badge" id="badge-1">⏳ Ожидание</div>
</div>
<div class="step-content">
<div class="step-info" id="step-1-info">
Запроси подтверждение у диспетчера, чтобы получить машину и данные.
</div>
<button class="btn btn-primary btn-block" id="btn-step1" onclick="step1Action()">
Запросить апрув
</button>
</div>
</div>
<!-- Step 2 -->
<div class="step" id="step-2">
<div class="step-header">
<div class="step-number">2</div>
<div class="step-title">Медосмотр</div>
<div class="step-badge" id="badge-2">🔒 Заблокировано</div>
</div>
<div class="step-content">
<div class="step-info" id="step-2-info">
Отметь прохождение предрейсового медицинского осмотра.
</div>
<button class="btn btn-primary btn-block" id="btn-step2" onclick="step2Action()" disabled>
Отметить прохождение
</button>
</div>
</div>
<!-- Step 3 -->
<div class="step" id="step-3">
<div class="step-header">
<div class="step-number">3</div>
<div class="step-title">Показания спидометра</div>
<div class="step-badge" id="badge-3">🔒 Заблокировано</div>
</div>
<div class="step-content">
<div class="step-info" id="step-3-info">
Введи текущий пробег и планируемый километраж на смену.
</div>
<div class="form-row">
<div class="form-group">
<label>Текущий пробег (км)</label>
<input type="number" id="odo-current" min="0" placeholder="например 45230" disabled>
</div>
<div class="form-group">
<label>Планирую проехать (км)</label>
<input type="number" id="odo-plan" min="1" placeholder="например 300" disabled>
</div>
</div>
<button class="btn btn-primary btn-block" id="btn-step3" onclick="step3Action()" disabled style="margin-top:8px">
Сохранить показания
</button>
</div>
</div>
<!-- Step 4 -->
<div class="step" id="step-4">
<div class="step-header">
<div class="step-number">4</div>
<div class="step-title">Осмотр механика</div>
<div class="step-badge" id="badge-4">🔒 Заблокировано</div>
</div>
<div class="step-content">
<div class="step-info" id="step-4-info">
Отправь заявку механику на осмотр транспортного средства.
</div>
<button class="btn btn-primary btn-block" id="btn-step4" onclick="step4Action()" disabled>
Отправить на осмотр
</button>
</div>
</div>
</div>
<!-- ===== MECHANIC VIEW ===== -->
<div id="mechanic-view">
<div class="view-header">
<h2>Заявки на осмотр</h2>
<p>Принимай и подтверждай осмотры транспортных средств</p>
</div>
<div class="tabs">
<button class="tab active" id="tab-new" onclick="switchTab('new')">
📋 Новые <span class="tab-count" id="count-new">0</span>
</button>
<button class="tab" id="tab-history" onclick="switchTab('history')">
📜 История
</button>
</div>
<div id="waybills-list"></div>
</div>
</div>
<script>
// ===== DATA LAYER =====
const KEYS = { INSTALLER: 'waybill_installer', WAYBILLS: 'waybills' };
function getInstaller() {
try { return JSON.parse(localStorage.getItem(KEYS.INSTALLER)) || getDefaultInstaller(); }
catch { return getDefaultInstaller(); }
}
function getDefaultInstaller() {
return {
step1_done: false, step2_done: false, step3_done: false, step4_done: false,
installerName: '', vehicle: '', odometerCurrent: null, odometerPlanned: null,
sentToMechanic: false, waybillId: null, mechanicApproved: false
};
}
function saveInstaller(data) {
localStorage.setItem(KEYS.INSTALLER, JSON.stringify(data));
}
function getWaybills() {
try { return JSON.parse(localStorage.getItem(KEYS.WAYBILLS)) || []; }
catch { return []; }
}
function saveWaybills(list) {
localStorage.setItem(KEYS.WAYBILLS, JSON.stringify(list));
}
function generateId() { return 'wb_' + Date.now() + '_' + Math.random().toString(36).slice(2,6); }
// ===== API LAYER (mock — ready for real integration) =====
const API = {
getDispatcherApproval: async () => {
// TODO: заменить на реальный fetch('/api/dispatcher/approve?driverId=...')
return new Promise(resolve => {
setTimeout(() => {
resolve({
success: true,
installerName: 'Сергей Иванов',
vehicle: 'ГАЗель NEXT, А123ВС 77'
});
}, 1200);
});
},
getMedicalStatus: async () => {
// TODO: заменить на реальный fetch('/api/medical/check?driverId=...')
return new Promise(resolve => {
setTimeout(() => {
resolve({ success: true, passed: true });
}, 1000);
});
},
submitMechanicApproval: async (waybillId) => {
// TODO: заменить на реальный fetch('/api/mechanic/approve', { method: 'POST', body: {...} })
return new Promise(resolve => {
setTimeout(() => {
resolve({ success: true });
}, 800);
});
}
};
// ===== NOTIFICATION =====
function toast(msg, type) {
const el = document.createElement('div');
el.className = 'toast' + (type ? ' ' + type : '');
el.textContent = msg;
document.body.appendChild(el);
setTimeout(() => { el.remove(); }, 3000);
}
// ===== ROLE SWITCHING =====
let currentRole = 'installer';
let currentTab = 'new';
function switchRole(role) {
currentRole = role;
document.getElementById('role-installer').className = role === 'installer' ? 'active' : '';
document.getElementById('role-mechanic').className = role === 'mechanic' ? 'active' : '';
document.getElementById('installer-view').style.display = role === 'installer' ? 'block' : 'none';
document.getElementById('mechanic-view').style.display = role === 'mechanic' ? 'block' : 'none';
if (role === 'installer') renderInstaller();
else renderMechanic();
}
function switchTab(tab) {
currentTab = tab;
document.getElementById('tab-new').className = tab === 'new' ? 'tab active' : 'tab';
document.getElementById('tab-history').className = tab === 'history' ? 'tab active' : 'tab';
renderWaybills();
}
// ===== INSTALLER RENDER =====
function renderInstaller() {
const s = getInstaller();
renderProgress(s);
for (let i = 1; i <= 4; i++) {
renderStep(i, s);
}
}
function renderProgress(s) {
const steps = [
{ label: 'Апрув', done: s.step1_done },
{ label: 'Медосмотр', done: s.step2_done },
{ label: 'Пробег', done: s.step3_done },
{ label: 'Осмотр', done: s.sentToMechanic || s.step4_done }
];
const activeIdx = steps.findIndex(st => !st.done);
const html = steps.map((st, i) => {
let cls = 'progress-step';
if (st.done) cls += ' done';
else if (i === activeIdx || activeIdx === -1) cls += ' active';
else cls += ' locked';
const icon = st.done ? '✓' : (i + 1);
return `
<div class="${cls}">
<div class="progress-dot">${icon}</div>
<div class="progress-label">${st.label}</div>
</div>
${i < 3 ? `<div class="progress-line ${st.done ? 'done' : ''}"></div>` : ''}
`;
}).join('');
document.getElementById('progress-bar').innerHTML = html;
}
function renderStep(num, s) {
const stepEl = document.getElementById('step-' + num);
const badgeEl = document.getElementById('badge-' + num);
const btnEl = document.getElementById('btn-step' + num);
let isDone = false, isLocked = false, badgeText = '', badgeCls = '', btnText = '', btnDisabled = true, infoHtml = '';
switch (num) {
case 1: {
isDone = s.step1_done;
isLocked = false;
if (isDone) {
badgeText = '✅ Получен'; badgeCls = 'badge-done';
btnText = 'Апрув получен'; btnDisabled = true;
infoHtml = `<strong>Машина:</strong> ${s.vehicle}<br><strong>Водитель:</strong> ${s.installerName}`;
} else {
badgeText = '⏳ Ожидание'; badgeCls = 'badge-pending';
btnText = 'Запросить апрув'; btnDisabled = false;
infoHtml = 'Запроси подтверждение у диспетчера, чтобы получить машину и данные.';
}
break;
}
case 2: {
isDone = s.step2_done;
isLocked = !s.step1_done;
if (isLocked) {
badgeText = '🔒 Заблокировано'; badgeCls = 'badge-locked';
btnText = 'Отметить прохождение'; btnDisabled = true;
infoHtml = 'Сначала получи апрув диспетчера.';
} else if (isDone) {
badgeText = '✅ Пройден'; badgeCls = 'badge-done';
btnText = 'Пройден'; btnDisabled = true;
infoHtml = 'Медосмотр успешно пройден.';
} else {
badgeText = '⏳ Ожидание'; badgeCls = 'badge-pending';
btnText = 'Отметить прохождение'; btnDisabled = false;
infoHtml = 'Отметь прохождение предрейсового медицинского осмотра.';
}
break;
}
case 3: {
isDone = s.step3_done;
isLocked = !s.step2_done;
document.getElementById('odo-current').disabled = isLocked || isDone;
document.getElementById('odo-plan').disabled = isLocked || isDone;
if (isLocked) {
badgeText = '🔒 Заблокировано'; badgeCls = 'badge-locked';
btnText = 'Сохранить показания'; btnDisabled = true;
infoHtml = 'Сначала пройди медосмотр.';
} else if (isDone) {
badgeText = '✅ Сохранено'; badgeCls = 'badge-done';
btnText = 'Сохранено'; btnDisabled = true;
const diff = s.odometerPlanned || 0;
infoHtml = `<strong>Текущий пробег:</strong> ${s.odometerCurrent.toLocaleString()} км<br><strong>Планируется проехать:</strong> ${diff} км`;
document.getElementById('odo-current').value = s.odometerCurrent || '';
document.getElementById('odo-plan').value = s.odometerPlanned || '';
} else {
badgeText = '⏳ Ожидание ввода'; badgeCls = 'badge-pending';
btnText = 'Сохранить показания'; btnDisabled = true; // enabled by input change
infoHtml = 'Введи текущий пробег и планируемый километраж на смену.';
document.getElementById('odo-current').value = s.odometerCurrent || '';
document.getElementById('odo-plan').value = s.odometerPlanned || '';
btnDisabled = !(parseInt(document.getElementById('odo-current').value) > 0 && parseInt(document.getElementById('odo-plan').value) > 0);
}
break;
}
case 4: {
isLocked = !s.step3_done;
const sent = s.sentToMechanic;
const approved = s.mechanicApproved;
if (approved) {
badgeText = '✅ Подтверждён'; badgeCls = 'badge-done';
btnText = 'Осмотр пройден'; btnDisabled = true;
infoHtml = 'Осмотр транспортного средства подтверждён механиком.';
isDone = true;
} else if (sent) {
badgeText = '⏳ На рассмотрении'; badgeCls = 'badge-waiting';
btnText = 'Отправлено'; btnDisabled = true;
infoHtml = 'Заявка отправлена механику. Ожидай подтверждения.';
isDone = false;
} else if (isLocked) {
badgeText = '🔒 Заблокировано'; badgeCls = 'badge-locked';
btnText = 'Отправить на осмотр'; btnDisabled = true;
infoHtml = 'Сначала заполни показания спидометра.';
} else {
badgeText = '⏳ Готово к отправке'; badgeCls = 'badge-pending';
btnText = 'Отправить на осмотр'; btnDisabled = false;
infoHtml = 'Отправь заявку механику на осмотр транспортного средства.';
}
break;
}
}
badgeEl.textContent = badgeText;
badgeEl.className = 'step-badge ' + badgeCls;
btnEl.textContent = btnText;
btnEl.disabled = btnDisabled;
document.getElementById('step-' + num + '-info').innerHTML = infoHtml;
let stepCls = 'step';
if (isDone) stepCls += ' active';
else if (!isLocked) stepCls += ' active';
stepEl.className = stepCls;
const numEl = stepEl.querySelector('.step-number');
if (numEl) {
numEl.className = 'step-number';
if (isDone) numEl.classList.add('done');
else if (isLocked) numEl.classList.add('locked');
numEl.textContent = isDone ? '✓' : num;
}
}
// ===== INSTALLER ACTIONS =====
async function step1Action() {
const btn = document.getElementById('btn-step1');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Запрос...';
try {
const res = await API.getDispatcherApproval();
if (res.success) {
const s = getInstaller();
s.step1_done = true;
s.installerName = res.installerName;
s.vehicle = res.vehicle;
saveInstaller(s);
renderInstaller();
toast('✅ Апрув получен: ' + res.vehicle);
}
} catch(e) {
toast('Ошибка запроса к диспетчеру', '');
btn.disabled = false;
btn.textContent = 'Запросить апрув';
}
}
async function step2Action() {
const btn = document.getElementById('btn-step2');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Отправка...';
try {
const res = await API.getMedicalStatus();
if (res.success && res.passed) {
const s = getInstaller();
s.step2_done = true;
saveInstaller(s);
renderInstaller();
toast('✅ Медосмотр пройден');
} else {
toast('Медосмотр не пройден');
btn.disabled = false;
btn.textContent = 'Отметить прохождение';
}
} catch(e) {
toast('Ошибка запроса медосмотра');
btn.disabled = false;
btn.textContent = 'Отметить прохождение';
}
}
function step3Action() {
const odoCur = parseInt(document.getElementById('odo-current').value);
const odoPlan = parseInt(document.getElementById('odo-plan').value);
if (!odoCur || !odoPlan || odoCur < 0 || odoPlan < 1) {
toast('Заполни оба поля корректно');
return;
}
const s = getInstaller();
s.step3_done = true;
s.odometerCurrent = odoCur;
s.odometerPlanned = odoPlan;
saveInstaller(s);
renderInstaller();
toast('✅ Показания сохранены');
}
function onInputChange() {
const s = getInstaller();
if (s.step3_done) return;
const odoCur = parseInt(document.getElementById('odo-current').value);
const odoPlan = parseInt(document.getElementById('odo-plan').value);
const btn = document.getElementById('btn-step3');
btn.disabled = !(odoCur > 0 && odoPlan > 0);
}
async function step4Action() {
const s = getInstaller();
const waybillId = generateId();
s.sentToMechanic = true;
s.waybillId = waybillId;
s.step4_done = true;
saveInstaller(s);
const waybills = getWaybills();
waybills.push({
id: waybillId,
installerName: s.installerName,
vehicle: s.vehicle,
odometerCurrent: s.odometerCurrent,
odometerPlanned: s.odometerPlanned,
mechanicStatus: 'pending',
createdAt: new Date().toISOString(),
approvedAt: null
});
saveWaybills(waybills);
renderInstaller();
toast('🚛 Заявка отправлена механику');
}
// ===== MECHANIC RENDER =====
function renderMechanic() {
renderWaybills();
}
function renderWaybills() {
const list = getWaybills();
const isNew = currentTab === 'new';
const filtered = isNew ? list.filter(w => w.mechanicStatus === 'pending') : list.filter(w => w.mechanicStatus === 'approved').reverse();
document.getElementById('count-new').textContent = list.filter(w => w.mechanicStatus === 'pending').length;
const container = document.getElementById('waybills-list');
if (filtered.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="emoji">${isNew ? '📭' : '📪'}</div>
<p>${isNew ? 'Новых заявок нет' : 'История пуста'}</p>
</div>
`;
return;
}
container.innerHTML = filtered.map(w => {
const created = new Date(w.createdAt);
const dateStr = created.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' });
const approvedStr = w.approvedAt ? new Date(w.approvedAt).toLocaleString('ru-RU', { day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' }) : '';
return `
<div class="waybill-card ${w.mechanicStatus === 'approved' ? 'status-approved' : ''}">
<div class="waybill-card-header">
<div>
<div class="waybill-id">№ ${w.id} · ${dateStr}</div>
</div>
<span class="waybill-status ${w.mechanicStatus}">${w.mechanicStatus === 'pending' ? '⏳ Ожидает' : '✅ Подтверждён'}</span>
</div>
<div class="waybill-info">
<div class="info-item">
<div class="info-label">Водитель</div>
<div class="info-value">${w.installerName}</div>
</div>
<div class="info-item">
<div class="info-label">Машина</div>
<div class="info-value">${w.vehicle}</div>
</div>
<div class="info-item">
<div class="info-label">Текущий пробег</div>
<div class="info-value">${w.odometerCurrent.toLocaleString()} км</div>
</div>
<div class="info-item">
<div class="info-label">Планирует проехать</div>
<div class="info-value">${w.odometerPlanned.toLocaleString()} км</div>
</div>
<div class="info-item">
<div class="info-label">Разница (после смены)</div>
<div class="info-value">${(w.odometerCurrent + w.odometerPlanned).toLocaleString()} км</div>
</div>
${w.approvedAt ? `<div class="info-item"><div class="info-label">Подтверждён</div><div class="info-value">${approvedStr}</div></div>` : ''}
</div>
${w.mechanicStatus === 'pending' ? `
<div class="waybill-actions">
<button class="btn btn-success btn-block btn-small" onclick="approveWaybill('${w.id}')">
✅ Подтвердить осмотр
</button>
</div>
` : ''}
</div>
`;
}).join('');
}
async function approveWaybill(id) {
const waybills = getWaybills();
const idx = waybills.findIndex(w => w.id === id);
if (idx === -1) return;
// Update via API
try {
const res = await API.submitMechanicApproval(id);
if (res.success) {
waybills[idx].mechanicStatus = 'approved';
waybills[idx].approvedAt = new Date().toISOString();
saveWaybills(waybills);
// Update installer state if this waybill matches
const s = getInstaller();
if (s.waybillId === id) {
s.mechanicApproved = true;
saveInstaller(s);
}
renderWaybills();
toast('✅ Осмотр подтверждён');
}
} catch(e) {
toast('Ошибка подтверждения');
}
}
// ===== POLLING: check waybill status for installer =====
function pollMechanicStatus() {
const s = getInstaller();
if (s.sentToMechanic && !s.mechanicApproved && s.waybillId) {
const waybills = getWaybills();
const found = waybills.find(w => w.id === s.waybillId);
if (found && found.mechanicStatus === 'approved') {
s.mechanicApproved = true;
saveInstaller(s);
renderInstaller();
toast('🔧 Механик подтвердил осмотр!');
}
}
}
// ===== INIT =====
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('odo-current').addEventListener('input', onInputChange);
document.getElementById('odo-plan').addEventListener('input', onInputChange);
switchRole('installer');
setInterval(pollMechanicStatus, 3000);
});
</script>
</body>
</html>