mirror of
https://github.com/wasrusgen/zashita-brandbook.git
synced 2026-06-03 15:04:49 +00:00
feat: case map tab + send doc to counterparty + auto-log all events
This commit is contained in:
parent
a0fc4d92ab
commit
a8fcc20745
230
mockup.html
230
mockup.html
@ -1726,6 +1726,7 @@ body{font-family:var(--font-ui);background:var(--surf);color:var(--ink);line-hei
|
|||||||
<a id="t-shab" onclick="tab('shab')">📋 Шаблоны</a>
|
<a id="t-shab" onclick="tab('shab')">📋 Шаблоны</a>
|
||||||
<a id="t-create" onclick="tab('create')">✍️ Составить документ</a>
|
<a id="t-create" onclick="tab('create')">✍️ Составить документ</a>
|
||||||
<a id="t-docs" onclick="tab('docs')">✅ Мои документы</a>
|
<a id="t-docs" onclick="tab('docs')">✅ Мои документы</a>
|
||||||
|
<a id="t-casemap" onclick="tab('casemap')">📝 Карта дела</a>
|
||||||
<a id="t-requisites" onclick="tab('requisites')">🖊️ Реквизиты</a>
|
<a id="t-requisites" onclick="tab('requisites')">🖊️ Реквизиты</a>
|
||||||
<a id="t-balance" onclick="tab('balance')">💳 Баланс и оплата</a>
|
<a id="t-balance" onclick="tab('balance')">💳 Баланс и оплата</a>
|
||||||
<a onclick="go('start')">↩ Выйти (в начало)</a>
|
<a onclick="go('start')">↩ Выйти (в начало)</a>
|
||||||
@ -2267,6 +2268,24 @@ body{font-family:var(--font-ui);background:var(--surf);color:var(--ink);line-hei
|
|||||||
</div>
|
</div>
|
||||||
</div></div>
|
</div></div>
|
||||||
|
|
||||||
|
<!-- Карта дела — примечания, риски, обещания -->
|
||||||
|
<div class="tabpane" id="p-casemap"><div class="main-body">
|
||||||
|
<div class="crumb">Кабинет</div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
||||||
|
<h1 style="margin:0">📝 Карта дела</h1>
|
||||||
|
<button class="svc-btn-detail" style="font-size:12px" onclick="_exportCaseMap()">📄 Скачать PDF</button>
|
||||||
|
</div>
|
||||||
|
<div class="enote" style="margin-bottom:20px"><img src="logos/elena-photo.jpg">
|
||||||
|
<div class="et">Здесь собрано всё что важно знать по делу: принятые решения, зафиксированные риски, обещания по документам. <b>Это приложение к делу — не сам документ.</b></div>
|
||||||
|
</div>
|
||||||
|
<div id="casemap-content">
|
||||||
|
<!-- Заполняется через renderCaseMap() -->
|
||||||
|
<div style="text-align:center;padding:32px;color:var(--mut)">
|
||||||
|
Начните работу с Еленой — карта дела заполнится автоматически.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
|
||||||
<!-- Реквизиты — подпись и печать -->
|
<!-- Реквизиты — подпись и печать -->
|
||||||
<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><h1>Реквизиты</h1>
|
||||||
@ -4694,8 +4713,10 @@ function _showGeneratedDoc(data) {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<div style="padding:14px 20px;border-top:1px solid var(--line);display:flex;gap:10px;flex-wrap:wrap">' +
|
'<div style="padding:14px 20px;border-top:1px solid var(--line);display:flex;gap:10px;flex-wrap:wrap">' +
|
||||||
'<button class="btn btn-p" style="padding:9px 18px;font-size:13px" onclick="_printDoc()">🖨️ Распечатать</button>' +
|
'<button class="btn btn-p" style="padding:9px 18px;font-size:13px" onclick="_printDoc()">🖨️ Распечатать</button>' +
|
||||||
'<button class="btn btn-o" style="padding:9px 18px;font-size:13px" onclick="_copyDoc()">📋 Скопировать</button>' +
|
'<button class="btn btn-o" style="padding:9px 18px;font-size:13px" onclick="_sendDocToCounterparty()">📨 Отправить контрагенту</button>' +
|
||||||
'<button class="svc-btn-detail" style="font-size:13px" onclick="_startTemplate(_tplCurrent&&_tplCurrent.key)">✏️ Изменить данные</button>' +
|
'<button class="btn btn-o" style="padding:9px 18px;font-size:13px" onclick="_copyDoc()">📋 Скопировать текст</button>' +
|
||||||
|
'<button class="svc-btn-detail" style="font-size:13px" onclick="_startTemplate(_tplCurrent&&_tplCurrent.key)">✏️ Изменить</button>' +
|
||||||
|
'<button class="svc-btn-detail" style="font-size:13px" onclick="document.getElementById(\'tpl-result\').remove();tab(\'casemap\');go(\'cabinet\')">📝 Карта дела</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
@ -4717,6 +4738,74 @@ function _printDoc() {
|
|||||||
w.document.close();
|
w.document.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _sendDocToCounterparty() {
|
||||||
|
var textEl = document.getElementById('tpl-doc-text');
|
||||||
|
if (!textEl) return;
|
||||||
|
|
||||||
|
var text = textEl.textContent || textEl.innerText || '';
|
||||||
|
var postalData = typeof _postalData !== 'undefined' ? _postalData : null;
|
||||||
|
var toEmail = (postalData && postalData.counterEmail) || '';
|
||||||
|
var toName = (postalData && postalData.counterparty) || 'Контрагенту';
|
||||||
|
|
||||||
|
// Закрываем модал документа
|
||||||
|
var modal = document.getElementById('tpl-result');
|
||||||
|
if (modal) modal.remove();
|
||||||
|
|
||||||
|
// Показываем модал отправки
|
||||||
|
var sendModal = document.createElement('div');
|
||||||
|
sendModal.id = 'send-doc-modal';
|
||||||
|
sendModal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:flex-end;justify-content:center';
|
||||||
|
sendModal.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:6px">📨 Отправить контрагенту</div>' +
|
||||||
|
'<div style="font-size:13px;color:var(--mut);margin-bottom:16px">Документ будет отправлен по email или вы скопируете ссылку</div>' +
|
||||||
|
'<div style="display:flex;flex-direction:column;gap:10px;margin-bottom:16px">' +
|
||||||
|
'<input id="send-email" class="elena-main-inp" type="email" placeholder="Email контрагента" value="' + toEmail + '">' +
|
||||||
|
'<input id="send-subject" class="elena-main-inp" placeholder="Тема письма" value="Направляю документ для ознакомления">' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display:flex;gap:8px;flex-wrap:wrap">' +
|
||||||
|
'<button class="btn btn-p" style="flex:1;padding:10px" onclick="_doSendEmail()">📧 Отправить по email</button>' +
|
||||||
|
'<button class="btn btn-o" style="padding:10px 14px" onclick="_copyAndClose()">📋 Скопировать + закрыть</button>' +
|
||||||
|
'<button class="svc-btn-detail" onclick="document.getElementById(\'send-doc-modal\').remove()">Отмена</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="margin-top:12px;font-size:11px;color:var(--mut)">' +
|
||||||
|
'💡 После отправки зафиксируем факт в карте дела — дата, получатель.' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
document.body.appendChild(sendModal);
|
||||||
|
setTimeout(function(){ var e = document.getElementById('send-email'); if (e && !e.value) e.focus(); }, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _doSendEmail() {
|
||||||
|
var email = (document.getElementById('send-email') || {}).value || '';
|
||||||
|
var subject = (document.getElementById('send-subject') || {}).value || 'Документ';
|
||||||
|
var textEl = document.getElementById('tpl-doc-text');
|
||||||
|
var body = textEl ? (textEl.textContent || '') : '';
|
||||||
|
|
||||||
|
// Открываем mailto (браузер откроет почтовый клиент)
|
||||||
|
var mailtoLink = 'mailto:' + encodeURIComponent(email) +
|
||||||
|
'?subject=' + encodeURIComponent(subject) +
|
||||||
|
'&body=' + encodeURIComponent(body.slice(0, 2000) + (body.length > 2000 ? '\n\n...(полный текст скопируйте из приложения)' : ''));
|
||||||
|
|
||||||
|
window.location.href = mailtoLink;
|
||||||
|
|
||||||
|
// Фиксируем в карте дела
|
||||||
|
_addCaseNote('decision',
|
||||||
|
'Документ отправлен: ' + subject + (email ? ' → ' + email : ''),
|
||||||
|
{ email: email, date: new Date().toISOString() });
|
||||||
|
|
||||||
|
document.getElementById('send-doc-modal').remove();
|
||||||
|
toast('✅ Открываю почтовый клиент. Факт отправки зафиксирован в карте дела.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _copyAndClose() {
|
||||||
|
_copyDoc();
|
||||||
|
var m = document.getElementById('send-doc-modal');
|
||||||
|
if (m) m.remove();
|
||||||
|
_addCaseNote('decision', 'Текст документа скопирован для отправки контрагенту', {});
|
||||||
|
toast('📋 Текст скопирован. Вставьте в письмо контрагенту.');
|
||||||
|
}
|
||||||
|
|
||||||
function _copyDoc() {
|
function _copyDoc() {
|
||||||
var text = document.getElementById('tpl-doc-text');
|
var text = document.getElementById('tpl-doc-text');
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
@ -7376,6 +7465,7 @@ function tab(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' && typeof _loadRequisites==='function') _loadRequisites();
|
||||||
|
if(name==='casemap' && typeof renderCaseMap==='function') renderCaseMap();
|
||||||
if(name==='docs') {
|
if(name==='docs') {
|
||||||
var contracts = typeof _getContracts === 'function' ? _getContracts() : [];
|
var contracts = typeof _getContracts === 'function' ? _getContracts() : [];
|
||||||
if (contracts.length && typeof renderDocChecklist === 'function') renderDocChecklist(contracts[0].type);
|
if (contracts.length && typeof renderDocChecklist === 'function') renderDocChecklist(contracts[0].type);
|
||||||
@ -7689,6 +7779,132 @@ function _compressAsync(showToast) {
|
|||||||
.catch(function(){});
|
.catch(function(){});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── КАРТА ДЕЛА ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
var _CASE_NOTES_KEY = 'zashita_case_notes';
|
||||||
|
|
||||||
|
// Добавить заметку в карту дела
|
||||||
|
function _addCaseNote(type, text, meta) {
|
||||||
|
try {
|
||||||
|
var notes = JSON.parse(localStorage.getItem(_CASE_NOTES_KEY) || '[]');
|
||||||
|
notes.push({
|
||||||
|
id: Date.now(),
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
type: type, // 'risk' | 'missing_doc' | 'promise' | 'decision' | 'refusal' | 'acknowledged'
|
||||||
|
text: text,
|
||||||
|
meta: meta || {}
|
||||||
|
});
|
||||||
|
localStorage.setItem(_CASE_NOTES_KEY, JSON.stringify(notes));
|
||||||
|
} catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getCaseNotes() {
|
||||||
|
try { return JSON.parse(localStorage.getItem(_CASE_NOTES_KEY) || '[]'); }
|
||||||
|
catch(e) { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCaseMap() {
|
||||||
|
var el = document.getElementById('casemap-content');
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
var notes = _getCaseNotes();
|
||||||
|
var dossier = _getDossier() || {};
|
||||||
|
|
||||||
|
if (!notes.length && !dossier.facts && !dossier.decisions && !dossier.open) {
|
||||||
|
el.innerHTML = '<div style="text-align:center;padding:32px;color:var(--mut)">Начните работу с Еленой — карта дела заполнится автоматически.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var today = new Date().toLocaleDateString('ru-RU', {day:'2-digit', month:'long', year:'numeric'});
|
||||||
|
var html = '<div style="font-size:12px;color:var(--mut);margin-bottom:20px">Сформировано: ' + today + '</div>';
|
||||||
|
|
||||||
|
// Секция: Принятые решения (из досье)
|
||||||
|
if (dossier.decisions && dossier.decisions.length) {
|
||||||
|
html += _cmSection('✅ Принятые решения', '#16a34a', dossier.decisions.map(function(d){
|
||||||
|
return {text: d, icon: '✅'};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция: Зафиксированные риски
|
||||||
|
var risks = notes.filter(function(n){ return n.type === 'risk' || n.type === 'acknowledged'; });
|
||||||
|
if (risks.length) {
|
||||||
|
html += _cmSection('⚠️ Принятые риски (клиент ознакомлен)', '#d97706', risks.map(function(n){
|
||||||
|
return {text: n.text, icon: n.type === 'acknowledged' ? '👁' : '⚠️', date: n.ts};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция: Отсутствующие документы
|
||||||
|
var missing = notes.filter(function(n){ return n.type === 'missing_doc'; });
|
||||||
|
if (missing.length) {
|
||||||
|
html += _cmSection('📁 Документы отсутствуют', '#dc2626', missing.map(function(n){
|
||||||
|
return {text: n.text, icon: '❌', sub: n.meta.reason || '', date: n.ts};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция: Обещания загрузить
|
||||||
|
var promises = notes.filter(function(n){ return n.type === 'promise'; });
|
||||||
|
if (promises.length) {
|
||||||
|
html += _cmSection('🕐 Обещано загрузить позже', '#2563eb', promises.map(function(n){
|
||||||
|
return {text: n.text, icon: '📋', date: n.ts};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция: Отказы контрагента
|
||||||
|
var refusals = notes.filter(function(n){ return n.type === 'refusal'; });
|
||||||
|
if (refusals.length) {
|
||||||
|
html += _cmSection('🚫 Отказы контрагента', '#7c3aed', refusals.map(function(n){
|
||||||
|
return {text: n.text, icon: '🚫', date: n.ts};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция: Открытые вопросы
|
||||||
|
if (dossier.open && dossier.open.length) {
|
||||||
|
html += _cmSection('❓ Открытые вопросы', '#6b7280', dossier.open.map(function(d){
|
||||||
|
return {text: d, icon: '❓'};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подпись
|
||||||
|
html += '<div style="margin-top:24px;padding-top:16px;border-top:1px solid var(--line);' +
|
||||||
|
'font-size:11px;color:var(--mut);text-align:center">' +
|
||||||
|
'Карта дела сформирована сервисом ЗАЩИТА (@wasrusgen1) · i@wasrusgen.ru · ' + today +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cmSection(title, color, items) {
|
||||||
|
if (!items || !items.length) return '';
|
||||||
|
var html = '<div style="margin-bottom:20px">' +
|
||||||
|
'<div style="font-weight:700;font-size:14px;color:' + color + ';margin-bottom:8px;' +
|
||||||
|
'padding-bottom:6px;border-bottom:2px solid ' + color + '20">' + title + '</div>' +
|
||||||
|
'<div style="display:flex;flex-direction:column;gap:6px">';
|
||||||
|
items.forEach(function(item) {
|
||||||
|
var dateStr = item.date ? ' <span style="color:#9ca3af;font-size:11px">' +
|
||||||
|
new Date(item.date).toLocaleDateString('ru-RU') + '</span>' : '';
|
||||||
|
html += '<div style="padding:8px 12px;background:#f9fafb;border-radius:8px;border-left:3px solid ' + color + '">' +
|
||||||
|
'<span style="margin-right:6px">' + item.icon + '</span>' +
|
||||||
|
'<span style="font-size:13px">' + item.text + '</span>' + dateStr +
|
||||||
|
(item.sub ? '<div style="font-size:12px;color:var(--mut);margin-top:3px">' + item.sub + '</div>' : '') +
|
||||||
|
'</div>';
|
||||||
|
});
|
||||||
|
return html + '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _exportCaseMap() {
|
||||||
|
var el = document.getElementById('casemap-content');
|
||||||
|
if (!el) return;
|
||||||
|
var w = window.open('', '_blank');
|
||||||
|
w.document.write('<html><head><title>Карта дела — ЗАЩИТА</title>' +
|
||||||
|
'<style>body{font-family:Arial,sans-serif;max-width:700px;margin:40px auto;font-size:13px;color:#1a1a2e;line-height:1.6}' +
|
||||||
|
'h1{color:#9f1239}h2{font-size:14px}' +
|
||||||
|
'.section{margin-bottom:20px}.item{padding:8px 12px;background:#f9fafb;border-radius:6px;margin-bottom:4px}' +
|
||||||
|
'</style></head><body>' +
|
||||||
|
'<h1>📝 Карта дела</h1>' + el.innerHTML +
|
||||||
|
'<script>window.print();<\/script></body></html>');
|
||||||
|
w.document.close();
|
||||||
|
}
|
||||||
|
|
||||||
// ── ОПРЕДЕЛЕНИЕ ТИПА ДОГОВОРА ───────────────────────────────────────────────
|
// ── ОПРЕДЕЛЕНИЕ ТИПА ДОГОВОРА ───────────────────────────────────────────────
|
||||||
function _detectContractType(text) {
|
function _detectContractType(text) {
|
||||||
var t = (text || '').toLowerCase();
|
var t = (text || '').toLowerCase();
|
||||||
@ -7879,7 +8095,7 @@ function _handleMissingDoc(contractType, docId, variant, intent) {
|
|||||||
uDiv.innerHTML = '<div class="hc-bubble">' + (labels[variant] || variant) + '</div>';
|
uDiv.innerHTML = '<div class="hc-bubble">' + (labels[variant] || variant) + '</div>';
|
||||||
msgs.appendChild(uDiv);
|
msgs.appendChild(uDiv);
|
||||||
|
|
||||||
// Сохраняем статус
|
// Сохраняем статус + пишем в карту дела
|
||||||
try {
|
try {
|
||||||
var statKey = 'zashita_docstatus_' + contractType;
|
var statKey = 'zashita_docstatus_' + contractType;
|
||||||
var statuses = JSON.parse(localStorage.getItem(statKey) || '{}');
|
var statuses = JSON.parse(localStorage.getItem(statKey) || '{}');
|
||||||
@ -7887,6 +8103,11 @@ function _handleMissingDoc(contractType, docId, variant, intent) {
|
|||||||
localStorage.setItem(statKey, JSON.stringify(statuses));
|
localStorage.setItem(statKey, JSON.stringify(statuses));
|
||||||
} catch(e){}
|
} catch(e){}
|
||||||
|
|
||||||
|
var noteTypeMap = { never:'missing_doc', later:'promise', unknown:'missing_doc', lost:'missing_doc', refused:'refusal' };
|
||||||
|
_addCaseNote(noteTypeMap[variant] || 'missing_doc',
|
||||||
|
doc.label + ' — ' + (labels[variant] || variant),
|
||||||
|
{ docId: docId, contractType: contractType, reason: labels[variant] });
|
||||||
|
|
||||||
// Ответ Елены через API
|
// Ответ Елены через API
|
||||||
var cl = DOC_CHECKLISTS[contractType] || {docs:[]};
|
var cl = DOC_CHECKLISTS[contractType] || {docs:[]};
|
||||||
var doc = cl.docs.find(function(d){ return d.id === docId; }) || {};
|
var doc = cl.docs.find(function(d){ return d.id === docId; }) || {};
|
||||||
@ -8007,8 +8228,9 @@ function _acknowledgeRisksAndProceed(contractType, intent) {
|
|||||||
uDiv.innerHTML = '<div class="hc-bubble">Понимаю риски — продолжаем</div>';
|
uDiv.innerHTML = '<div class="hc-bubble">Понимаю риски — продолжаем</div>';
|
||||||
msgs.appendChild(uDiv);
|
msgs.appendChild(uDiv);
|
||||||
|
|
||||||
// Сохраняем факт подтверждения в досье
|
// Сохраняем факт подтверждения в досье + карту дела
|
||||||
_updateDossier({ decisions: ['Клиент подтвердил осведомлённость о рисках по ' + contractType + ' и продолжил'] });
|
_updateDossier({ decisions: ['Клиент подтвердил осведомлённость о рисках по ' + contractType + ' и продолжил'] });
|
||||||
|
_addCaseNote('acknowledged', 'Клиент ознакомлен с рисками по договору (' + contractType + ') и подтвердил готовность продолжать', { contractType: contractType });
|
||||||
_chatHistory.push({role: 'user', content: 'Понимаю риски — продолжаем'});
|
_chatHistory.push({role: 'user', content: 'Понимаю риски — продолжаем'});
|
||||||
_saveHistory();
|
_saveHistory();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user