v2: Нормы выдачи СИЗ по должностям + климатические пояса + быстрый подбор при выдаче
This commit is contained in:
parent
9b15463ba2
commit
6a6512db49
359
index.html
359
index.html
@ -364,6 +364,45 @@ tr:hover td { background: var(--cyan-50); }
|
|||||||
}
|
}
|
||||||
.action-btn:hover { background: var(--gray-100); }
|
.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 */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.header { padding: 12px 16px; }
|
.header { padding: 12px 16px; }
|
||||||
@ -394,6 +433,7 @@ tr:hover td { background: var(--cyan-50); }
|
|||||||
<button class="active" data-panel="tab-employees">👤 Работники</button>
|
<button class="active" data-panel="tab-employees">👤 Работники</button>
|
||||||
<button data-panel="tab-siz">🛡️ Справочник СИЗ</button>
|
<button data-panel="tab-siz">🛡️ Справочник СИЗ</button>
|
||||||
<button data-panel="tab-warehouse">📦 Склад</button>
|
<button data-panel="tab-warehouse">📦 Склад</button>
|
||||||
|
<button data-panel="tab-norms">📋 Нормы выдачи</button>
|
||||||
<button data-panel="tab-issuance">📤 Выдача</button>
|
<button data-panel="tab-issuance">📤 Выдача</button>
|
||||||
<button data-panel="tab-control">⚠️ Контроль сроков</button>
|
<button data-panel="tab-control">⚠️ Контроль сроков</button>
|
||||||
<button data-panel="tab-reports">📊 Отчеты</button>
|
<button data-panel="tab-reports">📊 Отчеты</button>
|
||||||
@ -490,6 +530,41 @@ tr:hover td { background: var(--cyan-50); }
|
|||||||
</div>
|
</div>
|
||||||
</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: Выдача СИЗ -->
|
<!-- TAB: Выдача СИЗ -->
|
||||||
<div class="panel" id="tab-issuance">
|
<div class="panel" id="tab-issuance">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@ -612,6 +687,17 @@ tr:hover td { background: var(--cyan-50); }
|
|||||||
<input type="text" id="empDepartment" placeholder="Технический отдел">
|
<input type="text" id="empDepartment" placeholder="Технический отдел">
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<input type="hidden" id="empEditId">
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn btn-outline" onclick="closeModal('employeeModal')">Отмена</button>
|
<button class="btn btn-outline" onclick="closeModal('employeeModal')">Отмена</button>
|
||||||
@ -700,6 +786,53 @@ tr:hover td { background: var(--cyan-50); }
|
|||||||
</div>
|
</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 -->
|
<!-- ISSUANCE MODAL -->
|
||||||
<div class="modal-overlay" id="issuanceModal">
|
<div class="modal-overlay" id="issuanceModal">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
@ -726,6 +859,10 @@ tr:hover td { background: var(--cyan-50); }
|
|||||||
<label>Примечание</label>
|
<label>Примечание</label>
|
||||||
<textarea id="issNotes" rows="2" placeholder="Размер, рост, доп. инфо..."></textarea>
|
<textarea id="issNotes" rows="2" placeholder="Размер, рост, доп. инфо..."></textarea>
|
||||||
</div>
|
</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">
|
<input type="hidden" id="issEditId">
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn btn-outline" onclick="closeModal('issuanceModal')">Отмена</button>
|
<button class="btn btn-outline" onclick="closeModal('issuanceModal')">Отмена</button>
|
||||||
@ -747,6 +884,8 @@ const DB = {
|
|||||||
set warehouse(v) { this._save('warehouse', v); },
|
set warehouse(v) { this._save('warehouse', v); },
|
||||||
get issuances() { return this._load('issuances'); },
|
get issuances() { return this._load('issuances'); },
|
||||||
set issuances(v) { this._save('issuances', v); },
|
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 uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 7); }
|
||||||
@ -769,11 +908,12 @@ function daysBetween(d1, d2) {
|
|||||||
function seedDemo() {
|
function seedDemo() {
|
||||||
if (DB.employees.length > 0) return;
|
if (DB.employees.length > 0) return;
|
||||||
DB.employees = [
|
DB.employees = [
|
||||||
{ id: uid(), tabNum: '00001', fullName: 'Иванов Иван Иванович', position: 'Инженер', department: 'Технический отдел', dateHired: '2020-03-15' },
|
{ id: uid(), tabNum: '00001', fullName: 'Иванов Иван Иванович', position: 'Кабельщик-спайщик', department: 'Линейный цех', climateZone: '1', dateHired: '2020-03-15' },
|
||||||
{ id: uid(), tabNum: '00002', fullName: 'Петрова Анна Сергеевна', position: 'Монтажник', department: 'Линейный цех', dateHired: '2021-06-01' },
|
{ id: uid(), tabNum: '00002', fullName: 'Петрова Анна Сергеевна', position: 'Электромонтер', department: 'Энергоцех', climateZone: '2', dateHired: '2021-06-01' },
|
||||||
{ id: uid(), tabNum: '00003', fullName: 'Сериков Асхат Нурланович', position: 'Электрик', department: 'Энергоцех', dateHired: '2019-11-10' },
|
{ id: uid(), tabNum: '00003', fullName: 'Сериков Асхат Нурланович', position: 'Электромонтер', department: 'Энергоцех', climateZone: '3', dateHired: '2019-11-10' },
|
||||||
{ id: uid(), tabNum: '00004', fullName: 'Ким Елена Викторовна', position: 'Техник', department: 'Технический отдел', dateHired: '2022-01-20' },
|
{ id: uid(), tabNum: '00004', fullName: 'Ким Елена Викторовна', position: 'Инженер', department: 'Технический отдел', climateZone: '0', dateHired: '2022-01-20' },
|
||||||
{ id: uid(), tabNum: '00005', fullName: 'Нургалиев Даурен Кайратович', position: 'Сварщик', department: 'Ремонтный цех', dateHired: '2018-08-05' },
|
{ 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 = [
|
DB.siz = [
|
||||||
{ id: uid(), name: 'Каска защитная', type: 'Головы', protection: '1 класс', standard: 'ГОСТ 12.4.128-83', wearMonths: 24, unit: 'шт.' },
|
{ id: uid(), name: 'Каска защитная', type: 'Головы', protection: '1 класс', standard: 'ГОСТ 12.4.128-83', wearMonths: 24, unit: 'шт.' },
|
||||||
@ -783,6 +923,33 @@ function seedDemo() {
|
|||||||
{ id: uid(), name: 'Ботинки кожаные', type: 'Ног', protection: 'Мун 200', standard: 'ГОСТ 12.4.137-2001', wearMonths: 12, 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: 'Ми', 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: '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 = [
|
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[0].id, quantity: 50, date: '2025-06-01', batch: 'Б-2025001', supplier: 'ТОО Спецзащита', opType: 'in' },
|
||||||
@ -812,6 +979,7 @@ document.querySelectorAll('.nav button').forEach(btn => {
|
|||||||
document.getElementById(btn.dataset.panel).classList.add('active');
|
document.getElementById(btn.dataset.panel).classList.add('active');
|
||||||
if (btn.dataset.panel === 'tab-reports') renderReports();
|
if (btn.dataset.panel === 'tab-reports') renderReports();
|
||||||
if (btn.dataset.panel === 'tab-control') renderControl();
|
if (btn.dataset.panel === 'tab-control') renderControl();
|
||||||
|
if (btn.dataset.panel === 'tab-norms') renderNorms();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -832,6 +1000,7 @@ function openEmployeeModal(emp) {
|
|||||||
document.getElementById('empPosition').value = emp ? emp.position : '';
|
document.getElementById('empPosition').value = emp ? emp.position : '';
|
||||||
document.getElementById('empDepartment').value = emp ? emp.department : '';
|
document.getElementById('empDepartment').value = emp ? emp.department : '';
|
||||||
document.getElementById('empDateHired').value = emp ? emp.dateHired : '';
|
document.getElementById('empDateHired').value = emp ? emp.dateHired : '';
|
||||||
|
document.getElementById('empClimateZone').value = emp ? (emp.climateZone || '0') : '0';
|
||||||
openModal('employeeModal');
|
openModal('employeeModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -845,6 +1014,7 @@ function saveEmployee() {
|
|||||||
fullName: name,
|
fullName: name,
|
||||||
position: document.getElementById('empPosition').value.trim(),
|
position: document.getElementById('empPosition').value.trim(),
|
||||||
department: document.getElementById('empDepartment').value.trim(),
|
department: document.getElementById('empDepartment').value.trim(),
|
||||||
|
climateZone: document.getElementById('empClimateZone').value || '0',
|
||||||
dateHired: document.getElementById('empDateHired').value,
|
dateHired: document.getElementById('empDateHired').value,
|
||||||
};
|
};
|
||||||
let list = DB.employees;
|
let list = DB.employees;
|
||||||
@ -1032,7 +1202,170 @@ function renderWarehouse() {
|
|||||||
document.getElementById('whEmpty').style.display = list.length ? 'none' : 'block';
|
document.getElementById('whEmpty').style.display = list.length ? 'none' : 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== ISSUANCES =====================
|
// ===================== 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() {
|
function fillEmployeeSelect() {
|
||||||
const sel = document.getElementById('issEmployeeId');
|
const sel = document.getElementById('issEmployeeId');
|
||||||
sel.innerHTML = '<option value="">— Выберите —</option>' +
|
sel.innerHTML = '<option value="">— Выберите —</option>' +
|
||||||
@ -1045,11 +1378,13 @@ function openIssuanceModal(iss) {
|
|||||||
document.getElementById('issQuantity').value = iss ? iss.quantity : '1';
|
document.getElementById('issQuantity').value = iss ? iss.quantity : '1';
|
||||||
document.getElementById('issDate').value = iss ? iss.dateIssued : todayStr();
|
document.getElementById('issDate').value = iss ? iss.dateIssued : todayStr();
|
||||||
document.getElementById('issNotes').value = iss ? iss.notes : '';
|
document.getElementById('issNotes').value = iss ? iss.notes : '';
|
||||||
|
document.getElementById('issNormHint').style.display = 'none';
|
||||||
fillEmployeeSelect();
|
fillEmployeeSelect();
|
||||||
fillSizSelect('issSizId', '— Выберите СИЗ —');
|
fillSizSelect('issSizId', '— Выберите СИЗ —');
|
||||||
if (iss) {
|
if (iss) {
|
||||||
document.getElementById('issEmployeeId').value = iss.employeeId;
|
document.getElementById('issEmployeeId').value = iss.employeeId;
|
||||||
document.getElementById('issSizId').value = iss.sizId;
|
document.getElementById('issSizId').value = iss.sizId;
|
||||||
|
showNormsForEmployee();
|
||||||
}
|
}
|
||||||
openModal('issuanceModal');
|
openModal('issuanceModal');
|
||||||
}
|
}
|
||||||
@ -1281,6 +1616,17 @@ function exportAllToExcel() {
|
|||||||
const ws4 = XLSX.utils.json_to_sheet(iss);
|
const ws4 = XLSX.utils.json_to_sheet(iss);
|
||||||
XLSX.utils.book_append_sheet(wb, ws4, 'Выдача СИЗ');
|
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');
|
XLSX.writeFile(wb, 'Учет_СИЗ_' + new Date().toISOString().slice(0, 10) + '.xlsx');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1297,6 +1643,7 @@ seedDemo();
|
|||||||
renderEmployees();
|
renderEmployees();
|
||||||
renderSiz();
|
renderSiz();
|
||||||
renderWarehouse();
|
renderWarehouse();
|
||||||
|
renderNorms();
|
||||||
renderIssuances();
|
renderIssuances();
|
||||||
renderControl();
|
renderControl();
|
||||||
renderReports();
|
renderReports();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user