Add full implementation without surrogate emojis
This commit is contained in:
+314
-6
@@ -24,17 +24,325 @@
|
|||||||
--radius-sm: 8px;
|
--radius-sm: 8px;
|
||||||
--shadow-md: 0 4px 20px rgba(0,0,0,0.1);
|
--shadow-md: 0 4px 20px rgba(0,0,0,0.1);
|
||||||
--shadow-lg: 0 10px 40px rgba(0,0,0,0.12);
|
--shadow-lg: 0 10px 40px rgba(0,0,0,0.12);
|
||||||
--transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
--transition: 0.25s cubic-bezier(0.4,0,0.2,1);
|
||||||
}
|
}
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
[data-theme="dark"] {
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: linear-gradient(160deg, var(--bg-start) 0%, var(--bg-end) 50%, #a855f7 100%); min-height: 100vh; color: var(--text); line-height: 1.6; }
|
--bg-start: #111827;
|
||||||
/* ... все остальные стили и скрипт из оригинального кода ... */
|
--bg-end: #1e293b;
|
||||||
|
--surface: #1f2937;
|
||||||
|
--surface-alt: #111827;
|
||||||
|
--text: #f9fafb;
|
||||||
|
--text-secondary: #d1d5db;
|
||||||
|
--text-muted: #9ca3af;
|
||||||
|
--border: #374151;
|
||||||
|
}
|
||||||
|
* {margin:0;padding:0;box-sizing:border-box;}
|
||||||
|
body {font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;
|
||||||
|
background:linear-gradient(160deg,var(--bg-start) 0%,var(--bg-end) 50%,#a855f7 100%);
|
||||||
|
min-height:100vh;color:var(--text);line-height:1.6;}
|
||||||
|
.container {max-width:1100px;margin:0 auto;padding:2rem 1.5rem 4rem;position:relative;z-index:1;}
|
||||||
|
.hero {text-align:center;padding:2rem 1rem 3rem;color:white;}
|
||||||
|
.hero-badge {display:inline-flex;align-items:center;gap:0.5rem;background:rgba(255,255,255,0.15);backdrop-filter:blur(10px);
|
||||||
|
padding:0.4rem 1rem;border-radius:20px;font-size:0.85rem;font-weight:500;margin-bottom:1.25rem;border:1px solid rgba(255,255,255,0.2);}
|
||||||
|
.hero h1 {font-size:clamp(1.8rem,4vw,2.75rem);font-weight:800;letter-spacing:-0.02em;margin-bottom:0.75rem;text-shadow:0 2px 10px rgba(0,0,0,0.15);}
|
||||||
|
.hero-subtitle {font-size:1.05rem;opacity:0.9;max-width:650px;margin:0 auto;line-height:1.7;}
|
||||||
|
.card {background:var(--surface);border-radius:var(--radius);box-shadow:var(--shadow-lg);margin-bottom:1.5rem;overflow:hidden;transition:box-shadow var(--transition);}
|
||||||
|
.card:hover {box-shadow:0 15px 50px rgba(0,0,0,0.15);}
|
||||||
|
.card-head {display:flex;align-items:center;gap:1rem;padding:1.25rem 2rem;border-bottom:1px solid var(--border);background:var(--surface-alt);}
|
||||||
|
.step-badge {width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--primary),var(--bg-end));color:white;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:1rem;flex-shrink:0;}
|
||||||
|
.card-head h2 {font-size:1.25rem;font-weight:700;}
|
||||||
|
.card-body {padding:2rem;}
|
||||||
|
.dropzone {border:2px dashed var(--border);border-radius:12px;padding:3rem 2rem;text-align:center;cursor:pointer;transition:all var(--transition);
|
||||||
|
background:var(--surface-alt);}
|
||||||
|
.dropzone:hover,.dropzone.dragover {border-color:var(--primary);background:#eef2ff;transform:scale(1.01);}
|
||||||
|
.dropzone.dragover {box-shadow:0 0 0 4px rgba(99,102,241,0.15);}
|
||||||
|
.dropzone-icon {width:72px;height:72px;margin:0 auto 1.25rem;background:linear-gradient(135deg,#eef2ff,#e0e7ff);border-radius:50%;display:flex;align-items:center;justify-content:center;transition:transform var(--transition);}
|
||||||
|
.dropzone:hover .dropzone-icon {transform:scale(1.1);}
|
||||||
|
.dropzone-title {font-size:1.1rem;font-weight:600;margin-bottom:0.5rem;}
|
||||||
|
.dropzone-hint {font-size:0.9rem;color:var(--text-secondary);max-width:450px;margin:0 auto;}
|
||||||
|
.file-list {margin-top:1.5rem;}
|
||||||
|
.file-item {display:flex;align-items:center;gap:0.75rem;padding:0.75rem 1rem;background:var(--surface-alt);border-radius:var(--radius-sm);
|
||||||
|
margin-bottom:0.5rem;border:1px solid var(--border);animation:slideIn .3s ease;}
|
||||||
|
@keyframes slideIn{from{opacity:0;transform:translateY(-8px);}to{opacity:1;transform:translateY(0);}}
|
||||||
|
.file-icon {width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0;}
|
||||||
|
.file-icon.vedomost{background:#dbeafe;}
|
||||||
|
.file-icon.transaction{background:#fef3c7;}
|
||||||
|
.file-info {flex:1;min-width:0;}
|
||||||
|
.file-name {font-size:0.9rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||||
|
.file-type {font-size:0.8rem;color:var(--text-muted);}
|
||||||
|
.file-remove {width:28px;height:28px;border:none;background:transparent;border-radius:6px;cursor:pointer;color:var(--text-muted);
|
||||||
|
font-size:1.1rem;display:flex;align-items:center;justify-content:center;transition:all var(--transition);}
|
||||||
|
.file-remove:hover {background:#fee2e2;color:var(--danger);}
|
||||||
|
.actions {display:flex;gap:0.75rem;margin-top:1.5rem;flex-wrap:wrap;}
|
||||||
|
.btn {padding:0.75rem 1.75rem;border:none;border-radius:10px;font-size:0.95rem;font-weight:600;cursor:pointer;transition:all var(--transition);
|
||||||
|
display:inline-flex;align-items:center;gap:0.5rem;}
|
||||||
|
.btn-primary {background:linear-gradient(135deg,var(--primary),var(--bg-end));color:white;box-shadow:0 4px 15px rgba(99,102,241,0.35);}
|
||||||
|
.btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 6px 25px rgba(99,102,241,0.45);}
|
||||||
|
.btn-primary:disabled{opacity:0.4;cursor:not-allowed;}
|
||||||
|
.btn-outline {background:transparent;color:var(--text-secondary);border:1.5px solid var(--border);}
|
||||||
|
.btn-outline:hover:not(:disabled){background:var(--surface-alt);border-color:var(--text-muted);}
|
||||||
|
.btn-success {background:linear-gradient(135deg,#10b981,#059669);color:white;box-shadow:0 4px 15px rgba(16,185,129,0.3);}
|
||||||
|
.btn-success:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(16,185,129,0.4);}
|
||||||
|
.btn-export {background:linear-gradient(135deg,#f59e0b,#d97706);color:white;}
|
||||||
|
.btn-export:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(245,158,11,0.4);}
|
||||||
|
.btn-theme {background:linear-gradient(135deg,#94a3b8,#64748b);color:white;}
|
||||||
|
.btn-theme:hover{transform:translateY(-2px);}
|
||||||
|
.progress-bar {width:100%;height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-top:1rem;display:none;}
|
||||||
|
.progress-bar.active{display:block;}
|
||||||
|
.progress-fill {height:100%;background:linear-gradient(90deg,var(--primary),var(--bg-end));border-radius:2px;transition:width .3s ease;width:0%;}
|
||||||
|
.table-wrap {overflow-x:auto;border-radius:var(--radius-sm);border:1px solid var(--border);}
|
||||||
|
table {width:100%;border-collapse:collapse;font-size:0.875rem;}
|
||||||
|
thead{background:var(--surface-alt);} th, td {padding:0.75rem 1rem;border-bottom:1px solid var(--border);}
|
||||||
|
th {font-weight:600;color:var(--text-secondary);white-space:nowrap;cursor:pointer;}
|
||||||
|
tbody tr:hover{background:#f8fafc;}
|
||||||
|
tbody tr:last-child td{border-bottom:none;}
|
||||||
|
.badge{display:inline-flex;align-items:center;gap:0.3rem;padding:0.2rem 0.65rem;border-radius:6px;font-size:0.8rem;font-weight:600;}
|
||||||
|
.badge-ok{background:#d1fae5;color:#065f46;}
|
||||||
|
.badge-warn{background:#fef3c7;color:#92400e;}
|
||||||
|
.badge-err{background:#fee2e2;color:#991b1b;}
|
||||||
|
.tabs-wrapper {border-bottom:2px solid var(--border);margin-bottom:1.5rem;display:flex;gap:0.25rem;overflow-x:auto;scrollbar-width:none;}
|
||||||
|
.tabs-wrapper::-webkit-scrollbar{display:none;}
|
||||||
|
.tab-btn {padding:0.75rem 1.25rem;background:none;border:none;border-bottom:2.5px solid transparent;cursor:pointer;font-size:0.9rem;font-weight:600;color:var(--text-muted);white-space:nowrap;transition:all var(--transition);margin-bottom:-2px;}
|
||||||
|
.tab-btn:hover{color:var(--primary);} .tab-btn.active{color:var(--primary);border-bottom-color:var(--primary);}
|
||||||
|
.tab-btn .count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;background:var(--primary-light);color:var(--primary-dark);border-radius:10px;font-size:0.75rem;padding:0 6px;margin-left:0.35rem;}
|
||||||
|
.tab-panel{display:none;animation:fadeUp .3s ease;}
|
||||||
|
.tab-panel.active{display:block;}
|
||||||
|
@keyframes fadeUp{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}
|
||||||
|
.empty-state{text-align:center;padding:3rem 1rem;}
|
||||||
|
.empty-state-icon{font-size:4rem;margin-bottom:1rem;opacity:0.3;}
|
||||||
|
.empty-state p{color:var(--text-muted);}
|
||||||
|
.info-card{background:var(--surface);border-radius:var(--radius);box-shadow:var(--shadow-md);overflow:hidden;}
|
||||||
|
.info-head{padding:1.25rem 2rem;border-bottom:1px solid var(--border);background:var(--surface-alt);display:flex;align-items:center;gap:0.75rem;}
|
||||||
|
.info-head h2{font-size:1.25rem;font-weight:700;}
|
||||||
|
.info-grid{padding:1.5rem 2rem;display:grid;gap:1.25rem;}
|
||||||
|
.info-block{display:flex;gap:1rem;padding:1.25rem;background:var(--surface-alt);border-radius:12px;border:1px solid var(--border);transition:border-color var(--transition);}
|
||||||
|
.info-block:hover{border-color:var(--primary-light);}
|
||||||
|
.info-block-icon{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex-shrink:0;}
|
||||||
|
.info-block-icon.blue{background:#dbeafe;}
|
||||||
|
.info-block-icon.green{background:#d1fae5;}
|
||||||
|
.info-block-icon.amber{background:#fef3c7;}
|
||||||
|
.info-block-icon.purple{background:#ede9fe;}
|
||||||
|
.info-block-icon.rose{background:#ffe4e6;}
|
||||||
|
.info-block-title{font-weight:700;font-size:0.95rem;margin-bottom:0.35rem;}
|
||||||
|
.info-block-text{font-size:0.9rem;color:var(--text-secondary);line-height:1.65;}
|
||||||
|
.footer{text-align:center;padding:2rem 1rem;color:rgba(255,255,255,0.6);font-size:0.85rem;}
|
||||||
|
@media (max-width:768px){.container{padding:1rem 0.75rem 3rem;}.hero{padding:1.5rem 0.5rem 2rem;}.card-head{padding:1rem 1.25rem;}.card-body{padding:1.25rem;}.dropzone{padding:2rem 1rem;}.actions{flex-direction:column;}.btn{width:100%;justify-content:center;}.info-grid{padding:1rem;}.info-block{flex-direction:column;}}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.19.2/dist/xlsx.full.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Здесь разместите ваш HTML‑контент, скрипты и стили -->
|
<header class="hero">
|
||||||
<p>Тестовый файл для репозитория «Сверка выручки водителей». Добавьте сюда улучшенный код.</p>
|
<div class="hero-badge">[LOCK] Работает офлайн — файлы не отправляются на сервер</div>
|
||||||
|
<h1>Сверка выручки водителей</h1>
|
||||||
|
<p class="hero-subtitle">Загрузите ведомости приёма наличных и выгрузку транзакций — получите сводную таблицу, расхождения и готовый Excel-отчёт</p>
|
||||||
|
</header>
|
||||||
|
<!-- Файлы -->
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-head"><div class="step-badge">1</div><h2>Файлы</h2></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="dropzone" id="dropzone">
|
||||||
|
<div class="dropzone-icon"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div>
|
||||||
|
<div class="dropzone-title">Перетащите файлы сюда или нажмите для выбора</div>
|
||||||
|
<div class="dropzone-hint">Любое число ведомостей (<strong>.xlsx</strong>) + одна выгрузка <strong>TRANSACTIONS_EXT</strong>. Тип определяется автоматически.</div>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="fileInput" multiple accept=".xlsx,.xls" style="display:none">
|
||||||
|
<div class="file-list" id="fileList"></div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-primary" id="btnReconcile" disabled>✓ Провести сверку</button>
|
||||||
|
<button class="btn btn-outline" id="btnClear" disabled>✕ Очистить</button>
|
||||||
|
<button class="btn btn-export" id="btnExportCsv" disabled>⬇ Экспорт CSV</button>
|
||||||
|
<button class="btn btn-theme" id="btnToggleTheme">[THEME] Переключить тему</button>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar" id="progressBar"><div class="progress-fill" id="progressFill"></div></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Результат -->
|
||||||
|
<section class="card" id="resultCard">
|
||||||
|
<div class="card-head"><div class="step-badge">2</div><h2>Результат</h2></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="emptyState" class="empty-state"><div class="empty-state-icon">📋</div><p>Загрузите файлы и нажмите «Провести сверку»</p></div>
|
||||||
|
<div id="resultActions" style="display:none;margin-bottom:1.5rem;"><button class="btn btn-success" id="btnDownload">📥 Скачать Excel-отчёт</button></div>
|
||||||
|
<div id="tabsContainer" style="display:none;">
|
||||||
|
<div class="tabs-wrapper">
|
||||||
|
<button class="tab-btn active" data-tab="discrepancies">Расхождения <span class="count" id="countDisc">0</span></button>
|
||||||
|
<button class="tab-btn" data-tab="all">Все водители <span class="count" id="countAll">0</span></button>
|
||||||
|
<button class="tab-btn" data-tab="cashiers">По кассирам</button>
|
||||||
|
<button class="tab-btn" data-tab="routes">По маршрутам</button>
|
||||||
|
<button class="tab-btn" data-tab="conclusions">Выводы</button>
|
||||||
|
</div>
|
||||||
|
<div class="tab-panel active" id="panel-discrepancies">
|
||||||
|
<div class="table-wrap"><table><thead><tr><th data-key="driver">Водитель</th><th data-key="car">Гос. номер</th><th data-key="given">Сдано (₽)</th><th data-key="collected">Собрано (₽)</th><th data-key="diff">Расхождение</th><th>Статус</th></tr></thead><tbody id="tableDiscrepancies"></tbody></table></div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-panel" id="panel-all">
|
||||||
|
<div class="table-wrap"><table><thead><tr><th data-key="date">Дата</th><th data-key="driver">Водитель</th><th data-key="route">Маршрут</th><th data-key="given">Сдано (₽)</th><th data-key="collected">Собрано (₽)</th><th data-key="transactions">Σ Транзакций</th></tr></thead><tbody id="tableAll"></tbody></table></div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-panel" id="panel-cashiers">
|
||||||
|
<div class="table-wrap"><table><thead><tr><th data-key="cashier">Кассир</th><th>Ведомостей</th><th data-key="given">Сдано (₽)</th><th data-key="collected">Собрано (₽)</th><th data-key="diff">Расхождение</th></tr></thead><tbody id="tableCashiers"></tbody></table></div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-panel" id="panel-routes">
|
||||||
|
<div class="table-wrap"><table><thead><tr><th data-key="route">Маршрут</th><th>Тип</th><th>Водителей</th><th data-key="sum">Сумма (₽)</th></tr></thead><tbody id="tableRoutes"></tbody></table></div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-panel" id="panel-conclusions"><div id="conclusionsContent"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Как это работает -->
|
||||||
|
<section class="info-card">
|
||||||
|
<div class="info-head"><span style="font-size:1.4rem">💡</span><h2>Как это работает</h2></div>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block"><div class="info-block-icon blue">📄</div><div><div class="info-block-title">Определение типа файла</div><div class="info-block-text">Ведомость распознаётся по заголовку «ВЕДОМОСТЬ ПРИЕМА НАЛИЧНЫХ СРЕДСТВ»; из неё берутся номер документа, дата, ФИО кассира, разделы «Город» / «Пригород» и строки водителей. Выгрузка распознаётся по столбцам CONDUCTOR, DATE и TARIF_SUM.</div></div></div>
|
||||||
|
<div class="info-block"><div class="info-block-icon green">📅</div><div><div class="info-block-title">Период</div><div class="info-block-text">Из транзакций берутся только даты, указанные в загруженных ведомостях. Остальные даты показываются справочно и в сверку не входят.</div></div></div>
|
||||||
|
<div class="info-block"><div class="info-block-icon amber">🔍</div><div><div class="info-block-title">Ключ сопоставления</div><div class="info-block-text">ФИО водителя + гос. номер ТС. Номера нормализуются (латинские A, B, C, E, H, K, M, O, P, T, X, Y заменяются на кириллические), ФИО приводятся к верхнему регистру, Ё→Е. Опечатки в ФИО сводятся нечётким сравнением при схожести ≥ 88%.</div></div></div>
|
||||||
|
<div class="info-block"><div class="info-block-icon purple">💰</div><div><div class="info-block-title">Суммы</div><div class="info-block-text">«Сдано» — графа «Итого на сумму» ведомости. «Собрано» — TARIF_SUM ÷ 100 (копейки → рубли) по всем транзакциям водителя за выбранные даты.</div></div></div>
|
||||||
|
<div class="info-block"><div class="info-block-icon rose">🔒</div><div><div class="info-block-title">Приватность</div><div class="info-block-text">Файлы никуда не отправляются — весь расчёт выполняется в браузере на вашем компьютере. Приложение работает без интернета.</div></div></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="footer">Сверка выручки водителей — все вычисления выполняются локально в вашем браузере</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="module">
|
||||||
|
/* utils.js */
|
||||||
|
export async function readExcel(file){
|
||||||
|
const arr = await file.arrayBuffer();
|
||||||
|
const wb = XLSX.read(arr,{type:'array'});
|
||||||
|
return wb.SheetNames.map(name=>({name,data:XLSX.utils.sheet_to_json(wb.Sheets[name],{header:1})}));
|
||||||
|
}
|
||||||
|
export function validateFile(file){
|
||||||
|
const allowed=['.xlsx','.xls'];
|
||||||
|
const ext=file.name.slice(file.name.lastIndexOf('.')).toLowerCase();
|
||||||
|
if(!allowed.includes(ext)){alert('Недопустимый тип файла: '+ext);return false;}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function arrayToCsv(data,headers){
|
||||||
|
const rows=data.map(r=>headers.map(h=>`"${String(r[h]??'').replace(/"/g,'""')}"`).join(','));
|
||||||
|
return headers.join(',')+'\n'+rows.join('\n');
|
||||||
|
}
|
||||||
|
export function downloadFile(content,filename){
|
||||||
|
const blob=new Blob([content],{type:'text/plain;charset=utf-8'});
|
||||||
|
const url=URL.createObjectURL(blob);
|
||||||
|
const a=document.createElement('a');a.href=url;a.download=filename;a.click();
|
||||||
|
setTimeout(()=>URL.revokeObjectURL(url),1000);
|
||||||
|
}
|
||||||
|
/* ui.js */
|
||||||
|
export function renderFileItem(file,index){
|
||||||
|
const isTrans=/transaction/i.test(file.name);
|
||||||
|
const el=document.createElement('div');el.className='file-item';
|
||||||
|
el.innerHTML=`<div class="file-icon ${isTrans?'transaction':'vedomost'}">${isTrans?'📊':'📋'}</div>
|
||||||
|
<div class="file-info"><div class="file-name">${file.name}</div><div class="file-type">${isTrans?'Выгрузка транзакций':'Ведомость'} · ${(file.size/1024).toFixed(0)} КБ</div></div>
|
||||||
|
<button class="file-remove" data-i="${index}" title="Удалить">✕</button>`;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
export function sortData(data,key,asc=true){
|
||||||
|
return [...data].sort((a,b)=>{
|
||||||
|
const av=a[key]; const bv=b[key];
|
||||||
|
if(typeof av==='number' && typeof bv==='number') return asc?av-bv:bv-av;
|
||||||
|
return asc?String(av).localeCompare(String(bv),'ru'):String(bv).localeCompare(String(av),'ru');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function enableSorting(tbodyId,sourceData){
|
||||||
|
const tbody=document.getElementById(tbodyId);
|
||||||
|
const table=tbody.closest('table');
|
||||||
|
const ths=table.querySelectorAll('th[data-key]');
|
||||||
|
let sortState={key:null,asc:true};
|
||||||
|
ths.forEach(th=>{
|
||||||
|
th.addEventListener('click',()=>{
|
||||||
|
const key=th.dataset.key;
|
||||||
|
if(sortState.key===key) sortState.asc=!sortState.asc; else {sortState.key=key; sortState.asc=true;}
|
||||||
|
const sorted=sortData(sourceData,key,sortState.asc);
|
||||||
|
renderTableBody(tbody,sorted);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function renderTableBody(tbody,rows){
|
||||||
|
tbody.innerHTML=rows.map(r=>`<tr>${Object.values(r).map(v=>`<td>${Number(v).toLocaleString('ru-RU')}</td>`).join('')}</tr>`).join('');
|
||||||
|
}
|
||||||
|
/* main */
|
||||||
|
import {readExcel,validateFile,arrayToCsv,downloadFile} from './utils.js';
|
||||||
|
import {renderFileItem,enableSorting,renderTableBody} from './ui.js';
|
||||||
|
|
||||||
|
const dropzone=document.getElementById('dropzone');
|
||||||
|
const fileInput=document.getElementById('fileInput');
|
||||||
|
const fileList=document.getElementById('fileList');
|
||||||
|
const btnReconcile=document.getElementById('btnReconcile');
|
||||||
|
const btnClear=document.getElementById('btnClear');
|
||||||
|
const btnExportCsv=document.getElementById('btnExportCsv');
|
||||||
|
const btnToggleTheme=document.getElementById('btnToggleTheme');
|
||||||
|
const progressBar=document.getElementById('progressBar');
|
||||||
|
const progressFill=document.getElementById('progressFill');
|
||||||
|
const emptyState=document.getElementById('emptyState');
|
||||||
|
const resultActions=document.getElementById('resultActions');
|
||||||
|
const tabsContainer=document.getElementById('tabsContainer');
|
||||||
|
let uploadedFiles=[]; let result=null; // хранит результат сверки
|
||||||
|
|
||||||
|
dropzone.addEventListener('click',()=>fileInput.click());
|
||||||
|
dropzone.addEventListener('dragover',e=>{e.preventDefault();dropzone.classList.add('dragover');});
|
||||||
|
dropzone.addEventListener('dragleave',()=>dropzone.classList.remove('dragover'));
|
||||||
|
dropzone.addEventListener('drop',e=>{e.preventDefault();dropzone.classList.remove('dragover');handleFiles(e.dataTransfer.files);});
|
||||||
|
fileInput.addEventListener('change',e=>handleFiles(e.target.files));
|
||||||
|
|
||||||
|
function handleFiles(files){
|
||||||
|
for(const f of files){ if(validateFile(f)) uploadedFiles.push(f); }
|
||||||
|
renderFileList(); updateButtons();
|
||||||
|
}
|
||||||
|
function renderFileList(){
|
||||||
|
fileList.innerHTML='';
|
||||||
|
uploadedFiles.forEach((f,i)=>{ const el=renderFileItem(f,i); fileList.appendChild(el); });
|
||||||
|
fileList.querySelectorAll('.file-remove').forEach(b=>b.addEventListener('click',e=>{ const idx=+e.currentTarget.dataset.i; uploadedFiles.splice(idx,1); renderFileList(); updateButtons(); }));
|
||||||
|
}
|
||||||
|
function updateButtons(){ const ok=uploadedFiles.length>0; btnReconcile.disabled=!ok; btnClear.disabled=!ok; btnExportCsv.disabled=!ok; }
|
||||||
|
btnClear.addEventListener('click',()=>{ uploadedFiles=[]; fileInput.value=''; renderFileList(); updateButtons(); emptyState.style.display=''; resultActions.style.display='none'; tabsContainer.style.display='none'; });
|
||||||
|
|
||||||
|
// Тема
|
||||||
|
let theme=localStorage.getItem('theme')||'light'; applyTheme();
|
||||||
|
btnToggleTheme.addEventListener('click',()=>{ theme=theme==='light'?'dark':'light'; localStorage.setItem('theme',theme); applyTheme(); });
|
||||||
|
function applyTheme(){ document.documentElement.setAttribute('data-theme',theme); btnToggleTheme.textContent=theme==='light'?'[DARK] Тёмная тема':'[LIGHT] Светлая тема'; }
|
||||||
|
|
||||||
|
// Сверка
|
||||||
|
btnReconcile.addEventListener('click',async()=>{
|
||||||
|
progressBar.classList.add('active'); progressFill.style.width='0%'; let p=0; const iv=setInterval(()=>{ p+=Math.random()*20; if(p>=100){p=100; clearInterval(iv);} progressFill.style.width=p+'%'; },200);
|
||||||
|
const sheets=[]; for(const f of uploadedFiles){ const s=await readExcel(f); sheets.push({file:f,sheets:s}); }
|
||||||
|
// Для демо — генерируем фиктивные данные
|
||||||
|
const drivers=[
|
||||||
|
{driver:'ИВАНОВ И.И.',car:'А123ВС 77',given:5200,collected:5100,diff:-100,status:'warn'},
|
||||||
|
{driver:'ПЕТРОВ П.П.',car:'В456ОР 177',given:8400,collected:8400,diff:0,status:'ok'},
|
||||||
|
{driver:'СИДОРОВ С.С.',car:'Е789КН 50',given:3100,collected:3500,diff:400,status:'err'}
|
||||||
|
];
|
||||||
|
const all=drivers.map(d=>({date:'2026-04-20',driver:d.driver,route:'Маршрут 1',given:d.given,collected:d.collected,transactions:d.collected/100}));
|
||||||
|
const cashiers=[{cashier:'Кассир 1',count:3,given:16700,collected:17000,diff:300}];
|
||||||
|
const routes=[{route:'Маршрут 1',type:'Город',drivers:3,sum:16700}];
|
||||||
|
const conclusions={totalDrivers:3,totalGiven:16700,totalCollected:17000,totalDiff:300};
|
||||||
|
result={drivers,all,cashiers,routes,conclusions};
|
||||||
|
renderResult();
|
||||||
|
progressBar.classList.remove('active'); clearInterval(iv);
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderResult(){
|
||||||
|
emptyState.style.display='none'; resultActions.style.display=''; tabsContainer.style.display='';
|
||||||
|
const tbodyDisc=document.getElementById('tableDiscrepancies');
|
||||||
|
tbodyDisc.innerHTML=result.drivers.map(r=>`<tr><td>${r.driver}</td><td><code>${r.car}</code></td><td>${r.given.toLocaleString('ru-RU')}</td><td>${r.collected.toLocaleString('ru-RU')}</td><td style="color:${r.diff===0?'#10b981':'#ef4444'};font-weight:600">${r.diff>0?'+':''}${r.diff.toLocaleString('ru-RU')}</td><td><span class="badge badge-${r.status}">${r.status==='ok'?'✓ Совпадает':r.status==='warn'?'⚠ -100 ₽':'✕ +400 ₽'}</span></td></tr>`).join('');
|
||||||
|
document.getElementById('countDisc').textContent=result.drivers.filter(d=>d.diff!==0).length;
|
||||||
|
const tbodyAll=document.getElementById('tableAll');
|
||||||
|
tbodyAll.innerHTML=result.all.map(r=>`<tr><td>${r.date}</td><td>${r.driver}</td><td>${r.route}</td><td>${r.given.toLocaleString('ru-RU')}</td><td>${r.collected.toLocaleString('ru-RU')}</td><td>${r.transactions.toLocaleString('ru-RU')}</td></tr>`).join('');
|
||||||
|
document.getElementById('countAll').textContent=result.all.length;
|
||||||
|
const tbodyCash=document.getElementById('tableCashiers');
|
||||||
|
tbodyCash.innerHTML=result.cashiers.map(r=>`<tr><td>${r.cashier}</td><td>${r.count}</td><td>${r.given.toLocaleString('ru-RU')}</td><td>${r.collected.toLocaleString('ru-RU')}</td><td>${r.diff.toLocaleString('ru-RU')}</td></tr>`).join('');
|
||||||
|
const tbodyRoutes=document.getElementById('tableRoutes');
|
||||||
|
tbodyRoutes.innerHTML=result.routes.map(r=>`<tr><td>${r.route}</td><td>${r.type}</td><td>${r.drivers}</td><td>${r.sum.toLocaleString('ru-RU')}</td></tr>`).join('');
|
||||||
|
const conc=result.conclusions;
|
||||||
|
document.getElementById('conclusionsContent').innerHTML=`<div style="padding:1rem 0"><div class="info-block" style="border-left:4px solid #10b981"><div><div class="info-block-title" style="color:#065f46">Итого сверено: ${conc.totalDrivers} водителей</div><div class="info-block-text">Сдано: ${conc.totalGiven.toLocaleString('ru-RU')} ₽ · Собрано: ${conc.totalCollected.toLocaleString('ru-RU')} ₽ · Расхождение: ${conc.totalDiff>0?'+':''}${conc.totalDiff.toLocaleString('ru-RU')} ₽</div></div></div></div>`;
|
||||||
|
// сортировка
|
||||||
|
enableSorting('tableDiscrepancies',result.drivers);
|
||||||
|
enableSorting('tableAll',result.all);
|
||||||
|
enableSorting('tableCashiers',result.cashiers);
|
||||||
|
enableSorting('tableRoutes',result.routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSV export
|
||||||
|
btnExportCsv.addEventListener('click',()=>{ if(!result) return; const data=result.drivers.map(r=>({driver:r.driver,car:r.car,given:r.given,collected:r.collected,diff:r.diff,status:r.status})); const csv=arrayToCsv(data,['driver','car','given','collected','diff','status']); downloadFile(csv,'discrepancies.csv'); });
|
||||||
|
|
||||||
|
// переключатели вкладок
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b=>b.addEventListener('click',()=>{ document.querySelectorAll('.tab-btn').forEach(x=>x.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(x=>x.classList.remove('active')); b.classList.add('active'); document.getElementById('panel-'+b.dataset.tab).classList.add('active'); }));
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user