Загрузка данных
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Учёт ремонта принтеров и МФУ</title>
<style>
:root{--bg:#f4f7fb;--card:#fff;--text:#172033;--muted:#64748b;--primary:#2563eb;--danger:#dc2626;--border:#dbe3ef;--shadow:0 8px 24px #17203312}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}header{background:#102a56;color:white;padding:18px 24px;display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap}h1{font-size:22px;margin:0}h2{font-size:18px;margin:0 0 14px}main{max-width:1500px;margin:auto;padding:20px}.tabs{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}.tab,button{border:1px solid var(--border);background:white;border-radius:8px;padding:9px 13px;cursor:pointer;font:inherit}.tab.active,button.primary{background:var(--primary);color:white;border-color:var(--primary)}button.danger{color:var(--danger)}button.small{padding:5px 8px;font-size:12px}.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px;box-shadow:var(--shadow);margin-bottom:16px}.grid{display:grid;grid-template-columns:repeat(6,minmax(120px,1fr));gap:12px}.field{display:flex;flex-direction:column;gap:5px}.field.wide{grid-column:span 2}.field.full{grid-column:1/-1}label{font-weight:600}input,select,textarea{width:100%;border:1px solid #cbd5e1;border-radius:7px;padding:9px;background:white;color:var(--text);font:inherit}textarea{min-height:70px;resize:vertical}.actions{display:flex;gap:8px;align-items:end;flex-wrap:wrap}.stats{display:grid;grid-template-columns:repeat(8,1fr);gap:10px}.stat{background:#eef4ff;border-radius:10px;padding:12px}.stat b{display:block;font-size:20px;color:#123d85}.stat span{color:var(--muted);font-size:12px}.toolbar{display:flex;gap:10px;flex-wrap:wrap;align-items:end}.toolbar .field{min-width:150px}.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse;min-width:1180px}th,td{border-bottom:1px solid var(--border);padding:9px 8px;text-align:left;vertical-align:top}th{background:#f8fafc;white-space:nowrap}td.num,th.num{text-align:right}.money{white-space:nowrap;font-weight:700}.muted{color:var(--muted)}.hidden{display:none!important}.notice{padding:10px;border-radius:8px;background:#eff6ff;color:#1e40af;margin-bottom:12px}.form-total{font-size:18px;font-weight:700;color:#123d85}.footer-note{color:var(--muted);font-size:12px;margin-top:8px}@media(max-width:1000px){.grid{grid-template-columns:repeat(3,1fr)}.stats{grid-template-columns:repeat(4,1fr)}}@media(max-width:600px){main{padding:12px}.grid{grid-template-columns:1fr}.field.wide{grid-column:auto}.stats{grid-template-columns:repeat(2,1fr)}header{padding:16px}}
</style>
</head>
<body>
<header><h1>Учёт ремонта принтеров и МФУ</h1><div id="clock"></div></header>
<main>
<div class="tabs"><button class="tab active" data-tab="work">Рабочий экран</button><button class="tab" data-tab="settings">Настройки</button><button class="tab" data-tab="backup">Резервная копия</button></div>
<section id="work">
<div class="card"><h2>Общие итоги за выбранный период</h2><div class="stats" id="stats"></div></div>
<div class="card"><h2 id="formTitle">Добавить аппарат</h2><form id="entryForm"><input type="hidden" id="editId"><div class="grid">
<div class="field"><label for="date">Дата</label><input id="date" type="date" required></div>
<div class="field wide"><label for="org">Организация</label><input id="org" required placeholder="Например, ООО Ромашка"></div>
<div class="field"><label for="type">Тип аппарата</label><select id="type"><option>Принтер</option><option>МФУ</option></select></div>
<div class="field wide"><label for="model">Модель</label><input id="model" required placeholder="Производитель и модель"></div>
<div class="field"><label for="packs">Упаковки, шт.</label><input id="packs" type="number" min="0" step="1" value="0"></div>
<div class="field"><label for="hours">Часы работы</label><input id="hours" type="number" min="0" step="0.1" value="0"></div>
<div class="field"><label for="diagnostics">Диагностика, шт.</label><input id="diagnostics" type="number" min="0" step="1" value="0"></div>
<div class="field"><label for="painted">Окрашенные элементы</label><input id="painted" type="number" min="0" step="1" value="0"></div>
<div class="field full"><label for="workText">Описание выполненной работы</label><textarea id="workText" required placeholder="Что было сделано"></textarea></div>
<div class="field full"><label for="comment">Комментарий</label><textarea id="comment" placeholder="Дополнительная информация"></textarea></div>
<div class="actions full"><span class="form-total" id="liveTotal">Итого: 0 ₽</span><span style="flex:1"></span><button type="button" id="cancelEdit" class="hidden">Отменить редактирование</button><button class="primary" type="submit">Сохранить запись</button></div>
</div></form></div>
<div class="card"><h2>История</h2><div class="toolbar">
<div class="field" style="flex:1;min-width:240px"><label for="search">Поиск</label><input id="search" placeholder="Организация, модель или работа"></div>
<div class="field"><label for="typeFilter">Тип</label><select id="typeFilter"><option value="">Все типы</option><option>Принтер</option><option>МФУ</option></select></div>
<div class="field"><label for="period">Период</label><select id="period"><option value="all">Всё время</option><option value="today">Сегодня</option><option value="week">Текущая неделя</option><option value="month">Текущий месяц</option><option value="custom">Произвольный</option></select></div>
<div class="field customDate hidden"><label for="from">С</label><input id="from" type="date"></div><div class="field customDate hidden"><label for="to">По</label><input id="to" type="date"></div>
<button id="clearFilters">Сбросить</button></div><div class="table-wrap"><table><thead><tr><th>Дата</th><th>Организация</th><th>Тип</th><th>Модель</th><th>Выполненная работа</th><th class="num">Упаковки</th><th class="num">Часы</th><th class="num">Диагностика</th><th class="num">Покраска</th><th class="num">Итог</th><th>Действия</th></tr></thead><tbody id="tbody"></tbody></table></div><div id="empty" class="muted" style="padding:16px 0">Записей пока нет.</div></div>
</section>
<section id="settings" class="hidden"><div class="card"><h2>Настройки цен</h2><div class="notice">Изменения применяются к новым расчётам и автоматически пересчитывают сохранённые записи.</div><form id="settingsForm"><div class="grid"><div class="field"><label>Принтер, ₽</label><input id="pricePrinter" type="number" min="0" step="0.01"></div><div class="field"><label>МФУ, ₽</label><input id="priceMfu" type="number" min="0" step="0.01"></div><div class="field"><label>Упаковка, ₽/шт.</label><input id="pricePack" type="number" min="0" step="0.01"></div><div class="field"><label>Час работы, ₽</label><input id="priceHour" type="number" min="0" step="0.01"></div><div class="field"><label>Диагностика принтера, ₽/шт.</label><input id="priceDiagPrinter" type="number" min="0" step="0.01"></div><div class="field"><label>Диагностика МФУ, ₽/шт.</label><input id="priceDiagMfu" type="number" min="0" step="0.01"></div><div class="field"><label>Покраска, ₽/элемент</label><input id="pricePaint" type="number" min="0" step="0.01"></div><div class="actions"><button class="primary">Сохранить цены</button><button type="button" id="resetPrices">Вернуть стандартные</button></div></div></form></div></section>
<section id="backup" class="hidden"><div class="card"><h2>Резервная копия и экспорт</h2><p>Резервная копия содержит записи и текущие настройки цен в формате JSON.</p><div class="actions"><button class="primary" id="backupBtn">Скачать резервную копию</button><label class="tab" style="display:inline-block">Восстановить из JSON <input id="restoreFile" type="file" accept="application/json" hidden></label><button id="csvBtn">Экспорт CSV</button><button id="excelBtn">Экспорт Excel</button></div><p class="footer-note">Все данные также автоматически сохраняются в памяти браузера (localStorage) и доступны после его закрытия.</p></div></section>
</main>
<script>
const KEY='printerRepairDB_v1', defaults={printer:350,mfu:456,pack:100,hour:150,diagPrinter:175,diagMfu:228,paint:130};
let state=JSON.parse(localStorage.getItem(KEY)||'null')||{entries:[],prices:{...defaults}}; state.prices={...defaults,...(state.prices||{})};
const $=id=>document.getElementById(id), money=n=>new Intl.NumberFormat('ru-RU',{style:'currency',currency:'RUB',maximumFractionDigits:2}).format(n||0), save=()=>localStorage.setItem(KEY,JSON.stringify(state));
function esc(x){return String(x??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))}
function num(id){return Math.max(0,Number($(id).value)||0)}
function calc(e){let p=state.prices;return (e.type==='Принтер'?p.printer:p.mfu)+e.packs*p.pack+e.hours*p.hour+e.diagnostics*(e.type==='Принтер'?p.diagPrinter:p.diagMfu)+e.painted*p.paint}
function dateToday(){return new Date().toISOString().slice(0,10)}
function getRange(){let mode=$('period').value, now=new Date(), today=dateToday(); if(mode==='today')return [today,today]; if(mode==='week'){let d=new Date(now);let day=(d.getDay()+6)%7;d.setDate(d.getDate()-day);return [d.toISOString().slice(0,10),today]} if(mode==='month'){return [today.slice(0,8)+'01',today]} if(mode==='custom')return [$('from').value||'0000-00-00',$('to').value||'9999-99-99'];return ['0000-00-00','9999-99-99']}
function filtered(){let q=$('search').value.trim().toLowerCase(), type=$('typeFilter').value, [from,to]=getRange();return state.entries.filter(e=>(!q||[e.org,e.model,e.workText].join(' ').toLowerCase().includes(q))&&(!type||e.type===type)&&e.date>=from&&e.date<=to)}
function render(){let rows=filtered();$('tbody').innerHTML=rows.map(e=>`<tr><td>${esc(e.date)}</td><td>${esc(e.org)}</td><td>${esc(e.type)}</td><td>${esc(e.model)}</td><td>${esc(e.workText)}${e.comment?`<br><span class="muted">${esc(e.comment)}</span>`:''}</td><td class="num">${e.packs}</td><td class="num">${e.hours}</td><td class="num">${e.diagnostics}</td><td class="num">${e.painted}</td><td class="num money"