diff --git a/docs/client_card_demo.html b/docs/client_card_demo.html
index fded419..f70d4ce 100644
--- a/docs/client_card_demo.html
+++ b/docs/client_card_demo.html
@@ -211,6 +211,9 @@ body{font-family:'Inter',sans-serif;background:#F5F6F8;color:#1A1A2E;height:100v
🕐История
+
+ 💰Финансы
+
@@ -524,6 +528,87 @@ body{font-family:'Inter',sans-serif;background:#F5F6F8;color:#1A1A2E;height:100v
+
+
+
+
+
+
+ Договор
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ График платежей
+
+
+
Договор ещё не отправлен клиенту
+
+
+
+
+
Критерии приёмки услуг
+
+
+
1️⃣
+
Диагностика AS-IS
Передан отчёт + клиент подтвердил получение
+
+
+
2️⃣
+
Аудит + TO-BE
Согласован план внедрения (дорожная карта)
+
+
+
3️⃣
+
Внедрение
Подписан итоговый Акт об оказанных услугах
+
+
+
🔄
+
Сопровождение
Ежемесячный мини-акт по итогам периода
+
+
+ ⚖️ По п. 3.4 договора: при отсутствии мотивированного отказа в течение 5 рабочих дней с момента направления Акта — услуги считаются принятыми.
+
+
+
+
+
+
@@ -664,7 +749,7 @@ function toggleWorkFormat(){
toggleWorkFormat();
// ── Tabs ─────────────────────────────────────────────────────────────────────
-const TAB_IDS = ['work','interview','details','history'];
+const TAB_IDS = ['work','interview','details','history','finance'];
function switchTab(id){
TAB_IDS.forEach(t=>{
document.getElementById('tab-'+t).classList.toggle('active', t===id);
@@ -672,6 +757,7 @@ function switchTab(id){
const navItem = document.querySelector(`.nav-item[onclick*="'${t}'"]`);
if(navItem) navItem.classList.toggle('active', t===id);
});
+ if(id==='finance') loadFinances();
}
// ── Collapse (passport) ──────────────────────────────────────────────────────
@@ -950,6 +1036,145 @@ async function runSuggest(){
btn.disabled=false;btn.textContent='✨ Сформировать';
}
+// ── Finances ──────────────────────────────────────────────────────────────────
+const TARIFF_DEFAULTS = {express:0, audit:150000, impl:350000};
+const TARIFF_NAMES = {express:'Экспресс-диагностика', audit:'Полный аудит + план', impl:'Внедрение под ключ'};
+
+function onTariffChange(){
+ const t = document.getElementById('fin-tariff').value;
+ document.getElementById('fin-price').value = TARIFF_DEFAULTS[t] ?? 0;
+}
+
+async function loadFinances(){
+ try{
+ const r = await fetch('/consult/api/admin/finances/'+CODE, {credentials:'include'});
+ if(!r.ok) return;
+ const d = await r.json();
+ renderFinances(d);
+ }catch(e){}
+}
+
+function fmtRub(n){ return n===0?'0 ₽':(n.toLocaleString('ru')+' ₽'); }
+
+function renderFinances(d){
+ const c = d.contract;
+ const chip = document.getElementById('contract-chip');
+ const signInfo = document.getElementById('contract-sign-info');
+
+ // Contract chip
+ const statusMap = {
+ draft: {bg:'#F1F5F9',color:'#64748B',text:'Черновик'},
+ sent: {bg:'#FEF3C7',color:'#D97706',text:'📨 Отправлен'},
+ signed: {bg:'#ECFDF5',color:'#047857',text:'✅ Подписан'},
+ };
+ if(c){
+ const s = statusMap[c.status] || statusMap.draft;
+ chip.innerHTML = `${s.text}`;
+ // Prefill form
+ if(c.tariff) document.getElementById('fin-tariff').value = c.tariff;
+ if(c.price) document.getElementById('fin-price').value = c.price;
+ // Signed info
+ if(c.status === 'signed' && c.signed_at){
+ signInfo.style.display = 'block';
+ signInfo.innerHTML = `✅ Подписан: ${fmtTime(c.signed_at)} · IP: ${c.sign_ip||'—'}`;
+ } else {
+ signInfo.style.display = 'none';
+ }
+ } else {
+ chip.innerHTML = `Не создан`;
+ }
+
+ // Payments
+ const payList = document.getElementById('payments-list');
+ const payChip = document.getElementById('pay-summary-chip');
+ if(!d.payments || !d.payments.length){
+ payList.innerHTML = 'Договор ещё не отправлен клиенту — график платежей сформируется автоматически
';
+ payChip.textContent = '';
+ return;
+ }
+ const paidColor = d.balance===0 ? '#047857' : '#D97706';
+ payChip.textContent = `${fmtRub(d.paid)} из ${fmtRub(d.total)}`;
+ payChip.style.color = paidColor;
+
+ payList.innerHTML = `
+
+
+
Этап
+
Сумма
+
Оплачено
+
Статус
+
Действие
+
+ ${d.payments.map((p,i)=>{
+ const last = i===d.payments.length-1;
+ const isPaid = p.status==='paid';
+ const rowBg = isPaid ? '#F0FDF4' : '#fff';
+ const statusBadge = isPaid
+ ? '
✓ Оплачен'
+ : '
Ожидает';
+ const border = last?'':'border-bottom:1px solid #E5E7EB';
+ const action = isPaid
+ ? `
`
+ : `
`;
+ const noteSpan = p.note ? `
${p.note}
` : '';
+ return `
+
${p.phase_label}
${noteSpan}${p.paid_at?`
Оплачен: ${fmtTime(p.paid_at)}
`:''}
+
${fmtRub(p.amount)}
+
${fmtRub(p.paid_amount)}
+
${statusBadge}
+
${action}
+
`;
+ }).join('')}
+
+
ИТОГО
+
${fmtRub(d.total)}
+
${fmtRub(d.paid)}
+
${d.balance===0?'✅ Закрыт':'−'+fmtRub(d.balance)}
+
+
+
`;
+}
+
+async function saveContract(action='save'){
+ const tariff = document.getElementById('fin-tariff').value;
+ const price = parseInt(document.getElementById('fin-price').value)||0;
+ const msg = document.getElementById('contract-send-msg');
+ msg.textContent = action==='send'?'Отправляю…':'Сохраняю…'; msg.style.color='#94A3B8';
+ try{
+ const r = await fetch('/consult/api/admin/finances/'+CODE+'/contract', {
+ method:'POST', credentials:'include',
+ headers:{'Content-Type':'application/json'},
+ body:JSON.stringify({tariff, tariff_name: TARIFF_NAMES[tariff]||tariff, price, action})
+ });
+ const d = await r.json();
+ if(d.ok){
+ msg.textContent = action==='send'?'✓ Договор отправлен клиенту':'✓ Сохранено'; msg.style.color='#059669';
+ setTimeout(()=>{msg.textContent='';}, 3000);
+ await loadFinances();
+ } else { msg.textContent='Ошибка'; msg.style.color='#DC2626'; }
+ }catch(e){ msg.textContent='Ошибка подключения'; msg.style.color='#DC2626'; }
+}
+function sendContract(){
+ if(!confirm('Отправить договор клиенту? Будет сформирован график платежей и клиент получит доступ к документу.')) return;
+ saveContract('send');
+}
+
+async function markPayment(payId, amount){
+ await fetch('/consult/api/admin/finances/'+CODE+'/payment/'+payId, {
+ method:'PATCH', credentials:'include',
+ headers:{'Content-Type':'application/json'},
+ body:JSON.stringify({paid_amount: amount})
+ });
+ await loadFinances();
+}
+
+function openMarkPayModal(payId, amount){
+ const entered = prompt(`Введите оплаченную сумму (₽):\nПлановая: ${fmtRub(amount)}`, amount);
+ if(entered===null) return;
+ const paid = parseInt(entered)||0;
+ markPayment(payId, paid);
+}
+
// ── Save ──────────────────────────────────────────────────────────────────────
async function saveCard(){
const status=document.getElementById('save-status');