feat: signature/stamp library with roles - whose sig, whose stamp, multi-party docs

This commit is contained in:
WASRUSGEN 2026-05-30 15:12:02 +03:00
parent 738dfb28dd
commit 6bd71a906f

View File

@ -2414,10 +2414,72 @@ body{font-family:var(--font-ui);background:var(--surf);color:var(--ink);line-hei
<!-- Реквизиты — подпись и печать --> <!-- Реквизиты — подпись и печать -->
<div class="tabpane" id="p-requisites"><div class="main-body"> <div class="tabpane" id="p-requisites"><div class="main-body">
<div class="crumb">Кабинет</div><h1>Реквизиты</h1> <div class="crumb">Кабинет</div>
<div class="enote"><img src="logos/elena-photo.jpg"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<div class="et"><b>Подпись и печать автоматически подставляются в документы.</b> Загрузите один раз — и все документы будут подписаны 💛</div> <h1 style="margin:0">Реквизиты</h1>
<button class="btn btn-p" style="padding:7px 14px;font-size:13px" onclick="_addSignatureModal()">+ Добавить</button>
</div> </div>
<div class="enote" style="margin-bottom:20px"><img src="logos/elena-photo.jpg">
<div class="et"><b>База подписей и печатей.</b> Для каждой стороны документа выбирается нужная. Загрузите один раз — подставляются автоматически.</div>
</div>
<!-- Библиотека подписей -->
<div style="margin-bottom:28px">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);margin-bottom:10px">Подписи</div>
<div id="sig-library" style="display:flex;flex-direction:column;gap:8px">
<!-- Заполняется через _renderSigLibrary() -->
</div>
</div>
<!-- Библиотека печатей -->
<div style="margin-bottom:28px">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);margin-bottom:10px">Печати и штампы</div>
<div id="stamp-library" style="display:flex;flex-direction:column;gap:8px">
<!-- Заполняется через _renderStampLibrary() -->
</div>
</div>
<hr style="border:none;border-top:1px solid var(--line);margin:20px 0">
<!-- Реквизиты компании -->
<div>
<h3 style="font-size:15px;font-weight:700;margin-bottom:12px">Реквизиты для документов</h3>
<div style="display:flex;flex-direction:column;gap:10px;max-width:500px">
<input class="elena-main-inp" id="req-name" placeholder="Полное наименование (ООО «...» / ИП ФИО)" oninput="_saveRequisites()">
<input class="elena-main-inp" id="req-inn" placeholder="ИНН / ОГРН" oninput="_saveRequisites()">
<input class="elena-main-inp" id="req-addr" placeholder="Юридический адрес" oninput="_saveRequisites()">
<input class="elena-main-inp" id="req-phone" placeholder="Телефон" oninput="_saveRequisites()">
<input class="elena-main-inp" id="req-email" placeholder="Email" oninput="_saveRequisites()">
</div>
</div>
<!-- Скрытые legacy-элементы для совместимости -->
<div style="display:none">
<div id="sig-preview"></div><img id="sig-img">
<div id="stamp-preview"></div><img id="stamp-img">
<div id="stamp-size"></div>
<select id="stamp-type-sel" onchange="localStorage.setItem('zashita_stamp_type',this.value)">
<option value="round">Круглая</option>
<option value="rect_ip">ИП</option>
<option value="rect_ooo">ООО</option>
<option value="triangle">Треугольная</option>
</select>
</div>
<!-- Canvas для рисования подписи -->
<div id="sig-draw-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;align-items:center;justify-content:center">
<div style="background:#fff;border-radius:16px;padding:20px;width:400px">
<div style="font-weight:700;margin-bottom:12px">Нарисуйте подпись</div>
<canvas id="sig-canvas" width="360" height="120" style="border:2px solid var(--line);border-radius:8px;cursor:crosshair;touch-action:none;background:#fff"></canvas>
<div style="display:flex;gap:10px;margin-top:12px">
<button class="btn btn-p" style="flex:1;padding:9px" onclick="_saveSigCanvasToLib()">✅ Сохранить</button>
<button class="btn btn-o" style="padding:9px 14px" onclick="_clearCanvas()">Очистить</button>
<button class="svc-btn-detail" onclick="document.getElementById('sig-draw-modal').style.display='none'">Отмена</button>
</div>
</div>
</div>
</div></div>
<!-- Подпись --> <!-- Подпись -->
<div style="margin-bottom:24px"> <div style="margin-bottom:24px">
@ -4833,17 +4895,18 @@ var STAMP_SIZES = {
triangle: { w: 189, h: 135, label: 'Треугольная 35×50 мм' }, triangle: { w: 189, h: 135, label: 'Треугольная 35×50 мм' },
}; };
function _getRequisiteImages() { function _getRequisiteImages(role) {
var sig = localStorage.getItem('zashita_sig') || null; // Используем библиотеку с привязкой по роли
var stamp = localStorage.getItem('zashita_stamp') || null; var sigImage = _getSigForRole(role);
var stampType = (document.getElementById('stamp-type-sel') || {}).value var stampData = _getStampForRole(role);
|| localStorage.getItem('zashita_stamp_type') || 'round'; return { sig: sigImage, stamp: stampData.image, stampType: stampData.type };
return { sig: sig, stamp: stamp, stampType: stampType };
} }
function _buildSignatureBlock(req, forPrint) { function _buildSignatureBlock(req, forPrint, fromName, toName) {
// req = {sig, stamp, stampType} // req = {sig, stamp, stampType}
if (!req.sig && !req.stamp) return ''; if (!req.sig && !req.stamp) return '';
fromName = fromName || ((_tplCurrent && _tplCurrent.parties && _tplCurrent.parties.from_name) || '');
toName = toName || ((_tplCurrent && _tplCurrent.parties && _tplCurrent.parties.to_name) || '');
var stampSize = STAMP_SIZES[req.stampType] || STAMP_SIZES.round; var stampSize = STAMP_SIZES[req.stampType] || STAMP_SIZES.round;
// При печати: px → mm (96dpi: 1mm ≈ 3.78px) // При печати: px → mm (96dpi: 1mm ≈ 3.78px)
@ -4856,19 +4919,18 @@ function _buildSignatureBlock(req, forPrint) {
'padding-top:' + (forPrint ? '6mm' : '16px') + ';' + 'padding-top:' + (forPrint ? '6mm' : '16px') + ';' +
'border-top:1px solid ' + (forPrint ? '#ccc' : 'var(--line)') + '">'; 'border-top:1px solid ' + (forPrint ? '#ccc' : 'var(--line)') + '">';
// Подпись слева // Подпись + печать — наша сторона (слева)
html += '<div style="display:flex;flex-direction:column;gap:6px;min-width:180px">';
if (fromName) html += '<div style="font-size:' + (forPrint?'9pt':'12px') + ';font-weight:600;color:#374151">' + fromName + '</div>';
if (req.sig) { if (req.sig) {
html += '<div style="display:flex;flex-direction:column;align-items:flex-start;gap:4px">' + html += '<div style="font-size:' + (forPrint?'8pt':'11px') + ';color:#9ca3af;margin-bottom:2px">Подпись</div>' +
'<div style="font-size:' + (forPrint ? '8pt' : '11px') + ';color:#9ca3af">Подпись</div>' + '<img src="' + req.sig + '" style="height:' + sigH + ';max-width:180px;object-fit:contain">';
'<img src="' + req.sig + '" style="height:' + sigH + ';max-width:200px;object-fit:contain">' +
'</div>';
} else {
html += '<div></div>';
} }
html += '</div>';
// Печать справа // Печать — отдельный блок по центру-справа
if (req.stamp) { if (req.stamp) {
html += '<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px">' + html += '<div style="display:flex;flex-direction:column;align-items:center;gap:4px">' +
'<div style="font-size:' + (forPrint?'8pt':'11px') + ';color:#9ca3af">М.П.</div>' + '<div style="font-size:' + (forPrint?'8pt':'11px') + ';color:#9ca3af">М.П.</div>' +
'<img src="' + req.stamp + '" style="width:' + stW + ';height:' + stH + ';object-fit:contain">' + '<img src="' + req.stamp + '" style="width:' + stW + ';height:' + stH + ';object-fit:contain">' +
'</div>'; '</div>';
@ -7965,7 +8027,11 @@ function tab(name){
document.querySelectorAll('.tabpane').forEach(p=>p.classList.toggle('on',p.id==='p-'+name)); document.querySelectorAll('.tabpane').forEach(p=>p.classList.toggle('on',p.id==='p-'+name));
if(name==='sroki' && typeof renderDeadlines==='function') renderDeadlines(); if(name==='sroki' && typeof renderDeadlines==='function') renderDeadlines();
if(name==='shab' && typeof renderContextTemplates==='function') renderContextTemplates(); if(name==='shab' && typeof renderContextTemplates==='function') renderContextTemplates();
if(name==='requisites' && typeof _loadRequisites==='function') _loadRequisites(); if(name==='requisites') {
if(typeof _loadRequisites==='function') _loadRequisites();
if(typeof _renderSigLibrary==='function') _renderSigLibrary();
if(typeof _renderStampLibrary==='function') _renderStampLibrary();
}
if(name==='casemap' && typeof renderCaseMap==='function') renderCaseMap(); if(name==='casemap' && typeof renderCaseMap==='function') renderCaseMap();
if(name==='team' && typeof renderTeamDashboard==='function') renderTeamDashboard(); if(name==='team' && typeof renderTeamDashboard==='function') renderTeamDashboard();
if(name==='docs') { if(name==='docs') {
@ -9567,6 +9633,249 @@ function _toggleDocCheck(key, docId, checked) {
} catch(e){} } catch(e){}
} }
// ── БИБЛИОТЕКА ПОДПИСЕЙ И ПЕЧАТЕЙ ────────────────────────────────────────────
var _SIG_LIB_KEY = 'zashita_sig_library';
var _STAMP_LIB_KEY = 'zashita_stamp_library';
function _getSigLib() { try { return JSON.parse(localStorage.getItem(_SIG_LIB_KEY) || '[]'); } catch(e){ return []; } }
function _getStampLib() { try { return JSON.parse(localStorage.getItem(_STAMP_LIB_KEY) || '[]'); } catch(e){ return []; } }
function _saveSigLib(lib) { try { localStorage.setItem(_SIG_LIB_KEY, JSON.stringify(lib)); } catch(e){} }
function _saveStampLib(lib) { try { localStorage.setItem(_STAMP_LIB_KEY, JSON.stringify(lib)); } catch(e){} }
// Рендер библиотеки подписей
function _renderSigLibrary() {
var el = document.getElementById('sig-library'); if (!el) return;
var lib = _getSigLib();
if (!lib.length) {
el.innerHTML = '<div style="font-size:13px;color:var(--mut);padding:12px 0">Нет сохранённых подписей. Нажмите «+ Добавить».</div>';
return;
}
el.innerHTML = lib.map(function(s, i) {
return '<div style="display:flex;align-items:center;gap:12px;padding:12px;border:1.5px solid ' +
(s.isDefault ? 'var(--bg)' : 'var(--line)') + ';border-radius:12px;background:' +
(s.isDefault ? 'var(--bg-light)' : '#fff') + '">' +
'<img src="' + s.image + '" style="height:40px;max-width:120px;object-fit:contain">' +
'<div style="flex:1">' +
'<div style="font-weight:600;font-size:13px">' + s.label + '</div>' +
'<div style="font-size:11px;color:var(--mut)">' + (s.role || 'Без привязки к роли') + '</div>' +
'</div>' +
'<div style="display:flex;gap:6px">' +
(!s.isDefault ? '<button class="svc-btn-detail" style="font-size:11px" onclick="_setSigDefault(' + i + ')">По умолч.</button>' : '<span style="font-size:11px;color:var(--bg);font-weight:700">✓ По умолч.</span>') +
'<button class="svc-btn-detail" style="font-size:11px;color:#dc2626" onclick="_deleteSig(' + i + ')">Удалить</button>' +
'</div>' +
'</div>';
}).join('');
}
// Рендер библиотеки печатей
function _renderStampLibrary() {
var el = document.getElementById('stamp-library'); if (!el) return;
var lib = _getStampLib();
if (!lib.length) {
el.innerHTML = '<div style="font-size:13px;color:var(--mut);padding:12px 0">Нет сохранённых печатей. Нажмите «+ Добавить».</div>';
return;
}
el.innerHTML = lib.map(function(s, i) {
var sz = STAMP_SIZES[s.stampType] || STAMP_SIZES.round;
return '<div style="display:flex;align-items:center;gap:12px;padding:12px;border:1.5px solid ' +
(s.isDefault ? 'var(--bg)' : 'var(--line)') + ';border-radius:12px;background:' +
(s.isDefault ? 'var(--bg-light)' : '#fff') + '">' +
'<img src="' + s.image + '" style="height:48px;max-width:60px;object-fit:contain">' +
'<div style="flex:1">' +
'<div style="font-weight:600;font-size:13px">' + s.label + '</div>' +
'<div style="font-size:11px;color:var(--mut)">' + sz.label + ' · ' + (s.role || 'Без привязки') + '</div>' +
'</div>' +
'<div style="display:flex;gap:6px">' +
(!s.isDefault ? '<button class="svc-btn-detail" style="font-size:11px" onclick="_setStampDefault(' + i + ')">По умолч.</button>' : '<span style="font-size:11px;color:var(--bg);font-weight:700">✓ По умолч.</span>') +
'<button class="svc-btn-detail" style="font-size:11px;color:#dc2626" onclick="_deleteStamp(' + i + ')">Удалить</button>' +
'</div>' +
'</div>';
}).join('');
}
// Модал добавления подписи/печати
function _addSignatureModal() {
var old = document.getElementById('add-sig-modal'); if (old) old.remove();
var modal = document.createElement('div');
modal.id = 'add-sig-modal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:flex-end;justify-content:center';
modal.innerHTML =
'<div style="background:#fff;border-radius:18px 18px 0 0;width:100%;max-width:540px;padding:24px">' +
'<div style="font-weight:700;font-size:15px;margin-bottom:16px">Добавить подпись или печать</div>' +
'<div style="display:flex;flex-direction:column;gap:10px;margin-bottom:16px">' +
'<input class="elena-main-inp" id="add-sig-label" placeholder="Название: «ИП Соколов А.В.», «Директор ООО» и т.п.">' +
'<input class="elena-main-inp" id="add-sig-role" placeholder="Роль в документах: «Арендатор», «Исполнитель», «Продавец»...">' +
'<select id="add-sig-type" style="border:1.5px solid var(--line);border-radius:9px;padding:9px 12px;font-size:13px;font-family:inherit;color:var(--ink)">' +
'<option value="signature">Подпись (факсимиле)</option>' +
'<option value="stamp_round">Печать круглая ⌀ 40 мм</option>' +
'<option value="stamp_rect_ip">Печать ИП прямоугольная 38×70 мм</option>' +
'<option value="stamp_rect_ooo">Печать ООО прямоугольная 38×58 мм</option>' +
'<option value="stamp_triangle">Печать треугольная 35×50 мм</option>' +
'</select>' +
'</div>' +
'<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px">' +
'<label style="cursor:pointer">' +
'<input type="file" accept="image/*" style="display:none" onchange="_addFromFile(this)">' +
'<span class="btn btn-p" style="padding:9px 16px;font-size:13px">📷 Загрузить файл</span>' +
'</label>' +
'<button class="btn btn-o" style="padding:9px 16px;font-size:13px" onclick="_addFromDraw()">✏️ Нарисовать</button>' +
'<button class="svc-btn-detail" onclick="document.getElementById(\'add-sig-modal\').remove()">Отмена</button>' +
'</div>' +
'<div id="add-sig-preview" style="display:none;padding:10px;background:#f9fafb;border-radius:10px;text-align:center;margin-bottom:12px">' +
'<img id="add-sig-img" style="max-height:80px;max-width:200px;object-fit:contain">' +
'</div>' +
'<button id="add-sig-save-btn" class="btn btn-p" style="display:none;width:100%;padding:11px;font-size:14px" onclick="_saveNewItem()">✅ Сохранить</button>' +
'</div>';
document.body.appendChild(modal);
setTimeout(function(){ var i=document.getElementById('add-sig-label'); if(i) i.focus(); }, 200);
}
var _pendingItemImage = null;
function _addFromFile(input) {
var file = input.files[0]; if (!file) return;
var reader = new FileReader();
reader.onload = function(e) {
var img = new Image();
img.onload = function() {
var canvas = document.createElement('canvas');
canvas.width = img.width; canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
var d = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (var i = 0; i < d.data.length; i += 4) {
if (d.data[i] > 220 && d.data[i+1] > 220 && d.data[i+2] > 220) d.data[i+3] = 0;
}
ctx.putImageData(d, 0, 0);
_pendingItemImage = canvas.toDataURL('image/png');
var prev = document.getElementById('add-sig-preview');
var img2 = document.getElementById('add-sig-img');
var btn = document.getElementById('add-sig-save-btn');
if (prev) prev.style.display = '';
if (img2) img2.src = _pendingItemImage;
if (btn) btn.style.display = '';
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function _addFromDraw() {
document.getElementById('add-sig-modal').remove();
var drawModal = document.getElementById('sig-draw-modal');
if (drawModal) { drawModal.style.display = 'flex'; _initSigCanvas(); }
}
function _saveSigCanvasToLib() {
var c = document.getElementById('sig-canvas');
if (!c) return;
_pendingItemImage = c.toDataURL('image/png');
var drawModal = document.getElementById('sig-draw-modal');
if (drawModal) drawModal.style.display = 'none';
_addSignatureModal();
setTimeout(function(){
var prev = document.getElementById('add-sig-preview');
var img2 = document.getElementById('add-sig-img');
var btn = document.getElementById('add-sig-save-btn');
if (prev) prev.style.display = '';
if (img2) img2.src = _pendingItemImage;
if (btn) btn.style.display = '';
}, 100);
}
function _saveNewItem() {
if (!_pendingItemImage) { toast('Загрузите или нарисуйте изображение'); return; }
var label = (document.getElementById('add-sig-label') || {}).value || 'Без названия';
var role = (document.getElementById('add-sig-role') || {}).value || '';
var type = (document.getElementById('add-sig-type') || {}).value || 'signature';
var isStamp = type.startsWith('stamp_');
var stampType = isStamp ? type.replace('stamp_', '') : null;
if (isStamp) {
var lib = _getStampLib();
lib.push({ label: label, role: role, stampType: stampType, image: _pendingItemImage, isDefault: lib.length === 0 });
_saveStampLib(lib);
// Обратная совместимость — первый штамп → legacy key
if (lib.length === 1) { localStorage.setItem('zashita_stamp', _pendingItemImage); localStorage.setItem('zashita_stamp_type', stampType); }
} else {
var lib2 = _getSigLib();
lib2.push({ label: label, role: role, image: _pendingItemImage, isDefault: lib2.length === 0 });
_saveSigLib(lib2);
// Обратная совместимость
if (lib2.length === 1) localStorage.setItem('zashita_sig', _pendingItemImage);
}
_pendingItemImage = null;
document.getElementById('add-sig-modal').remove();
_renderSigLibrary();
_renderStampLibrary();
toast('✅ Сохранено');
}
function _setSigDefault(idx) {
var lib = _getSigLib();
lib.forEach(function(s,i){ s.isDefault = (i===idx); });
_saveSigLib(lib);
// Обновляем legacy key
if (lib[idx]) localStorage.setItem('zashita_sig', lib[idx].image);
_renderSigLibrary();
}
function _setStampDefault(idx) {
var lib = _getStampLib();
lib.forEach(function(s,i){ s.isDefault = (i===idx); });
_saveStampLib(lib);
if (lib[idx]) {
localStorage.setItem('zashita_stamp', lib[idx].image);
localStorage.setItem('zashita_stamp_type', lib[idx].stampType || 'round');
}
_renderStampLibrary();
}
function _deleteSig(idx) {
var lib = _getSigLib(); lib.splice(idx, 1);
if (lib.length && !lib.some(function(s){ return s.isDefault; })) lib[0].isDefault = true;
_saveSigLib(lib);
if (lib[0]) localStorage.setItem('zashita_sig', lib[0].image); else localStorage.removeItem('zashita_sig');
_renderSigLibrary();
}
function _deleteStamp(idx) {
var lib = _getStampLib(); lib.splice(idx, 1);
if (lib.length && !lib.some(function(s){ return s.isDefault; })) lib[0].isDefault = true;
_saveStampLib(lib);
if (lib[0]) localStorage.setItem('zashita_stamp', lib[0].image); else localStorage.removeItem('zashita_stamp');
_renderStampLibrary();
}
// Получить подпись/печать для конкретной стороны документа
function _getSigForRole(role) {
var lib = _getSigLib();
if (!lib.length) return localStorage.getItem('zashita_sig') || null;
// Ищем по роли
if (role) {
var byRole = lib.find(function(s){ return s.role && s.role.toLowerCase().includes(role.toLowerCase()); });
if (byRole) return byRole.image;
}
// По умолчанию
var def = lib.find(function(s){ return s.isDefault; });
return def ? def.image : lib[0].image;
}
function _getStampForRole(role) {
var lib = _getStampLib();
if (!lib.length) return { image: localStorage.getItem('zashita_stamp'), type: localStorage.getItem('zashita_stamp_type') || 'round' };
if (role) {
var byRole = lib.find(function(s){ return s.role && s.role.toLowerCase().includes(role.toLowerCase()); });
if (byRole) return { image: byRole.image, type: byRole.stampType || 'round' };
}
var def = lib.find(function(s){ return s.isDefault; });
var item = def || lib[0];
return { image: item.image, type: item.stampType || 'round' };
}
// ── РЕКВИЗИТЫ — ПОДПИСЬ И ПЕЧАТЬ ──────────────────────────────────────────── // ── РЕКВИЗИТЫ — ПОДПИСЬ И ПЕЧАТЬ ────────────────────────────────────────────
function _uploadRequisite(type, input) { function _uploadRequisite(type, input) {