From 88c69e36e63f21a6af9370501fc7f02b0be3f813 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Thu, 28 May 2026 17:00:02 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20add=20=D0=A4=D0=B8=D0=BD=D0=B0=D0=BD?= =?UTF-8?q?=D1=81=D1=8B=20tab=20(contract=20signing=20+=20payment=20tracki?= =?UTF-8?q?ng)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/client_card_demo.html | 227 ++++++++++++++++++++++++++++++++++++- 1 file changed, 226 insertions(+), 1 deletion(-) 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');