1653 lines
62 KiB
HTML
1653 lines
62 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Учет СИЗ — АО «Казахтелеком»</title>
|
||
<script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"></script>
|
||
<style>
|
||
:root {
|
||
--ink: #0F1218;
|
||
--cyan: #00E5FF;
|
||
--cyan-50: #E8FCFF;
|
||
--white: #FFFFFF;
|
||
--gray-500: #5B6573;
|
||
--gray-100: #F2F4F7;
|
||
--gray-200: #E4E7EC;
|
||
--gray-300: #D0D5DD;
|
||
--gray-700: #344054;
|
||
--red: #D92D20;
|
||
--red-50: #FEF3F2;
|
||
--orange: #F79009;
|
||
--orange-50: #FFFAEB;
|
||
--green: #12B76A;
|
||
--green-50: #ECFDF3;
|
||
--radius: 8px;
|
||
--radius-lg: 12px;
|
||
--shadow: 0 1px 3px rgba(16,24,40,.06), 0 1px 2px rgba(16,24,40,.08);
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font: 15px/1.5 -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;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
.header h1 {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
.header .subtitle {
|
||
font-size: 13px;
|
||
color: var(--gray-500);
|
||
white-space: nowrap;
|
||
}
|
||
.header-right {
|
||
margin-left: auto;
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.header .btn-sm {
|
||
font-size: 13px;
|
||
padding: 6px 14px;
|
||
border-radius: var(--radius);
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
border: none;
|
||
transition: opacity .15s;
|
||
text-decoration: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.header .btn-sm:hover { opacity: .85; }
|
||
|
||
/* Nav tabs */
|
||
.nav {
|
||
background: var(--white);
|
||
border-bottom: 1px solid var(--gray-200);
|
||
display: flex;
|
||
gap: 0;
|
||
overflow-x: auto;
|
||
padding: 0 24px;
|
||
position: sticky;
|
||
top: 54px;
|
||
z-index: 99;
|
||
}
|
||
.nav button {
|
||
background: none;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
padding: 12px 16px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--gray-500);
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: all .15s;
|
||
}
|
||
.nav button:hover { color: var(--ink); }
|
||
.nav button.active {
|
||
color: var(--ink);
|
||
border-bottom-color: var(--cyan);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Main */
|
||
.main {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 24px;
|
||
}
|
||
|
||
/* Panel */
|
||
.panel {
|
||
display: none;
|
||
}
|
||
.panel.active {
|
||
display: block;
|
||
}
|
||
|
||
/* Toolbar */
|
||
.toolbar {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
.toolbar input {
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--gray-300);
|
||
border-radius: var(--radius);
|
||
font-size: 14px;
|
||
flex: 1;
|
||
min-width: 180px;
|
||
outline: none;
|
||
transition: border-color .15s;
|
||
}
|
||
.toolbar input:focus { border-color: var(--cyan); }
|
||
.toolbar select {
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--gray-300);
|
||
border-radius: var(--radius);
|
||
font-size: 14px;
|
||
background: var(--white);
|
||
cursor: pointer;
|
||
outline: none;
|
||
}
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 9px 18px;
|
||
border-radius: var(--radius);
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
border: none;
|
||
transition: opacity .15s;
|
||
white-space: nowrap;
|
||
}
|
||
.btn:hover { opacity: .85; }
|
||
.btn-primary { background: var(--cyan); color: var(--ink); }
|
||
.btn-danger { background: var(--red); color: var(--white); }
|
||
.btn-outline { background: var(--white); color: var(--ink); border: 1px solid var(--gray-300); }
|
||
.btn-success { background: var(--green); color: var(--white); }
|
||
.btn-ink { background: var(--ink); color: var(--white); }
|
||
|
||
/* Tables */
|
||
.table-wrap {
|
||
overflow-x: auto;
|
||
background: var(--white);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
th, td {
|
||
text-align: left;
|
||
padding: 10px 14px;
|
||
font-size: 13px;
|
||
border-bottom: 1px solid var(--gray-100);
|
||
white-space: nowrap;
|
||
}
|
||
th {
|
||
font-weight: 600;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: .3px;
|
||
color: var(--gray-500);
|
||
background: var(--gray-100);
|
||
position: sticky;
|
||
top: 0;
|
||
}
|
||
tr:last-child td { border-bottom: none; }
|
||
tr:hover td { background: var(--cyan-50); }
|
||
|
||
/* Badges */
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: 100px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
.badge-green { background: var(--green-50); color: var(--green); }
|
||
.badge-orange { background: var(--orange-50); color: var(--orange); }
|
||
.badge-red { background: var(--red-50); color: var(--red); }
|
||
.badge-gray { background: var(--gray-100); color: var(--gray-500); }
|
||
|
||
/* Modal */
|
||
.modal-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(15,18,24,.5);
|
||
z-index: 200;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.modal-overlay.show {
|
||
display: flex;
|
||
}
|
||
.modal {
|
||
background: var(--white);
|
||
border-radius: var(--radius-lg);
|
||
padding: 28px;
|
||
width: 90%;
|
||
max-width: 560px;
|
||
max-height: 85vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,.2);
|
||
}
|
||
.modal h2 {
|
||
font-size: 18px;
|
||
margin-bottom: 20px;
|
||
}
|
||
.form-group {
|
||
margin-bottom: 14px;
|
||
}
|
||
.form-group label {
|
||
display: block;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
color: var(--gray-700);
|
||
}
|
||
.form-group input,
|
||
.form-group select,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--gray-300);
|
||
border-radius: var(--radius);
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
outline: none;
|
||
transition: border-color .15s;
|
||
}
|
||
.form-group input:focus,
|
||
.form-group select:focus,
|
||
.form-group textarea:focus {
|
||
border-color: var(--cyan);
|
||
}
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
}
|
||
.modal-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: flex-end;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* Stats cards */
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
.stat-card {
|
||
background: var(--white);
|
||
border-radius: var(--radius-lg);
|
||
padding: 20px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.stat-card .stat-label {
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: .5px;
|
||
color: var(--gray-500);
|
||
font-weight: 600;
|
||
}
|
||
.stat-card .stat-value {
|
||
font-size: 28px;
|
||
font-weight: 800;
|
||
margin-top: 4px;
|
||
line-height: 1.1;
|
||
}
|
||
.stat-card .stat-sub {
|
||
font-size: 12px;
|
||
color: var(--gray-500);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* Alert list */
|
||
.alert-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.alert-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
border-radius: var(--radius);
|
||
font-size: 13px;
|
||
}
|
||
.alert-item.red {
|
||
background: var(--red-50);
|
||
border: 1px solid rgba(217,45,32,.2);
|
||
}
|
||
.alert-item.orange {
|
||
background: var(--orange-50);
|
||
border: 1px solid rgba(247,144,9,.2);
|
||
}
|
||
|
||
/* Empty state */
|
||
.empty {
|
||
text-align: center;
|
||
padding: 48px 24px;
|
||
color: var(--gray-500);
|
||
}
|
||
.empty .empty-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 12px;
|
||
opacity: .4;
|
||
}
|
||
|
||
/* Action icons */
|
||
.action-btn {
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
border-radius: 4px;
|
||
font-size: 16px;
|
||
transition: background .15s;
|
||
line-height: 1;
|
||
}
|
||
.action-btn:hover { background: var(--gray-100); }
|
||
|
||
/* Norm hint in issuance modal */
|
||
.norm-hint {
|
||
margin: 16px 0 8px;
|
||
padding: 12px;
|
||
background: var(--cyan-50);
|
||
border-radius: var(--radius);
|
||
border: 1px solid rgba(0,229,255,.2);
|
||
}
|
||
.norm-hint-label {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
margin-bottom: 8px;
|
||
color: var(--ink);
|
||
}
|
||
.norm-hint-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
padding: 6px 0;
|
||
font-size: 13px;
|
||
border-bottom: 1px solid rgba(0,0,0,.05);
|
||
}
|
||
.norm-hint-item:last-child { border-bottom: none; }
|
||
.norm-hint-item-name { font-weight: 600; }
|
||
.norm-hint-item-wear { color: var(--gray-500); font-size: 12px; white-space: nowrap; }
|
||
.norm-hint-item .btn-xs {
|
||
padding: 3px 10px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
border-radius: 4px;
|
||
border: none;
|
||
cursor: pointer;
|
||
background: var(--cyan);
|
||
color: var(--ink);
|
||
white-space: nowrap;
|
||
}
|
||
.norm-hint-item .btn-xs:hover { opacity: .8; }
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
.header { padding: 12px 16px; }
|
||
.header h1 { font-size: 15px; }
|
||
.header .subtitle { display: none; }
|
||
.nav { padding: 0 8px; top: 46px; }
|
||
.nav button { padding: 10px 10px; font-size: 12px; }
|
||
.main { padding: 16px; }
|
||
.form-row { grid-template-columns: 1fr; }
|
||
.stats { grid-template-columns: 1fr 1fr; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="header">
|
||
<div>
|
||
<h1>🛡️ Учет СИЗ</h1>
|
||
<div class="subtitle">АО «Казахтелеком»</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<button class="btn btn-outline btn-sm" onclick="exportAllToExcel()">📥 Экспорт в Excel</button>
|
||
<button class="btn btn-primary btn-sm" onclick="resetData()">🔄 Сброс данных</button>
|
||
</div>
|
||
</header>
|
||
|
||
<nav class="nav">
|
||
<button class="active" data-panel="tab-employees">👤 Работники</button>
|
||
<button data-panel="tab-siz">🛡️ Справочник СИЗ</button>
|
||
<button data-panel="tab-warehouse">📦 Склад</button>
|
||
<button data-panel="tab-norms">📋 Нормы выдачи</button>
|
||
<button data-panel="tab-issuance">📤 Выдача</button>
|
||
<button data-panel="tab-control">⚠️ Контроль сроков</button>
|
||
<button data-panel="tab-reports">📊 Отчеты</button>
|
||
</nav>
|
||
|
||
<div class="main">
|
||
|
||
<!-- TAB: Работники -->
|
||
<div class="panel active" id="tab-employees">
|
||
<div class="toolbar">
|
||
<input type="text" id="empSearch" placeholder="🔍 Поиск по ФИО, должности, отделу..." oninput="renderEmployees()">
|
||
<button class="btn btn-primary" onclick="openEmployeeModal()">+ Добавить</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Таб. №</th>
|
||
<th>ФИО</th>
|
||
<th>Должность</th>
|
||
<th>Отдел</th>
|
||
<th>Дата приема</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="empTable"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="empty" id="empEmpty" style="display:none;">
|
||
<div class="empty-icon">👤</div>
|
||
<p>Нет работников. Нажмите «Добавить».</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Справочник СИЗ -->
|
||
<div class="panel" id="tab-siz">
|
||
<div class="toolbar">
|
||
<input type="text" id="sizSearch" placeholder="🔍 Поиск по названию, типу..." oninput="renderSiz()">
|
||
<select id="sizFilterType" onchange="renderSiz()">
|
||
<option value="">Все типы</option>
|
||
<option>Головы</option><option>Глаз</option><option>Органов дыхания</option>
|
||
<option>Рук</option><option>Ног</option><option>Спецодежда</option><option>Прочее</option>
|
||
</select>
|
||
<button class="btn btn-primary" onclick="openSizModal()">+ Добавить</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Наименование</th>
|
||
<th>Тип</th>
|
||
<th>Класс защиты</th>
|
||
<th>ГОСТ/ТУ</th>
|
||
<th>Срок носки (мес)</th>
|
||
<th>Ед. изм.</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sizTable"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="empty" id="sizEmpty" style="display:none;">
|
||
<div class="empty-icon">🛡️</div>
|
||
<p>Нет позиций СИЗ. Нажмите «Добавить».</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Склад -->
|
||
<div class="panel" id="tab-warehouse">
|
||
<div class="toolbar">
|
||
<input type="text" id="whSearch" placeholder="🔍 Поиск..." oninput="renderWarehouse()">
|
||
<button class="btn btn-primary" onclick="openWarehouseModal()">+ Приход</button>
|
||
<button class="btn btn-outline" onclick="openWarehouseModal('out')">− Расход (списание)</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>СИЗ</th>
|
||
<th>Кол-во</th>
|
||
<th>Дата</th>
|
||
<th>Операция</th>
|
||
<th>Партия</th>
|
||
<th>Поставщик</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="whTable"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="empty" id="whEmpty" style="display:none;">
|
||
<div class="empty-icon">📦</div>
|
||
<p>Нет складских операций.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Нормы выдачи -->
|
||
<div class="panel" id="tab-norms">
|
||
<div class="toolbar">
|
||
<input type="text" id="normSearch" placeholder="🔍 Поиск по должности..." oninput="renderNorms()">
|
||
<select id="normFilterZone" onchange="renderNorms()">
|
||
<option value="">Все пояса</option>
|
||
<option value="0">Общий</option>
|
||
<option value="1">Пояс 1</option>
|
||
<option value="2">Пояс 2</option>
|
||
<option value="3">Пояс 3</option>
|
||
</select>
|
||
<button class="btn btn-primary" onclick="openNormModal()">+ Добавить норму</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Должность</th>
|
||
<th>Клим. пояс</th>
|
||
<th>СИЗ</th>
|
||
<th>Срок носки</th>
|
||
<th>Кол-во</th>
|
||
<th>Примечание</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="normTable"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="empty" id="normEmpty" style="display:none;">
|
||
<div class="empty-icon">📋</div>
|
||
<p>Нет норм выдачи. Добавьте нормы для должностей.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Выдача СИЗ -->
|
||
<div class="panel" id="tab-issuance">
|
||
<div class="toolbar">
|
||
<input type="text" id="issSearch" placeholder="🔍 Поиск..." oninput="renderIssuances()">
|
||
<select id="issFilterStatus" onchange="renderIssuances()">
|
||
<option value="">Все статусы</option>
|
||
<option value="active">Действует</option>
|
||
<option value="expired">Просрочено</option>
|
||
<option value="returned">Возвращено</option>
|
||
</select>
|
||
<button class="btn btn-primary" onclick="openIssuanceModal()">+ Выдать СИЗ</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Работник</th>
|
||
<th>СИЗ</th>
|
||
<th>Кол-во</th>
|
||
<th>Дата выдачи</th>
|
||
<th>Годен до</th>
|
||
<th>Статус</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="issTable"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="empty" id="issEmpty" style="display:none;">
|
||
<div class="empty-icon">📤</div>
|
||
<p>Нет записей о выдаче СИЗ.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Контроль сроков -->
|
||
<div class="panel" id="tab-control">
|
||
<div class="stats">
|
||
<div class="stat-card">
|
||
<div class="stat-label">Просрочено</div>
|
||
<div class="stat-value" style="color:var(--red);" id="stExpired">0</div>
|
||
<div class="stat-sub">требуют замены</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Истекает (30 дн)</div>
|
||
<div class="stat-value" style="color:var(--orange);" id="stExpiring">0</div>
|
||
<div class="stat-sub">скоро замена</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Действует</div>
|
||
<div class="stat-value" style="color:var(--green);" id="stActive">0</div>
|
||
<div class="stat-sub">в норме</div>
|
||
</div>
|
||
</div>
|
||
<div class="alert-list" id="alertList"></div>
|
||
</div>
|
||
|
||
<!-- TAB: Отчеты -->
|
||
<div class="panel" id="tab-reports">
|
||
<div class="stats">
|
||
<div class="stat-card">
|
||
<div class="stat-label">Работников</div>
|
||
<div class="stat-value" id="repEmployees">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Позиций СИЗ</div>
|
||
<div class="stat-value" id="repSiz">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">На складе</div>
|
||
<div class="stat-value" id="repStock">0</div>
|
||
<div class="stat-sub">единиц</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Выдано</div>
|
||
<div class="stat-value" id="repIssued">0</div>
|
||
<div class="stat-sub">единиц</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Просрочено</div>
|
||
<div class="stat-value" style="color:var(--red);" id="repExpired">0</div>
|
||
</div>
|
||
</div>
|
||
<div class="table-wrap" style="margin-top:20px;">
|
||
<table>
|
||
<thead>
|
||
<tr><th>Работник</th><th>Должность</th><th>Выдано позиций</th><th>Из них просрочено</th></tr>
|
||
</thead>
|
||
<tbody id="repTable"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- EMPLOYEE MODAL -->
|
||
<div class="modal-overlay" id="employeeModal">
|
||
<div class="modal">
|
||
<h2 id="empModalTitle">Добавить работника</h2>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Табельный номер *</label>
|
||
<input type="text" id="empTabNum" placeholder="00001">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Дата приема</label>
|
||
<input type="date" id="empDateHired">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>ФИО *</label>
|
||
<input type="text" id="empName" placeholder="Иванов Иван Иванович">
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Должность</label>
|
||
<input type="text" id="empPosition" placeholder="Инженер">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Отдел</label>
|
||
<input type="text" id="empDepartment" placeholder="Технический отдел">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Климатический пояс</label>
|
||
<select id="empClimateZone">
|
||
<option value="0">Общий</option>
|
||
<option value="1">Пояс 1 (особый)</option>
|
||
<option value="2">Пояс 2 (холодный)</option>
|
||
<option value="3">Пояс 3 (умеренный)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<input type="hidden" id="empEditId">
|
||
<div class="modal-actions">
|
||
<button class="btn btn-outline" onclick="closeModal('employeeModal')">Отмена</button>
|
||
<button class="btn btn-primary" onclick="saveEmployee()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SIZ MODAL -->
|
||
<div class="modal-overlay" id="sizModal">
|
||
<div class="modal">
|
||
<h2 id="sizModalTitle">Добавить СИЗ</h2>
|
||
<div class="form-group">
|
||
<label>Наименование *</label>
|
||
<input type="text" id="sizName" placeholder="Каска защитная">
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Тип</label>
|
||
<select id="sizType">
|
||
<option>Головы</option><option>Глаз</option><option>Органов дыхания</option>
|
||
<option>Рук</option><option>Ног</option><option>Спецодежда</option><option>Прочее</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Класс защиты</label>
|
||
<input type="text" id="sizProtection" placeholder="1 класс">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>ГОСТ / ТУ</label>
|
||
<input type="text" id="sizStandard" placeholder="ГОСТ 12.4.128-83">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Срок носки (месяцев)</label>
|
||
<input type="number" id="sizWearMonths" value="12" min="1">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Единица измерения</label>
|
||
<input type="text" id="sizUnit" value="шт.">
|
||
</div>
|
||
<input type="hidden" id="sizEditId">
|
||
<div class="modal-actions">
|
||
<button class="btn btn-outline" onclick="closeModal('sizModal')">Отмена</button>
|
||
<button class="btn btn-primary" onclick="saveSiz()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WAREHOUSE MODAL -->
|
||
<div class="modal-overlay" id="warehouseModal">
|
||
<div class="modal">
|
||
<h2 id="whModalTitle">Приход на склад</h2>
|
||
<div class="form-group">
|
||
<label>СИЗ *</label>
|
||
<select id="whSizId"></select>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Количество *</label>
|
||
<input type="number" id="whQuantity" value="1" min="1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Дата</label>
|
||
<input type="date" id="whDate">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Номер партии</label>
|
||
<input type="text" id="whBatch" placeholder="Б-2026001">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Поставщик</label>
|
||
<input type="text" id="whSupplier" placeholder="ТОО Поставщик">
|
||
</div>
|
||
</div>
|
||
<input type="hidden" id="whEditId">
|
||
<input type="hidden" id="whOpType" value="in">
|
||
<div class="modal-actions">
|
||
<button class="btn btn-outline" onclick="closeModal('warehouseModal')">Отмена</button>
|
||
<button class="btn btn-primary" onclick="saveWarehouse()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- NORM MODAL -->
|
||
<div class="modal-overlay" id="normModal">
|
||
<div class="modal">
|
||
<h2 id="normModalTitle">Добавить норму выдачи</h2>
|
||
<div class="form-group">
|
||
<label>Должность *</label>
|
||
<input type="text" id="normPosition" list="positionsList" placeholder="Кабельщик-спайщик" autocomplete="off">
|
||
<datalist id="positionsList"></datalist>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Климатический пояс</label>
|
||
<select id="normClimateZone">
|
||
<option value="0">Общий</option>
|
||
<option value="1">Пояс 1</option>
|
||
<option value="2">Пояс 2</option>
|
||
<option value="3">Пояс 3</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>СИЗ *</label>
|
||
<select id="normSizId"></select>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Срок носки</label>
|
||
<input type="text" id="normWearDisplay" placeholder="12 мес = 1 год" oninput="parseNormWear()">
|
||
<input type="hidden" id="normWearMonths" value="12">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Количество</label>
|
||
<input type="number" id="normQuantity" value="1" min="1">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Примечание</label>
|
||
<input type="text" id="normNote" placeholder="Летний / зимний вариант...">
|
||
</div>
|
||
<input type="hidden" id="normEditId">
|
||
<div class="modal-actions">
|
||
<button class="btn btn-outline" onclick="closeModal('normModal')">Отмена</button>
|
||
<button class="btn btn-primary" onclick="saveNorm()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ISSUANCE MODAL -->
|
||
<div class="modal-overlay" id="issuanceModal">
|
||
<div class="modal">
|
||
<h2 id="issModalTitle">Выдача СИЗ</h2>
|
||
<div class="form-group">
|
||
<label>Работник *</label>
|
||
<select id="issEmployeeId"></select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>СИЗ *</label>
|
||
<select id="issSizId"></select>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Количество</label>
|
||
<input type="number" id="issQuantity" value="1" min="1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Дата выдачи</label>
|
||
<input type="date" id="issDate">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Примечание</label>
|
||
<textarea id="issNotes" rows="2" placeholder="Размер, рост, доп. инфо..."></textarea>
|
||
</div>
|
||
<div class="norm-hint" id="issNormHint" style="display:none;">
|
||
<div class="norm-hint-label">📋 Нормы для должности "<span id="issNormPosition"></span>":</div>
|
||
<div class="norm-hint-list" id="issNormList"></div>
|
||
</div>
|
||
<input type="hidden" id="issEditId">
|
||
<div class="modal-actions">
|
||
<button class="btn btn-outline" onclick="closeModal('issuanceModal')">Отмена</button>
|
||
<button class="btn btn-primary" onclick="saveIssuance()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ===================== DATA LAYER =====================
|
||
const DB = {
|
||
_load(k) { try { return JSON.parse(localStorage.getItem('siz_'+k)) || []; } catch { return []; } },
|
||
_save(k, v) { localStorage.setItem('siz_'+k, JSON.stringify(v)); },
|
||
get employees() { return this._load('employees'); },
|
||
set employees(v) { this._save('employees', v); },
|
||
get siz() { return this._load('siz'); },
|
||
set siz(v) { this._save('siz', v); },
|
||
get warehouse() { return this._load('warehouse'); },
|
||
set warehouse(v) { this._save('warehouse', v); },
|
||
get issuances() { return this._load('issuances'); },
|
||
set issuances(v) { this._save('issuances', v); },
|
||
get norms() { return this._load('norms'); },
|
||
set norms(v) { this._save('norms', v); },
|
||
};
|
||
|
||
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 7); }
|
||
|
||
function fmtDate(d) {
|
||
if (!d) return '';
|
||
const dt = new Date(d);
|
||
return dt.toLocaleDateString('ru-RU');
|
||
}
|
||
|
||
function todayStr() {
|
||
return new Date().toISOString().slice(0, 10);
|
||
}
|
||
|
||
function daysBetween(d1, d2) {
|
||
return Math.floor((new Date(d2) - new Date(d1)) / (1000 * 60 * 60 * 24));
|
||
}
|
||
|
||
// Seed demo data if empty
|
||
function seedDemo() {
|
||
if (DB.employees.length > 0) return;
|
||
DB.employees = [
|
||
{ id: uid(), tabNum: '00001', fullName: 'Иванов Иван Иванович', position: 'Кабельщик-спайщик', department: 'Линейный цех', climateZone: '1', dateHired: '2020-03-15' },
|
||
{ id: uid(), tabNum: '00002', fullName: 'Петрова Анна Сергеевна', position: 'Электромонтер', department: 'Энергоцех', climateZone: '2', dateHired: '2021-06-01' },
|
||
{ id: uid(), tabNum: '00003', fullName: 'Сериков Асхат Нурланович', position: 'Электромонтер', department: 'Энергоцех', climateZone: '3', dateHired: '2019-11-10' },
|
||
{ id: uid(), tabNum: '00004', fullName: 'Ким Елена Викторовна', position: 'Инженер', department: 'Технический отдел', climateZone: '0', dateHired: '2022-01-20' },
|
||
{ id: uid(), tabNum: '00005', fullName: 'Нургалиев Даурен Кайратович', position: 'Сварщик', department: 'Ремонтный цех', climateZone: '2', dateHired: '2018-08-05' },
|
||
{ id: uid(), tabNum: '00006', fullName: 'Ахметов Тимур Болатович', position: 'Кабельщик-спайщик', department: 'Линейный цех', climateZone: '2', dateHired: '2023-02-01' },
|
||
];
|
||
DB.siz = [
|
||
{ id: uid(), name: 'Каска защитная', type: 'Головы', protection: '1 класс', standard: 'ГОСТ 12.4.128-83', wearMonths: 24, unit: 'шт.' },
|
||
{ id: uid(), name: 'Очки защитные', type: 'Глаз', protection: '2 класс', standard: 'ГОСТ 12.4.013-97', wearMonths: 12, unit: 'шт.' },
|
||
{ id: uid(), name: 'Перчатки х/б', type: 'Рук', protection: 'Ми', standard: 'ГОСТ 12.4.010-75', wearMonths: 1, unit: 'пар' },
|
||
{ id: uid(), name: 'Респиратор', type: 'Органов дыхания', protection: 'FFP2', standard: 'ГОСТ 12.4.294-2015', wearMonths: 1, unit: 'шт.' },
|
||
{ id: uid(), name: 'Ботинки кожаные', type: 'Ног', protection: 'Мун 200', standard: 'ГОСТ 12.4.137-2001', wearMonths: 12, unit: 'пар' },
|
||
{ id: uid(), name: 'Костюм х/б', type: 'Спецодежда', protection: 'Ми', standard: 'ГОСТ 12.4.280-2014', wearMonths: 12, unit: 'компл.' },
|
||
{ id: uid(), name: 'Пояс предохранительный', type: 'Прочее', protection: '1 класс', standard: 'ГОСТ 12.4.089-86', wearMonths: 36, unit: 'шт.' },
|
||
{ id: uid(), name: 'Костюм летний х/б', type: 'Спецодежда', protection: 'Ми', standard: 'ГОСТ 12.4.280-2014', wearMonths: 12, unit: 'компл.' },
|
||
{ id: uid(), name: 'Костюм зимний утепленный', type: 'Спецодежда', protection: 'Тн', standard: 'ГОСТ 12.4.236-2011', wearMonths: 36, unit: 'компл.' },
|
||
{ id: uid(), name: 'Рукавицы брезентовые', type: 'Рук', protection: 'Ми', standard: 'ГОСТ 12.4.010-75', wearMonths: 1, unit: 'пар' },
|
||
{ id: uid(), name: 'Диэлектрические перчатки', type: 'Рук', protection: 'Эн', standard: 'ГОСТ 12.4.307-2016', wearMonths: 12, unit: 'пар' },
|
||
{ id: uid(), name: 'Диэлектрические боты', type: 'Ног', protection: 'Эн', standard: 'ГОСТ 12.4.307-2016', wearMonths: 36, unit: 'пар' },
|
||
{ id: uid(), name: 'Щиток сварочный', type: 'Глаз', protection: '3 класс', standard: 'ГОСТ 12.4.254-2013', wearMonths: 24, unit: 'шт.' },
|
||
];
|
||
DB.norms = [
|
||
{ id: uid(), position: 'Кабельщик-спайщик', climateZone: '0', sizId: DB.siz[0].id, wearMonths: 24, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Кабельщик-спайщик', climateZone: '0', sizId: DB.siz[2].id, wearMonths: 1, quantity: 12, note: '' },
|
||
{ id: uid(), position: 'Кабельщик-спайщик', climateZone: '0', sizId: DB.siz[4].id, wearMonths: 12, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Кабельщик-спайщик', climateZone: '0', sizId: DB.siz[7].id, wearMonths: 12, quantity: 1, note: 'Летний' },
|
||
{ id: uid(), position: 'Кабельщик-спайщик', climateZone: '0', sizId: DB.siz[8].id, wearMonths: 36, quantity: 1, note: 'Зимний' },
|
||
{ id: uid(), position: 'Электромонтер', climateZone: '0', sizId: DB.siz[0].id, wearMonths: 24, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Электромонтер', climateZone: '0', sizId: DB.siz[1].id, wearMonths: 12, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Электромонтер', climateZone: '0', sizId: DB.siz[4].id, wearMonths: 12, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Электромонтер', climateZone: '0', sizId: DB.siz[10].id, wearMonths: 12, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Электромонтер', climateZone: '0', sizId: DB.siz[11].id, wearMonths: 36, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Электромонтер', climateZone: '1', sizId: DB.siz[8].id, wearMonths: 36, quantity: 1, note: 'Зимний, пояс 1: 3 года' },
|
||
{ id: uid(), position: 'Электромонтер', climateZone: '2', sizId: DB.siz[8].id, wearMonths: 30, quantity: 1, note: 'Зимний, пояс 2: 2,5 года' },
|
||
{ id: uid(), position: 'Электромонтер', climateZone: '3', sizId: DB.siz[8].id, wearMonths: 24, quantity: 1, note: 'Зимний, пояс 3: 2 года' },
|
||
{ id: uid(), position: 'Сварщик', climateZone: '0', sizId: DB.siz[0].id, wearMonths: 24, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Сварщик', climateZone: '0', sizId: DB.siz[12].id, wearMonths: 24, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Сварщик', climateZone: '0', sizId: DB.siz[9].id, wearMonths: 1, quantity: 12, note: '' },
|
||
{ id: uid(), position: 'Сварщик', climateZone: '0', sizId: DB.siz[4].id, wearMonths: 12, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Инженер', climateZone: '0', sizId: DB.siz[0].id, wearMonths: 24, quantity: 1, note: '' },
|
||
{ id: uid(), position: 'Инженер', climateZone: '0', sizId: DB.siz[1].id, wearMonths: 12, quantity: 1, note: '' },
|
||
];
|
||
DB.warehouse = [
|
||
{ id: uid(), sizId: DB.siz[0].id, quantity: 50, date: '2025-06-01', batch: 'Б-2025001', supplier: 'ТОО Спецзащита', opType: 'in' },
|
||
{ id: uid(), sizId: DB.siz[1].id, quantity: 100, date: '2025-06-15', batch: 'Б-2025002', supplier: 'ТОО Оптика KZ', opType: 'in' },
|
||
{ id: uid(), sizId: DB.siz[2].id, quantity: 500, date: '2025-07-01', batch: 'Б-2025003', supplier: 'ТОО Промтекстиль', opType: 'in' },
|
||
{ id: uid(), sizId: DB.siz[4].id, quantity: 80, date: '2025-05-20', batch: 'Б-2025004', supplier: 'ТОО Спецобувь', opType: 'in' },
|
||
{ id: uid(), sizId: DB.siz[5].id, quantity: 120, date: '2025-04-10', batch: 'Б-2025005', supplier: 'ТОО Спецзащита', opType: 'in' },
|
||
];
|
||
const emp1 = DB.employees[0].id, emp2 = DB.employees[1].id, emp3 = DB.employees[2].id;
|
||
DB.issuances = [
|
||
{ id: uid(), employeeId: emp1, sizId: DB.siz[0].id, quantity: 1, dateIssued: '2025-06-10', dateExpire: '2027-06-10', status: 'active', notes: '' },
|
||
{ id: uid(), employeeId: emp1, sizId: DB.siz[2].id, quantity: 5, dateIssued: '2025-12-01', dateExpire: '2026-01-01', status: 'expired', notes: 'Размер L' },
|
||
{ id: uid(), employeeId: emp2, sizId: DB.siz[0].id, quantity: 1, dateIssued: '2025-07-15', dateExpire: '2027-07-15', status: 'active', notes: '' },
|
||
{ id: uid(), employeeId: emp2, sizId: DB.siz[4].id, quantity: 1, dateIssued: '2025-03-01', dateExpire: '2026-03-01', status: 'expired', notes: 'Размер 42' },
|
||
{ id: uid(), employeeId: emp3, sizId: DB.siz[1].id, quantity: 1, dateIssued: '2025-08-20', dateExpire: '2026-03-20', status: 'active', notes: '' },
|
||
{ id: uid(), employeeId: emp3, sizId: DB.siz[3].id, quantity: 20, dateIssued: '2025-12-01', dateExpire: '2026-01-01', status: 'expired', notes: '' },
|
||
{ id: uid(), employeeId: emp4, sizId: DB.siz[5].id, quantity: 1, dateIssued: '2025-09-01', dateExpire: '2026-09-01', status: 'active', notes: 'Размер 52-54' },
|
||
];
|
||
}
|
||
|
||
// ===================== NAVIGATION =====================
|
||
document.querySelectorAll('.nav button').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.nav button').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||
document.getElementById(btn.dataset.panel).classList.add('active');
|
||
if (btn.dataset.panel === 'tab-reports') renderReports();
|
||
if (btn.dataset.panel === 'tab-control') renderControl();
|
||
if (btn.dataset.panel === 'tab-norms') renderNorms();
|
||
});
|
||
});
|
||
|
||
// ===================== MODALS =====================
|
||
function openModal(id) { document.getElementById(id).classList.add('show'); }
|
||
function closeModal(id) { document.getElementById(id).classList.remove('show'); }
|
||
|
||
document.querySelectorAll('.modal-overlay').forEach(ov => {
|
||
ov.addEventListener('click', (e) => { if (e.target === ov) ov.classList.remove('show'); });
|
||
});
|
||
|
||
// ===================== EMPLOYEES =====================
|
||
function openEmployeeModal(emp) {
|
||
document.getElementById('empEditId').value = emp ? emp.id : '';
|
||
document.getElementById('empModalTitle').textContent = emp ? 'Редактировать работника' : 'Добавить работника';
|
||
document.getElementById('empTabNum').value = emp ? emp.tabNum : '';
|
||
document.getElementById('empName').value = emp ? emp.fullName : '';
|
||
document.getElementById('empPosition').value = emp ? emp.position : '';
|
||
document.getElementById('empDepartment').value = emp ? emp.department : '';
|
||
document.getElementById('empDateHired').value = emp ? emp.dateHired : '';
|
||
document.getElementById('empClimateZone').value = emp ? (emp.climateZone || '0') : '0';
|
||
openModal('employeeModal');
|
||
}
|
||
|
||
function saveEmployee() {
|
||
const name = document.getElementById('empName').value.trim();
|
||
const tabNum = document.getElementById('empTabNum').value.trim();
|
||
if (!name || !tabNum) return alert('Заполните ФИО и табельный номер');
|
||
const data = {
|
||
id: document.getElementById('empEditId').value || uid(),
|
||
tabNum: tabNum,
|
||
fullName: name,
|
||
position: document.getElementById('empPosition').value.trim(),
|
||
department: document.getElementById('empDepartment').value.trim(),
|
||
climateZone: document.getElementById('empClimateZone').value || '0',
|
||
dateHired: document.getElementById('empDateHired').value,
|
||
};
|
||
let list = DB.employees;
|
||
const idx = list.findIndex(e => e.id === data.id);
|
||
if (idx >= 0) list[idx] = data; else list.push(data);
|
||
DB.employees = list;
|
||
closeModal('employeeModal');
|
||
renderEmployees();
|
||
}
|
||
|
||
function deleteEmployee(id) {
|
||
if (!confirm('Удалить работника? Связанные выдачи СИЗ также будут удалены.')) return;
|
||
DB.employees = DB.employees.filter(e => e.id !== id);
|
||
DB.issuances = DB.issuances.filter(i => i.employeeId !== id);
|
||
renderEmployees();
|
||
renderIssuances();
|
||
}
|
||
|
||
function renderEmployees() {
|
||
const q = (document.getElementById('empSearch').value || '').toLowerCase();
|
||
let list = DB.employees;
|
||
if (q) list = list.filter(e =>
|
||
(e.fullName || '').toLowerCase().includes(q) ||
|
||
(e.position || '').toLowerCase().includes(q) ||
|
||
(e.department || '').toLowerCase().includes(q) ||
|
||
(e.tabNum || '').toLowerCase().includes(q)
|
||
);
|
||
const tbody = document.getElementById('empTable');
|
||
tbody.innerHTML = list.map(e => `<tr>
|
||
<td>${e.tabNum}</td>
|
||
<td><strong>${e.fullName}</strong></td>
|
||
<td>${e.position}</td>
|
||
<td>${e.department}</td>
|
||
<td>${fmtDate(e.dateHired)}</td>
|
||
<td>
|
||
<button class="action-btn" title="Редактировать" onclick="openEmployeeModal(DB.employees.find(x=>x.id==='${e.id}'))">✏️</button>
|
||
<button class="action-btn" title="Удалить" onclick="deleteEmployee('${e.id}')">🗑️</button>
|
||
</td>
|
||
</tr>`).join('');
|
||
document.getElementById('empEmpty').style.display = list.length ? 'none' : 'block';
|
||
}
|
||
|
||
// ===================== SIZ CATALOG =====================
|
||
function openSizModal(siz) {
|
||
document.getElementById('sizEditId').value = siz ? siz.id : '';
|
||
document.getElementById('sizModalTitle').textContent = siz ? 'Редактировать СИЗ' : 'Добавить СИЗ';
|
||
document.getElementById('sizName').value = siz ? siz.name : '';
|
||
document.getElementById('sizType').value = siz ? siz.type : 'Головы';
|
||
document.getElementById('sizProtection').value = siz ? siz.protection : '';
|
||
document.getElementById('sizStandard').value = siz ? siz.standard : '';
|
||
document.getElementById('sizWearMonths').value = siz ? siz.wearMonths : '12';
|
||
document.getElementById('sizUnit').value = siz ? siz.unit : 'шт.';
|
||
openModal('sizModal');
|
||
}
|
||
|
||
function saveSiz() {
|
||
const name = document.getElementById('sizName').value.trim();
|
||
if (!name) return alert('Введите наименование');
|
||
const data = {
|
||
id: document.getElementById('sizEditId').value || uid(),
|
||
name,
|
||
type: document.getElementById('sizType').value,
|
||
protection: document.getElementById('sizProtection').value.trim(),
|
||
standard: document.getElementById('sizStandard').value.trim(),
|
||
wearMonths: parseInt(document.getElementById('sizWearMonths').value) || 12,
|
||
unit: document.getElementById('sizUnit').value.trim() || 'шт.',
|
||
};
|
||
let list = DB.siz;
|
||
const idx = list.findIndex(e => e.id === data.id);
|
||
if (idx >= 0) list[idx] = data; else list.push(data);
|
||
DB.siz = list;
|
||
closeModal('sizModal');
|
||
renderSiz();
|
||
}
|
||
|
||
function deleteSiz(id) {
|
||
if (!confirm('Удалить позицию СИЗ? Связанные складские записи и выдачи также будут удалены.')) return;
|
||
DB.siz = DB.siz.filter(e => e.id !== id);
|
||
DB.warehouse = DB.warehouse.filter(w => w.sizId !== id);
|
||
DB.issuances = DB.issuances.filter(i => i.sizId !== id);
|
||
renderSiz();
|
||
}
|
||
|
||
function renderSiz() {
|
||
const q = (document.getElementById('sizSearch').value || '').toLowerCase();
|
||
const ft = document.getElementById('sizFilterType').value;
|
||
let list = DB.siz;
|
||
if (q) list = list.filter(e => e.name.toLowerCase().includes(q) || e.type.toLowerCase().includes(q));
|
||
if (ft) list = list.filter(e => e.type === ft);
|
||
document.getElementById('sizTable').innerHTML = list.map(e => `<tr>
|
||
<td><strong>${e.name}</strong></td>
|
||
<td>${e.type}</td>
|
||
<td>${e.protection}</td>
|
||
<td>${e.standard}</td>
|
||
<td>${e.wearMonths}</td>
|
||
<td>${e.unit}</td>
|
||
<td>
|
||
<button class="action-btn" title="Редактировать" onclick="openSizModal(DB.siz.find(x=>x.id==='${e.id}'))">✏️</button>
|
||
<button class="action-btn" title="Удалить" onclick="deleteSiz('${e.id}')">🗑️</button>
|
||
</td>
|
||
</tr>`).join('');
|
||
document.getElementById('sizEmpty').style.display = list.length ? 'none' : 'block';
|
||
}
|
||
|
||
// ===================== WAREHOUSE =====================
|
||
function fillSizSelect(selectId, emptyOption) {
|
||
const sel = document.getElementById(selectId);
|
||
sel.innerHTML = (emptyOption ? `<option value="">${emptyOption}</option>` : '') +
|
||
DB.siz.map(s => `<option value="${s.id}">${s.name} (${s.unit})</option>`).join('');
|
||
}
|
||
|
||
function openWarehouseModal(opType) {
|
||
opType = opType || 'in';
|
||
document.getElementById('whEditId').value = '';
|
||
document.getElementById('whOpType').value = opType;
|
||
document.getElementById('whModalTitle').textContent = opType === 'in' ? 'Приход на склад' : 'Расход (списание)';
|
||
document.getElementById('whQuantity').value = '1';
|
||
document.getElementById('whDate').value = todayStr();
|
||
document.getElementById('whBatch').value = '';
|
||
document.getElementById('whSupplier').value = '';
|
||
fillSizSelect('whSizId', '— Выберите СИЗ —');
|
||
openModal('warehouseModal');
|
||
}
|
||
|
||
function saveWarehouse() {
|
||
const sizId = document.getElementById('whSizId').value;
|
||
if (!sizId) return alert('Выберите СИЗ');
|
||
const qty = parseInt(document.getElementById('whQuantity').value) || 0;
|
||
if (qty <= 0) return alert('Количество должно быть больше 0');
|
||
const opType = document.getElementById('whOpType').value;
|
||
const data = {
|
||
id: document.getElementById('whEditId').value || uid(),
|
||
sizId,
|
||
quantity: opType === 'in' ? qty : -qty,
|
||
date: document.getElementById('whDate').value || todayStr(),
|
||
batch: document.getElementById('whBatch').value.trim(),
|
||
supplier: document.getElementById('whSupplier').value.trim(),
|
||
opType,
|
||
};
|
||
let list = DB.warehouse;
|
||
const idx = list.findIndex(e => e.id === data.id);
|
||
if (idx >= 0) list[idx] = data; else list.push(data);
|
||
DB.warehouse = list;
|
||
closeModal('warehouseModal');
|
||
renderWarehouse();
|
||
}
|
||
|
||
function deleteWarehouse(id) {
|
||
if (!confirm('Удалить складскую запись?')) return;
|
||
DB.warehouse = DB.warehouse.filter(w => w.id !== id);
|
||
renderWarehouse();
|
||
}
|
||
|
||
function getSizName(id) {
|
||
const s = DB.siz.find(x => x.id === id);
|
||
return s ? s.name : '—';
|
||
}
|
||
|
||
function getStockBalance(sizId) {
|
||
return DB.warehouse.filter(w => w.sizId === sizId).reduce((sum, w) => sum + w.quantity, 0);
|
||
}
|
||
|
||
function renderWarehouse() {
|
||
const q = (document.getElementById('whSearch').value || '').toLowerCase();
|
||
let list = DB.warehouse.slice().reverse();
|
||
if (q) list = list.filter(w =>
|
||
getSizName(w.sizId).toLowerCase().includes(q) ||
|
||
(w.batch || '').toLowerCase().includes(q) ||
|
||
(w.supplier || '').toLowerCase().includes(q)
|
||
);
|
||
document.getElementById('whTable').innerHTML = list.map(w => {
|
||
const sizInfo = DB.siz.find(s => s.id === w.sizId);
|
||
return `<tr>
|
||
<td><strong>${getSizName(w.sizId)}</strong></td>
|
||
<td style="color:${w.quantity >= 0 ? 'var(--green)' : 'var(--red)'}; font-weight:700;">${w.quantity > 0 ? '+' : ''}${w.quantity}</td>
|
||
<td>${fmtDate(w.date)}</td>
|
||
<td>${w.opType === 'in' ? '📥 Приход' : '📤 Расход'}</td>
|
||
<td>${w.batch || '—'}</td>
|
||
<td>${w.supplier || '—'}</td>
|
||
<td>
|
||
<button class="action-btn" title="Удалить" onclick="deleteWarehouse('${w.id}')">🗑️</button>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
document.getElementById('whEmpty').style.display = list.length ? 'none' : 'block';
|
||
}
|
||
|
||
// ===================== NORMS =====================
|
||
function fillNormSizSelect() {
|
||
const sel = document.getElementById('normSizId');
|
||
sel.innerHTML = '<option value="">— Выберите СИЗ —</option>' +
|
||
DB.siz.map(s => `<option value="${s.id}">${s.name} (${s.wearMonths} мес)</option>`).join('');
|
||
}
|
||
|
||
function fillPositionsDatalist() {
|
||
const list = document.getElementById('positionsList');
|
||
const positions = [...new Set([...DB.norms.map(n => n.position), ...DB.employees.map(e => e.position)].filter(Boolean))];
|
||
list.innerHTML = positions.map(p => `<option value="${p}">`).join('');
|
||
}
|
||
|
||
function openNormModal(norm) {
|
||
document.getElementById('normEditId').value = norm ? norm.id : '';
|
||
document.getElementById('normModalTitle').textContent = norm ? 'Редактировать норму' : 'Добавить норму выдачи';
|
||
document.getElementById('normPosition').value = norm ? norm.position : '';
|
||
document.getElementById('normClimateZone').value = norm ? norm.climateZone : '0';
|
||
document.getElementById('normWearMonths').value = norm ? norm.wearMonths : '12';
|
||
document.getElementById('normWearDisplay').value = norm ? monthsToText(norm.wearMonths) : '12 мес = 1 год';
|
||
document.getElementById('normQuantity').value = norm ? norm.quantity : '1';
|
||
document.getElementById('normNote').value = norm ? norm.note : '';
|
||
fillNormSizSelect();
|
||
fillPositionsDatalist();
|
||
if (norm) document.getElementById('normSizId').value = norm.sizId;
|
||
openModal('normModal');
|
||
}
|
||
|
||
function monthsToText(m) {
|
||
if (m <= 0) return '1 мес';
|
||
if (m < 12) return m + ' мес';
|
||
const y = Math.floor(m / 12);
|
||
const r = m % 12;
|
||
let s = y + ' год';
|
||
if (y > 1 && y < 5) s = y + ' года';
|
||
if (y >= 5) s = y + ' лет';
|
||
if (r > 0) s += ' ' + r + ' мес';
|
||
return s + ' (' + m + ' мес)';
|
||
}
|
||
|
||
function parseNormWear() {
|
||
const val = document.getElementById('normWearDisplay').value.trim().toLowerCase();
|
||
let months = 12;
|
||
const yearMatch = val.match(/(\d+)\s*(год|года|лет)/);
|
||
const monthMatch = val.match(/(\d+)\s*мес/);
|
||
if (yearMatch) months += parseInt(yearMatch[1]) * 12;
|
||
else if (monthMatch) months = parseInt(monthMatch[1]);
|
||
document.getElementById('normWearMonths').value = months;
|
||
}
|
||
|
||
function saveNorm() {
|
||
const pos = document.getElementById('normPosition').value.trim();
|
||
const sizId = document.getElementById('normSizId').value;
|
||
if (!pos || !sizId) return alert('Заполните должность и выберите СИЗ');
|
||
parseNormWear();
|
||
const data = {
|
||
id: document.getElementById('normEditId').value || uid(),
|
||
position: pos,
|
||
climateZone: document.getElementById('normClimateZone').value || '0',
|
||
sizId,
|
||
wearMonths: parseInt(document.getElementById('normWearMonths').value) || 12,
|
||
quantity: parseInt(document.getElementById('normQuantity').value) || 1,
|
||
note: document.getElementById('normNote').value.trim(),
|
||
};
|
||
let list = DB.norms;
|
||
const idx = list.findIndex(e => e.id === data.id);
|
||
if (idx >= 0) list[idx] = data; else list.push(data);
|
||
DB.norms = list;
|
||
closeModal('normModal');
|
||
renderNorms();
|
||
}
|
||
|
||
function deleteNorm(id) {
|
||
if (!confirm('Удалить норму выдачи?')) return;
|
||
DB.norms = DB.norms.filter(n => n.id !== id);
|
||
renderNorms();
|
||
}
|
||
|
||
function climateZoneLabel(z) {
|
||
const m = { '0': 'Общий', '1': 'Пояс 1', '2': 'Пояс 2', '3': 'Пояс 3' };
|
||
return m[z] || 'Общий';
|
||
}
|
||
|
||
function renderNorms() {
|
||
const q = (document.getElementById('normSearch').value || '').toLowerCase();
|
||
const fz = document.getElementById('normFilterZone').value;
|
||
let list = DB.norms;
|
||
if (q) list = list.filter(n => n.position.toLowerCase().includes(q) || getSizName(n.sizId).toLowerCase().includes(q));
|
||
if (fz !== '') list = list.filter(n => n.climateZone === fz);
|
||
document.getElementById('normTable').innerHTML = list.map(n => `<tr>
|
||
<td><strong>${n.position}</strong></td>
|
||
<td>${climateZoneLabel(n.climateZone)}</td>
|
||
<td>${getSizName(n.sizId)}</td>
|
||
<td>${monthsToText(n.wearMonths)}</td>
|
||
<td>${n.quantity} ${(DB.siz.find(s=>s.id===n.sizId)||{}).unit||'шт.'}</td>
|
||
<td>${n.note || '—'}</td>
|
||
<td>
|
||
<button class="action-btn" title="Редактировать" onclick="openNormModal(DB.norms.find(x=>x.id==='${n.id}'))">✏️</button>
|
||
<button class="action-btn" title="Удалить" onclick="deleteNorm('${n.id}')">🗑️</button>
|
||
</td>
|
||
</tr>`).join('');
|
||
document.getElementById('normEmpty').style.display = list.length ? 'none' : 'block';
|
||
}
|
||
|
||
// ===================== ISSUANCE — SHOW NORMS =====================
|
||
function showNormsForEmployee() {
|
||
const empId = document.getElementById('issEmployeeId').value;
|
||
const hint = document.getElementById('issNormHint');
|
||
const listEl = document.getElementById('issNormList');
|
||
const posEl = document.getElementById('issNormPosition');
|
||
if (!empId) { hint.style.display = 'none'; return; }
|
||
const emp = DB.employees.find(e => e.id === empId);
|
||
if (!emp) { hint.style.display = 'none'; return; }
|
||
posEl.textContent = emp.position;
|
||
const empZone = emp.climateZone || '0';
|
||
let matched = DB.norms.filter(n => n.position === emp.position && n.climateZone === empZone);
|
||
if (matched.length === 0) matched = DB.norms.filter(n => n.position === emp.position && n.climateZone === '0');
|
||
if (matched.length === 0) { hint.style.display = 'none'; return; }
|
||
listEl.innerHTML = matched.map(n => {
|
||
const siz = DB.siz.find(s => s.id === n.sizId);
|
||
const sizName = siz ? siz.name : '—';
|
||
const unit = siz ? siz.unit : 'шт.';
|
||
return `<div class="norm-hint-item">
|
||
<div>
|
||
<span class="norm-hint-item-name">${sizName}</span>
|
||
<span class="norm-hint-item-wear">— ${monthsToText(n.wearMonths)}${n.note ? ' • ' + n.note : ''}</span>
|
||
</div>
|
||
<button class="btn-xs" onclick="quickIssue('${n.sizId}',${n.wearMonths},${n.quantity},'${emp.id}',document.getElementById('issDate').value||todayStr())">Выдать</button>
|
||
</div>`;
|
||
}).join('');
|
||
hint.style.display = 'block';
|
||
}
|
||
|
||
function quickIssue(sizId, wearMonths, quantity, empId, dateIssued) {
|
||
const expireDate = new Date(dateIssued);
|
||
expireDate.setMonth(expireDate.getMonth() + wearMonths);
|
||
const dateExpire = expireDate.toISOString().slice(0, 10);
|
||
const today = todayStr();
|
||
let status = dateExpire < today ? 'expired' : 'active';
|
||
const data = {
|
||
id: uid(),
|
||
employeeId: empId,
|
||
sizId,
|
||
quantity,
|
||
dateIssued,
|
||
dateExpire,
|
||
status,
|
||
notes: '',
|
||
};
|
||
DB.issuances = [...DB.issuances, data];
|
||
closeModal('issuanceModal');
|
||
renderIssuances();
|
||
}
|
||
|
||
// ===== hook employee select change =====
|
||
const _origFillEmployeeSelect = fillEmployeeSelect;
|
||
fillEmployeeSelect = function() {
|
||
const sel = document.getElementById('issEmployeeId');
|
||
const v = sel.value;
|
||
sel.innerHTML = '<option value="">— Выберите —</option>' +
|
||
DB.employees.map(e => `<option value="${e.id}">${e.fullName} (${e.tabNum})</option>`).join('');
|
||
if (v) sel.value = v;
|
||
};
|
||
document.getElementById('issEmployeeId').addEventListener('change', showNormsForEmployee);
|
||
function fillEmployeeSelect() {
|
||
const sel = document.getElementById('issEmployeeId');
|
||
sel.innerHTML = '<option value="">— Выберите —</option>' +
|
||
DB.employees.map(e => `<option value="${e.id}">${e.fullName} (${e.tabNum})</option>`).join('');
|
||
}
|
||
|
||
function openIssuanceModal(iss) {
|
||
document.getElementById('issEditId').value = iss ? iss.id : '';
|
||
document.getElementById('issModalTitle').textContent = iss ? 'Редактировать выдачу' : 'Выдача СИЗ';
|
||
document.getElementById('issQuantity').value = iss ? iss.quantity : '1';
|
||
document.getElementById('issDate').value = iss ? iss.dateIssued : todayStr();
|
||
document.getElementById('issNotes').value = iss ? iss.notes : '';
|
||
document.getElementById('issNormHint').style.display = 'none';
|
||
fillEmployeeSelect();
|
||
fillSizSelect('issSizId', '— Выберите СИЗ —');
|
||
if (iss) {
|
||
document.getElementById('issEmployeeId').value = iss.employeeId;
|
||
document.getElementById('issSizId').value = iss.sizId;
|
||
showNormsForEmployee();
|
||
}
|
||
openModal('issuanceModal');
|
||
}
|
||
|
||
function saveIssuance() {
|
||
const empId = document.getElementById('issEmployeeId').value;
|
||
const sizId = document.getElementById('issSizId').value;
|
||
if (!empId || !sizId) return alert('Выберите работника и СИЗ');
|
||
const siz = DB.siz.find(s => s.id === sizId);
|
||
const wearMonths = siz ? siz.wearMonths : 12;
|
||
const dateIssued = document.getElementById('issDate').value || todayStr();
|
||
const expireDate = new Date(dateIssued);
|
||
expireDate.setMonth(expireDate.getMonth() + wearMonths);
|
||
const dateExpire = expireDate.toISOString().slice(0, 10);
|
||
const today = todayStr();
|
||
let status = 'active';
|
||
if (dateExpire < today) status = 'expired';
|
||
const data = {
|
||
id: document.getElementById('issEditId').value || uid(),
|
||
employeeId: empId,
|
||
sizId,
|
||
quantity: parseInt(document.getElementById('issQuantity').value) || 1,
|
||
dateIssued,
|
||
dateExpire,
|
||
status,
|
||
notes: document.getElementById('issNotes').value.trim(),
|
||
};
|
||
let list = DB.issuances;
|
||
const idx = list.findIndex(e => e.id === data.id);
|
||
if (idx >= 0) list[idx] = data; else list.push(data);
|
||
DB.issuances = list;
|
||
closeModal('issuanceModal');
|
||
renderIssuances();
|
||
}
|
||
|
||
function deleteIssuance(id) {
|
||
if (!confirm('Удалить запись о выдаче?')) return;
|
||
DB.issuances = DB.issuances.filter(i => i.id !== id);
|
||
renderIssuances();
|
||
}
|
||
|
||
function getEmpName(id) {
|
||
const e = DB.employees.find(x => x.id === id);
|
||
return e ? e.fullName : '—';
|
||
}
|
||
|
||
function getIssStatus(iss) {
|
||
if (iss.status === 'returned') return 'returned';
|
||
const today = todayStr();
|
||
if (iss.dateExpire < today) {
|
||
const list = DB.issuances;
|
||
const idx = list.findIndex(i => i.id === iss.id);
|
||
if (idx >= 0 && list[idx].status !== 'expired') { list[idx].status = 'expired'; DB.issuances = list; }
|
||
return 'expired';
|
||
}
|
||
const thirtyDays = new Date(today);
|
||
thirtyDays.setDate(thirtyDays.getDate() + 30);
|
||
if (iss.dateExpire <= thirtyDays.toISOString().slice(0, 10)) return 'expiring';
|
||
return 'active';
|
||
}
|
||
|
||
function statusBadge(status) {
|
||
if (status === 'expired') return '<span class="badge badge-red">Просрочено</span>';
|
||
if (status === 'expiring') return '<span class="badge badge-orange">Истекает</span>';
|
||
if (status === 'returned') return '<span class="badge badge-gray">Возвращено</span>';
|
||
return '<span class="badge badge-green">Действует</span>';
|
||
}
|
||
|
||
function returnIssuance(id) {
|
||
let list = DB.issuances;
|
||
const idx = list.findIndex(i => i.id === id);
|
||
if (idx >= 0) { list[idx].status = 'returned'; DB.issuances = list; }
|
||
renderIssuances();
|
||
}
|
||
|
||
function renderIssuances() {
|
||
const q = (document.getElementById('issSearch').value || '').toLowerCase();
|
||
const fs = document.getElementById('issFilterStatus').value;
|
||
let list = DB.issuances.slice().reverse();
|
||
list.forEach(iss => { iss._status = getIssStatus(iss); });
|
||
if (q) list = list.filter(i =>
|
||
getEmpName(i.employeeId).toLowerCase().includes(q) ||
|
||
getSizName(i.sizId).toLowerCase().includes(q) ||
|
||
(i.notes || '').toLowerCase().includes(q)
|
||
);
|
||
if (fs) list = list.filter(i => i._status === fs);
|
||
document.getElementById('issTable').innerHTML = list.map(i => {
|
||
const siz = DB.siz.find(s => s.id === i.sizId);
|
||
return `<tr>
|
||
<td><strong>${getEmpName(i.employeeId)}</strong></td>
|
||
<td>${getSizName(i.sizId)}</td>
|
||
<td>${i.quantity} ${siz ? siz.unit : ''}</td>
|
||
<td>${fmtDate(i.dateIssued)}</td>
|
||
<td>${fmtDate(i.dateExpire)}</td>
|
||
<td>${statusBadge(i._status)}</td>
|
||
<td>
|
||
<button class="action-btn" title="Редактировать" onclick="openIssuanceModal(DB.issuances.find(x=>x.id==='${i.id}'))">✏️</button>
|
||
${i._status !== 'returned' ? `<button class="action-btn" title="Возврат" onclick="returnIssuance('${i.id}')">↩️</button>` : ''}
|
||
<button class="action-btn" title="Удалить" onclick="deleteIssuance('${i.id}')">🗑️</button>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
document.getElementById('issEmpty').style.display = list.length ? 'none' : 'block';
|
||
}
|
||
|
||
// ===================== CONTROL =====================
|
||
function renderControl() {
|
||
let expired = 0, expiring = 0, active = 0;
|
||
const alerts = [];
|
||
DB.issuances.forEach(i => {
|
||
const st = getIssStatus(i);
|
||
if (st === 'expired') { expired++; }
|
||
else if (st === 'expiring') { expiring++; }
|
||
else if (st === 'active') { active++; }
|
||
if (st === 'expired' || st === 'expiring') {
|
||
alerts.push({
|
||
employee: getEmpName(i.employeeId),
|
||
siz: getSizName(i.sizId),
|
||
expire: i.dateExpire,
|
||
status: st,
|
||
days: daysBetween(todayStr(), i.dateExpire),
|
||
});
|
||
}
|
||
});
|
||
document.getElementById('stExpired').textContent = expired;
|
||
document.getElementById('stExpiring').textContent = expiring;
|
||
document.getElementById('stActive').textContent = active;
|
||
alerts.sort((a, b) => a.days - b.days);
|
||
document.getElementById('alertList').innerHTML = alerts.map(a =>
|
||
`<div class="alert-item ${a.status === 'expired' ? 'red' : 'orange'}">
|
||
<span style="font-size:20px;">${a.status === 'expired' ? '🔴' : '🟡'}</span>
|
||
<div>
|
||
<strong>${a.employee}</strong> — ${a.siz}<br>
|
||
<small>Срок истёк: ${fmtDate(a.expire)} (${a.days >= 0 ? 'просрочено на ' + a.days + ' дн.' : 'осталось ' + Math.abs(a.days) + ' дн.'})</small>
|
||
</div>
|
||
</div>`
|
||
).join('') || '<div class="empty"><p>✅ Все сроки в норме.</p></div>';
|
||
}
|
||
|
||
// ===================== REPORTS =====================
|
||
function renderReports() {
|
||
const employees = DB.employees;
|
||
const sizList = DB.siz;
|
||
const warehouse = DB.warehouse;
|
||
const issuances = DB.issuances;
|
||
|
||
document.getElementById('repEmployees').textContent = employees.length;
|
||
document.getElementById('repSiz').textContent = sizList.length;
|
||
|
||
let totalStock = 0, totalIssued = 0, totalExpired = 0;
|
||
sizList.forEach(s => {
|
||
totalStock += getStockBalance(s.id);
|
||
});
|
||
issuances.forEach(i => {
|
||
if (i.status !== 'returned') {
|
||
totalIssued += i.quantity;
|
||
if (getIssStatus(i) === 'expired') totalExpired += i.quantity;
|
||
} else {
|
||
totalIssued += i.quantity;
|
||
}
|
||
});
|
||
document.getElementById('repStock').textContent = totalStock;
|
||
document.getElementById('repIssued').textContent = totalIssued;
|
||
document.getElementById('repExpired').textContent = totalExpired;
|
||
|
||
document.getElementById('repTable').innerHTML = employees.map(e => {
|
||
const empIss = issuances.filter(i => i.employeeId === e.id);
|
||
const issuedCount = empIss.filter(i => i.status !== 'returned').length;
|
||
const expiredCount = empIss.filter(i => getIssStatus(i) === 'expired').length;
|
||
return `<tr>
|
||
<td><strong>${e.fullName}</strong></td>
|
||
<td>${e.position}</td>
|
||
<td>${issuedCount}</td>
|
||
<td>${expiredCount ? '<span class="badge badge-red">' + expiredCount + '</span>' : '<span class="badge badge-green">0</span>'}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
// ===================== EXCEL EXPORT =====================
|
||
function exportAllToExcel() {
|
||
const wb = XLSX.utils.book_new();
|
||
|
||
const emps = DB.employees.map(e => ({
|
||
'Таб. №': e.tabNum,
|
||
'ФИО': e.fullName,
|
||
'Должность': e.position,
|
||
'Отдел': e.department,
|
||
'Дата приема': fmtDate(e.dateHired),
|
||
}));
|
||
const ws1 = XLSX.utils.json_to_sheet(emps);
|
||
XLSX.utils.book_append_sheet(wb, ws1, 'Работники');
|
||
|
||
const siz = DB.siz.map(s => ({
|
||
'Наименование': s.name,
|
||
'Тип': s.type,
|
||
'Класс защиты': s.protection,
|
||
'ГОСТ/ТУ': s.standard,
|
||
'Срок носки (мес)': s.wearMonths,
|
||
'Ед. изм.': s.unit,
|
||
'Остаток на складе': getStockBalance(s.id),
|
||
}));
|
||
const ws2 = XLSX.utils.json_to_sheet(siz);
|
||
XLSX.utils.book_append_sheet(wb, ws2, 'Справочник СИЗ');
|
||
|
||
const wh = DB.warehouse.map(w => ({
|
||
'СИЗ': getSizName(w.sizId),
|
||
'Количество': w.quantity,
|
||
'Дата': fmtDate(w.date),
|
||
'Операция': w.opType === 'in' ? 'Приход' : 'Расход',
|
||
'Партия': w.batch,
|
||
'Поставщик': w.supplier,
|
||
}));
|
||
const ws3 = XLSX.utils.json_to_sheet(wh);
|
||
XLSX.utils.book_append_sheet(wb, ws3, 'Склад');
|
||
|
||
const iss = DB.issuances.map(i => {
|
||
const st = getIssStatus(i);
|
||
const siz = DB.siz.find(s => s.id === i.sizId);
|
||
return {
|
||
'Работник': getEmpName(i.employeeId),
|
||
'СИЗ': getSizName(i.sizId),
|
||
'Количество': i.quantity + ' ' + (siz ? siz.unit : ''),
|
||
'Дата выдачи': fmtDate(i.dateIssued),
|
||
'Годен до': fmtDate(i.dateExpire),
|
||
'Статус': st === 'expired' ? 'Просрочено' : st === 'expiring' ? 'Истекает' : st === 'returned' ? 'Возвращено' : 'Действует',
|
||
'Примечание': i.notes,
|
||
};
|
||
});
|
||
const ws4 = XLSX.utils.json_to_sheet(iss);
|
||
XLSX.utils.book_append_sheet(wb, ws4, 'Выдача СИЗ');
|
||
|
||
const norms = DB.norms.map(n => ({
|
||
'Должность': n.position,
|
||
'Климатический пояс': climateZoneLabel(n.climateZone),
|
||
'СИЗ': getSizName(n.sizId),
|
||
'Срок носки': monthsToText(n.wearMonths),
|
||
'Количество': n.quantity,
|
||
'Примечание': n.note,
|
||
}));
|
||
const ws5 = XLSX.utils.json_to_sheet(norms);
|
||
XLSX.utils.book_append_sheet(wb, ws5, 'Нормы выдачи');
|
||
|
||
XLSX.writeFile(wb, 'Учет_СИЗ_' + new Date().toISOString().slice(0, 10) + '.xlsx');
|
||
}
|
||
|
||
// ===================== RESET =====================
|
||
function resetData() {
|
||
if (!confirm('Сбросить все данные к демо-версии? Текущие данные будут потеряны.')) return;
|
||
localStorage.clear();
|
||
seedDemo();
|
||
location.reload();
|
||
}
|
||
|
||
// ===================== INIT =====================
|
||
seedDemo();
|
||
renderEmployees();
|
||
renderSiz();
|
||
renderWarehouse();
|
||
renderNorms();
|
||
renderIssuances();
|
||
renderControl();
|
||
renderReports();
|
||
</script>
|
||
</body>
|
||
</html>
|