mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 19:04:47 +00:00
1210 lines
75 KiB
HTML
1210 lines
75 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>{{code}} · @wasrusgen1 CRM</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@700;800&display=swap" rel="stylesheet">
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:'Inter',sans-serif;background:#F5F6F8;color:#1A1A2E;height:100vh;overflow:hidden}
|
||
|
||
/* ── Layout shell (same as admin) ── */
|
||
.layout{display:flex;height:100vh}
|
||
.sidebar{width:220px;background:#0F0F1A;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
|
||
.sb-top{padding:20px 18px 16px;border-bottom:1px solid rgba(255,255,255,.07)}
|
||
.sb-brand{font-family:'Montserrat',sans-serif;font-size:13px;font-weight:800;color:#fff;letter-spacing:.04em;display:flex;align-items:center;gap:8px}
|
||
.sb-sub{font-size:10px;color:rgba(255,255,255,.35);margin-top:3px;letter-spacing:.02em}
|
||
.sb-nav{flex:1;overflow-y:auto;padding:12px 0;scrollbar-width:none}
|
||
.sb-nav::-webkit-scrollbar{display:none}
|
||
.nav-section{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:rgba(255,255,255,.25);padding:12px 18px 4px}
|
||
.nav-item{display:flex;align-items:center;gap:9px;padding:8px 18px;cursor:pointer;font-size:13px;color:rgba(255,255,255,.55);border:none;background:none;width:100%;text-align:left;font-family:'Inter',sans-serif;transition:.15s;text-decoration:none}
|
||
.nav-item:hover{color:rgba(255,255,255,.85);background:rgba(255,255,255,.05)}
|
||
.nav-item.active{color:#fff;background:rgba(4,120,87,.12);border-left:2px solid #047857;padding-left:16px}
|
||
.ni-icon{font-size:14px;flex-shrink:0;width:18px;text-align:center}
|
||
.ni-label{flex:1}
|
||
.sb-client-box{margin:12px 14px 0;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:10px 12px}
|
||
.sb-client-code{font-family:monospace;font-size:15px;font-weight:700;color:#10B981;letter-spacing:.06em}
|
||
.sb-client-name{font-size:11px;color:rgba(255,255,255,.45);margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.sb-footer{padding:14px 18px;border-top:1px solid rgba(255,255,255,.07)}
|
||
.sb-stat{font-size:11px;color:rgba(255,255,255,.4);line-height:1.8}
|
||
.sb-stat b{color:rgba(255,255,255,.75);font-size:12px}
|
||
|
||
/* ── Content area ── */
|
||
.content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
|
||
.content-hdr{background:#fff;border-bottom:1.5px solid #E5E7EB;padding:0 28px;height:56px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;gap:16px}
|
||
.breadcrumb{display:flex;align-items:center;gap:8px;font-size:13px;color:#94A3B8}
|
||
.breadcrumb a{color:#94A3B8;text-decoration:none;transition:.15s}
|
||
.breadcrumb a:hover{color:#047857}
|
||
.breadcrumb-sep{color:#CBD5E1}
|
||
.breadcrumb-cur{font-weight:700;color:#0f172a;font-family:monospace;font-size:14px}
|
||
.hdr-actions{display:flex;align-items:center;gap:10px}
|
||
.btn-save{background:linear-gradient(135deg,#064E3B,#047857);color:#fff;border:none;padding:8px 22px;border-radius:8px;font-family:'Montserrat',sans-serif;font-size:13px;font-weight:700;cursor:pointer;transition:.2s;white-space:nowrap}
|
||
.btn-save:hover{opacity:.9;transform:translateY(-1px)}
|
||
.btn-contract{background:#F0FDF4;border:1.5px solid #A7F3D0;color:#047857;padding:7px 14px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:6px;text-decoration:none;transition:.2s;white-space:nowrap}
|
||
.btn-contract:hover{background:#DCFCE7}
|
||
.save-status{font-size:12px;color:#94A3B8;transition:.3s;white-space:nowrap}
|
||
.save-status.ok{color:#059669}
|
||
.save-status.err{color:#DC2626}
|
||
.content-body{flex:1;overflow-y:auto;padding:24px 28px 40px}
|
||
.content-body::-webkit-scrollbar{width:4px}
|
||
.content-body::-webkit-scrollbar-thumb{background:#E2E8F0;border-radius:4px}
|
||
|
||
/* ── Hero card ── */
|
||
.hero-card{background:#fff;border-radius:14px;border:1.5px solid #E5E7EB;padding:20px 24px;margin-bottom:20px;display:flex;align-items:center;gap:20px;flex-wrap:wrap}
|
||
.hero-avatar{width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#064E3B,#047857);display:flex;align-items:center;justify-content:center;font-family:'Montserrat',sans-serif;font-size:20px;font-weight:800;color:#fff;flex-shrink:0}
|
||
.hero-main{flex:1;min-width:200px}
|
||
.hero-name{font-family:'Montserrat',sans-serif;font-size:18px;font-weight:800;color:#0f172a;margin-bottom:4px}
|
||
.hero-meta{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||
.hero-code{font-family:monospace;font-size:11px;color:#94A3B8;background:#F8FAFC;border:1px solid #E2E8F0;padding:2px 8px;border-radius:6px}
|
||
.hero-stat{font-size:12px;color:#94A3B8}
|
||
.hero-badges{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px}
|
||
.hero-right{display:flex;flex-direction:column;align-items:flex-end;gap:8px}
|
||
.type-toggle{display:flex;background:#F1F5F9;border:1.5px solid #E2E8F0;border-radius:8px;padding:3px;gap:2px}
|
||
.type-btn{padding:5px 14px;border-radius:5px;border:none;font-family:'Inter',sans-serif;font-size:12px;font-weight:600;cursor:pointer;transition:.2s;background:transparent;color:#64748B}
|
||
.type-btn.active{background:#fff;color:#047857;box-shadow:0 1px 4px rgba(0,0,0,.1)}
|
||
|
||
/* ── Tab navigation ── */
|
||
.tabs-nav{display:flex;gap:2px;background:#fff;border-radius:14px 14px 0 0;border:1.5px solid #E5E7EB;border-bottom:none;padding:6px 6px 0;margin-bottom:0}
|
||
.tab-btn{padding:9px 18px;border-radius:8px 8px 0 0;border:none;font-family:'Inter',sans-serif;font-size:13px;font-weight:600;cursor:pointer;background:transparent;color:#94A3B8;transition:.15s;display:flex;align-items:center;gap:6px;position:relative;bottom:-1.5px}
|
||
.tab-btn:hover{color:#475569;background:#F8FAFC}
|
||
.tab-btn.active{color:#047857;background:#fff;border:1.5px solid #E5E7EB;border-bottom:1.5px solid #fff}
|
||
.tab-badge{background:#F1F5F9;color:#64748B;border-radius:10px;padding:1px 7px;font-size:10px;font-weight:700}
|
||
.tab-btn.active .tab-badge{background:#ECFDF5;color:#047857}
|
||
|
||
/* ── Tab panes ── */
|
||
.tab-pane{display:none;background:#fff;border:1.5px solid #E5E7EB;border-radius:0 14px 14px 14px;padding:24px}
|
||
.tab-pane.active{display:block}
|
||
|
||
/* ── Section inside tab ── */
|
||
.inner-section{margin-bottom:24px}
|
||
.inner-section:last-child{margin-bottom:0}
|
||
.inner-title{font-family:'Montserrat',sans-serif;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94A3B8;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #F1F5F9;display:flex;align-items:center;justify-content:space-between}
|
||
|
||
/* ── Fields grid ── */
|
||
.fields{display:grid;grid-template-columns:repeat(2,1fr);gap:14px}
|
||
.fields.cols3{grid-template-columns:repeat(3,1fr)}
|
||
.fields.cols1{grid-template-columns:1fr}
|
||
.field{display:flex;flex-direction:column;gap:5px}
|
||
.field.span2{grid-column:span 2}
|
||
.field.span3{grid-column:span 3}
|
||
.field label{font-size:10px;font-weight:600;color:#64748B;text-transform:uppercase;letter-spacing:.06em}
|
||
.field input,.field textarea,.field select{padding:9px 12px;border:1.5px solid #E2E8F0;border-radius:8px;font-family:'Inter',sans-serif;font-size:13px;outline:none;transition:.2s;background:#fff;color:#0f172a}
|
||
.field input:focus,.field textarea:focus,.field select:focus{border-color:#047857;box-shadow:0 0 0 3px rgba(4,120,87,.1)}
|
||
.field input::placeholder,.field textarea::placeholder{color:#CBD5E1}
|
||
.field textarea{resize:vertical;min-height:72px;line-height:1.5}
|
||
|
||
/* ── Collapse (паспорт) ── */
|
||
.collapse-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;padding:10px 0;font-size:13px;font-weight:600;color:#64748B;border:none;background:none;font-family:'Inter',sans-serif;width:100%;transition:.15s}
|
||
.collapse-toggle:hover{color:#047857}
|
||
.collapse-arrow{font-size:10px;transition:transform .2s;color:#94A3B8}
|
||
.collapse-arrow.open{transform:rotate(90deg)}
|
||
.collapse-body{display:none;padding-top:14px}
|
||
.collapse-body.open{display:block}
|
||
|
||
/* ── Process flow ── */
|
||
.phase-flow{display:flex;gap:0;overflow-x:auto;margin-bottom:10px}
|
||
.phase-bar{height:6px;background:#F5F6F8;border-radius:100px;overflow:hidden;margin-bottom:4px}
|
||
.phase-bar-fill{height:100%;background:linear-gradient(90deg,#047857,#10B981);border-radius:100px;transition:width .5s ease}
|
||
.phase-pct-row{display:flex;justify-content:space-between;font-size:10px;color:#94A3B8}
|
||
|
||
/* ── Tasks ── */
|
||
.task-row{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid #F8FAFC}
|
||
.task-row:last-child{border-bottom:none}
|
||
.task-cb{width:15px;height:15px;accent-color:#047857;cursor:pointer;flex-shrink:0}
|
||
.task-text{flex:1;font-size:13px;color:#0f172a}
|
||
.task-text.done-text{text-decoration:line-through;color:#94A3B8}
|
||
.task-prio-tag{font-size:10px;padding:2px 8px;border-radius:10px;flex-shrink:0}
|
||
.task-del{color:#CBD5E1;font-size:17px;cursor:pointer;line-height:1;border:none;background:none;padding:0 2px}
|
||
.task-del:hover{color:#DC2626}
|
||
.task-add-row{display:flex;gap:8px;margin-top:12px}
|
||
.task-input{flex:1;padding:8px 12px;border:1.5px solid #E2E8F0;border-radius:8px;font-family:'Inter',sans-serif;font-size:13px;outline:none;transition:.2s}
|
||
.task-input:focus{border-color:#047857;box-shadow:0 0 0 3px rgba(4,120,87,.1)}
|
||
.task-prio-sel{padding:8px 10px;border:1.5px solid #E2E8F0;border-radius:8px;font-family:'Inter',sans-serif;font-size:12px;outline:none;background:#fff;color:#0f172a;transition:.2s}
|
||
.btn-add{background:linear-gradient(135deg,#064E3B,#047857);color:#fff;border:none;padding:8px 16px;border-radius:8px;font-family:'Inter',sans-serif;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}
|
||
|
||
/* ── Interview card ── */
|
||
.iv-empty{font-size:13px;color:#94A3B8;text-align:center;padding:20px 0}
|
||
.iv-session{border:1.5px solid #E5E7EB;border-radius:12px;overflow:hidden;margin-bottom:14px}
|
||
.iv-session-hdr{padding:11px 16px;background:#F8FAFC;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;gap:10px}
|
||
|
||
/* ── Intake brief ── */
|
||
.intake-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}
|
||
.intake-block{border:1.5px solid #E5E7EB;border-radius:10px;padding:14px}
|
||
.intake-block.empty{opacity:.5;border-color:#F1F5F9}
|
||
.intake-field-label{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#94A3B8;margin-bottom:3px}
|
||
.intake-field-val{font-size:13px;color:#0f172a;background:#F8FAFC;border:1px solid #E2E8F0;border-radius:7px;padding:7px 10px;line-height:1.5}
|
||
|
||
/* ── Events ── */
|
||
.event-row{display:flex;gap:12px;padding:9px 0;border-bottom:1px solid #F8FAFC;align-items:flex-start}
|
||
.event-row:last-child{border-bottom:none}
|
||
.event-icon{font-size:17px;flex-shrink:0;margin-top:1px}
|
||
.event-title{font-size:13px;font-weight:600;color:#0f172a}
|
||
.event-ts{font-size:11px;color:#94A3B8;margin-top:2px}
|
||
.note-add-row{display:flex;gap:8px;margin-top:12px}
|
||
.note-input{flex:1;padding:8px 12px;border:1.5px solid #E2E8F0;border-radius:8px;font-family:'Inter',sans-serif;font-size:13px;outline:none;transition:.2s}
|
||
.note-input:focus{border-color:#047857;box-shadow:0 0 0 3px rgba(4,120,87,.1)}
|
||
|
||
/* ── Modals ── */
|
||
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.52);z-index:1000;align-items:center;justify-content:center}
|
||
.modal-box{background:#fff;border-radius:16px;padding:28px;width:400px;max-width:92vw;box-shadow:0 20px 60px rgba(0,0,0,.25)}
|
||
.modal-title{font-family:'Montserrat',sans-serif;font-size:15px;font-weight:800;color:#0f172a;margin-bottom:20px}
|
||
.modal-footer{display:flex;gap:10px;margin-top:20px}
|
||
.btn-modal-cancel{flex:1;background:#F1F5F9;border:1.5px solid #E2E8F0;color:#64748B;padding:10px;border-radius:10px;font-family:'Inter',sans-serif;font-size:13px;font-weight:600;cursor:pointer}
|
||
.btn-modal-ok{flex:2;background:linear-gradient(135deg,#022C22,#047857);color:#fff;border:none;padding:10px;border-radius:10px;font-family:'Inter',sans-serif;font-size:14px;font-weight:700;cursor:pointer}
|
||
.btn-modal-blue{flex:2;background:linear-gradient(135deg,#1D4ED8,#3B82F6);color:#fff;border:none;padding:10px;border-radius:10px;font-family:'Inter',sans-serif;font-size:14px;font-weight:700;cursor:pointer}
|
||
.modal-field{margin-bottom:14px}
|
||
.modal-label{font-size:11px;font-weight:600;color:#64748B;text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:6px}
|
||
.modal-select,.modal-textarea{width:100%;padding:9px 12px;border:1.5px solid #E2E8F0;border-radius:8px;font-family:'Inter',sans-serif;font-size:13px;outline:none;background:#fff;color:#0f172a;transition:.2s}
|
||
.modal-select:focus,.modal-textarea:focus{border-color:#047857;box-shadow:0 0 0 3px rgba(4,120,87,.1)}
|
||
.modal-textarea{resize:vertical;min-height:72px;line-height:1.5}
|
||
.modal-tip{background:#ECFDF5;border:1px solid #A7F3D0;border-radius:8px;padding:11px;font-size:12px;color:#065F46;margin-bottom:16px}
|
||
.modal-radio-row{display:flex;gap:8px;margin-top:6px}
|
||
.modal-radio-lbl{flex:1;border:1.5px solid #E2E8F0;border-radius:8px;padding:9px 11px;cursor:pointer;display:flex;align-items:center;gap:7px;transition:.2s;font-size:13px}
|
||
|
||
/* ── Suggest modal ── */
|
||
.suggest-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:1001;align-items:flex-start;justify-content:center;overflow-y:auto;padding:40px 16px}
|
||
.suggest-box{background:#fff;border-radius:16px;padding:28px;width:560px;max-width:100%;box-shadow:0 20px 60px rgba(0,0,0,.3);margin:auto}
|
||
|
||
/* ── Status / priority badges ── */
|
||
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="layout">
|
||
|
||
<!-- ── SIDEBAR ── -->
|
||
<aside class="sidebar">
|
||
<div class="sb-top">
|
||
<div class="sb-brand">
|
||
<div style="width:28px;height:28px;border-radius:8px;background:#047857;display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
||
</div>
|
||
<div>
|
||
<div style="font-family:'Montserrat',sans-serif;font-size:9px;font-weight:500;letter-spacing:.06em;color:rgba(255,255,255,.4);line-height:1;margin-bottom:2px">@wasrusgen1</div>
|
||
<div style="font-family:'Montserrat',sans-serif;font-size:13px;font-weight:800;color:#fff;letter-spacing:.02em;line-height:1">КОНСАЛТИНГ</div>
|
||
</div>
|
||
</div>
|
||
<div class="sb-sub" style="margin-top:4px;padding-left:36px">Admin Panel</div>
|
||
</div>
|
||
|
||
<div class="sb-client-box">
|
||
<div class="sb-client-code">{{code}}</div>
|
||
<div class="sb-client-name" id="sb-name">{{name}}</div>
|
||
</div>
|
||
|
||
<nav class="sb-nav">
|
||
<div class="nav-section">КЛИЕНТЫ</div>
|
||
<a class="nav-item" href="/consult/admin">
|
||
<span class="ni-icon">📋</span><span class="ni-label">Все клиенты</span>
|
||
</a>
|
||
<div class="nav-section">КАРТОЧКА</div>
|
||
<a class="nav-item active" href="#" onclick="switchTab('work');return false">
|
||
<span class="ni-icon">💼</span><span class="ni-label">Работа</span>
|
||
</a>
|
||
<a class="nav-item" href="#" onclick="switchTab('interview');return false">
|
||
<span class="ni-icon">📋</span><span class="ni-label">Интервью</span>
|
||
</a>
|
||
<a class="nav-item" href="#" onclick="switchTab('details');return false">
|
||
<span class="ni-icon">🗂️</span><span class="ni-label">Реквизиты</span>
|
||
</a>
|
||
<a class="nav-item" href="#" onclick="switchTab('history');return false">
|
||
<span class="ni-icon">🕐</span><span class="ni-label">История</span>
|
||
</a>
|
||
<a class="nav-item" href="#" onclick="switchTab('finance');return false">
|
||
<span class="ni-icon">💰</span><span class="ni-label">Финансы</span>
|
||
</a>
|
||
</nav>
|
||
|
||
<div class="sb-footer">
|
||
<div class="sb-stat">Добавлен <b>{{created}}</b></div>
|
||
<div class="sb-stat"><b>{{msg_count}}</b> сообщений</div>
|
||
<div class="sb-stat">Визит: <b>{{last_active}}</b></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ── CONTENT ── -->
|
||
<main class="content">
|
||
<div class="content-hdr">
|
||
<div class="breadcrumb">
|
||
<a href="/consult/admin">Клиенты</a>
|
||
<span class="breadcrumb-sep">›</span>
|
||
<span class="breadcrumb-cur">{{code}}</span>
|
||
<span id="hdr-type-chip"></span>
|
||
</div>
|
||
<div class="hdr-actions">
|
||
<span class="save-status" id="save-status"></span>
|
||
<a href="/consult/admin/client/{{code}}/contract" target="_blank" class="btn-contract">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||
Договор
|
||
</a>
|
||
<button class="btn-save" onclick="saveCard()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content-body">
|
||
|
||
<!-- ── HERO ── -->
|
||
<div class="hero-card">
|
||
<div class="hero-avatar" id="hero-avatar">?</div>
|
||
<div class="hero-main">
|
||
<div class="hero-name" id="hero-name">{{name}}</div>
|
||
<div class="hero-meta">
|
||
<span class="hero-code">{{code}}</span>
|
||
<span class="hero-stat">{{msg_count}} сообщ.</span>
|
||
<span class="hero-stat">Визит: {{last_active}}</span>
|
||
</div>
|
||
<div class="hero-badges" id="hero-badges"></div>
|
||
</div>
|
||
<div class="hero-right">
|
||
<div class="type-toggle">
|
||
<button class="type-btn" id="btn-fiz" onclick="setType('fiz')">Физ. лицо</button>
|
||
<button class="type-btn" id="btn-yur" onclick="setType('yur')">Юр. лицо</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── TAB NAV ── -->
|
||
<div class="tabs-nav">
|
||
<button class="tab-btn active" id="tbtn-work" onclick="switchTab('work')">💼 Работа</button>
|
||
<button class="tab-btn" id="tbtn-interview" onclick="switchTab('interview')">📋 Интервью</button>
|
||
<button class="tab-btn" id="tbtn-details" onclick="switchTab('details')">🗂 Реквизиты</button>
|
||
<button class="tab-btn" id="tbtn-history" onclick="switchTab('history')">🕐 История</button>
|
||
<button class="tab-btn" id="tbtn-finance" onclick="switchTab('finance')">💰 Финансы</button>
|
||
</div>
|
||
|
||
<!-- ══ TAB: РАБОТА ══ -->
|
||
<div class="tab-pane active" id="tab-work">
|
||
|
||
<!-- CRM статус -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">CRM статус</div>
|
||
<div class="fields cols3" style="margin-bottom:16px">
|
||
<div class="field">
|
||
<label>Статус лида</label>
|
||
<select id="f-status">
|
||
<option value="new">🔵 Лид</option>
|
||
<option value="in_progress">🟡 В работе</option>
|
||
<option value="proposal">🟠 Предложение отправлено</option>
|
||
<option value="active">🟢 Работаем</option>
|
||
<option value="one_time">🟣 Разовая консультация</option>
|
||
<option value="rejected">🔴 Отказ</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label>Приоритет</label>
|
||
<select id="f-priority-override" title="Перебивает автоформулу">
|
||
<option value="">⚙️ Авто</option>
|
||
<option value="urgent">🔴 Срочно</option>
|
||
<option value="high">🟠 Важно</option>
|
||
<option value="normal">🟡 Обычный</option>
|
||
<option value="low">⚪ Низкий</option>
|
||
</select>
|
||
</div>
|
||
<div class="field" id="wformat-field" style="display:none">
|
||
<label>Формат работы</label>
|
||
<select id="f-work-format">
|
||
<option value="">— не выбрано —</option>
|
||
<option value="express">Экспресс-диагностика</option>
|
||
<option value="audit">Полный аудит + план</option>
|
||
<option value="turnkey">Внедрение под ключ</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Фаза проекта -->
|
||
<div class="fields" style="margin-bottom:14px">
|
||
<div class="field">
|
||
<label>Фаза проекта</label>
|
||
<select id="f-project-phase" onchange="updateProcessFlow(this.value)">
|
||
<option value="">— не начат —</option>
|
||
<option value="discovery">1 · Диагностика AS-IS</option>
|
||
<option value="audit">2 · Аудит и отчёт</option>
|
||
<option value="design">3 · Проектирование TO-BE</option>
|
||
<option value="impl">4 · Внедрение CRM</option>
|
||
<option value="support">5 · Сопровождение</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div id="process-flow-wrap" style="display:none;margin-bottom:14px">
|
||
<div style="display:flex;gap:0;overflow-x:auto" id="process-flow-steps"></div>
|
||
<div class="phase-bar" style="margin-top:10px"><div class="phase-bar-fill" id="process-flow-bar" style="width:0"></div></div>
|
||
<div class="phase-pct-row" style="margin-top:3px"><span>0%</span><span id="process-flow-pct" style="font-weight:700;color:#047857"></span><span>100%</span></div>
|
||
</div>
|
||
|
||
<div class="fields cols1">
|
||
<div class="field">
|
||
<label>Потребность / запрос клиента</label>
|
||
<textarea id="f-need" placeholder="Что хочет клиент, какую задачу решает…" style="min-height:64px"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Задачи -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">
|
||
<span>Задачи</span>
|
||
<span id="tasks-open-cnt" style="font-size:11px;color:#047857;font-weight:600"></span>
|
||
</div>
|
||
<div id="tasks-list"></div>
|
||
<div class="task-add-row">
|
||
<input type="text" class="task-input" id="task-input" placeholder="Новая задача…" onkeydown="if(event.key==='Enter')addTask()">
|
||
<select class="task-prio-sel" id="task-prio">
|
||
<option value="medium">Обычная</option>
|
||
<option value="high">Важная</option>
|
||
<option value="low">Низкая</option>
|
||
</select>
|
||
<button class="btn-add" onclick="addTask()">+ Добавить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Примечание -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">Примечание консультанта</div>
|
||
<div class="fields cols1">
|
||
<div class="field">
|
||
<textarea id="f-notes" placeholder="Внутренние заметки по клиенту…" style="min-height:80px"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /tab-work -->
|
||
|
||
<!-- ══ TAB: ИНТЕРВЬЮ ══ -->
|
||
<div class="tab-pane" id="tab-interview">
|
||
|
||
<!-- Запуск интервью -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">
|
||
<span>ТЗ / Интервью <span id="interview-status-chip"></span></span>
|
||
<div style="display:flex;gap:8px">
|
||
<button onclick="openSuggestModal()" style="background:#EFF6FF;border:1.5px solid #BFDBFE;color:#1D4ED8;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer">🤖 Вопросы по чату</button>
|
||
<button onclick="openLaunchModal()" style="background:linear-gradient(135deg,#064E3B,#047857);color:#fff;border:none;padding:6px 14px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:5px">
|
||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="5 3 19 12 5 21 5 3"/></svg> Запустить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="interview-body"><div class="iv-empty">Интервью не запущено</div></div>
|
||
</div>
|
||
|
||
<!-- Первичный бриф -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">
|
||
<span>Первичный бриф</span>
|
||
<span id="intake-status-chip"></span>
|
||
</div>
|
||
<div id="intake-body"><div class="iv-empty">Загрузка…</div></div>
|
||
</div>
|
||
|
||
</div><!-- /tab-interview -->
|
||
|
||
<!-- ══ TAB: РЕКВИЗИТЫ ══ -->
|
||
<div class="tab-pane" id="tab-details">
|
||
|
||
<!-- Системные данные -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">Системные данные</div>
|
||
<div class="fields cols3">
|
||
<div class="field">
|
||
<label>Имя / Псевдоним</label>
|
||
<input type="text" id="f-name" placeholder="Отображаемое имя">
|
||
</div>
|
||
<div class="field">
|
||
<label>№ Договора</label>
|
||
<input type="text" id="f-contract" placeholder="КС-2026-001">
|
||
</div>
|
||
<div class="field">
|
||
<label>Email для входа</label>
|
||
<input type="email" id="f-email" placeholder="client@company.ru">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Физ. лицо -->
|
||
<div id="section-fiz">
|
||
<div class="inner-section">
|
||
<div class="inner-title">Физическое лицо</div>
|
||
<div class="fields">
|
||
<div class="field span2">
|
||
<label>ФИО полностью</label>
|
||
<input type="text" id="fiz-full_name" placeholder="Иванов Иван Петрович">
|
||
</div>
|
||
<div class="field">
|
||
<label>Телефон</label>
|
||
<input type="tel" id="fiz-phone" placeholder="+7 900 000-00-00">
|
||
</div>
|
||
<div class="field">
|
||
<label>Дата рождения</label>
|
||
<input type="text" id="fiz-birthdate" placeholder="01.01.1980">
|
||
</div>
|
||
<div class="field span2">
|
||
<label>Адрес регистрации</label>
|
||
<input type="text" id="fiz-address" placeholder="г. Москва, ул. Примерная, д. 1, кв. 1">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Паспорт — collapsed -->
|
||
<div class="inner-section">
|
||
<button class="collapse-toggle" onclick="togglePassport()" id="passport-toggle">
|
||
<span class="collapse-arrow" id="passport-arrow">▶</span>
|
||
🔐 Паспортные данные
|
||
<span style="font-size:11px;color:#CBD5E1;font-weight:400;margin-left:4px">(для договора)</span>
|
||
</button>
|
||
<div class="collapse-body" id="passport-body">
|
||
<div class="fields cols3">
|
||
<div class="field">
|
||
<label>Серия</label>
|
||
<input type="text" id="fiz-passport_series" placeholder="4510">
|
||
</div>
|
||
<div class="field">
|
||
<label>Номер</label>
|
||
<input type="text" id="fiz-passport_number" placeholder="123456">
|
||
</div>
|
||
<div class="field">
|
||
<label>Дата выдачи</label>
|
||
<input type="text" id="fiz-passport_date" placeholder="15.06.2015">
|
||
</div>
|
||
<div class="field span3">
|
||
<label>Кем выдан</label>
|
||
<input type="text" id="fiz-passport_issued" placeholder="УФМС России по г. Москве…">
|
||
</div>
|
||
<div class="field span3">
|
||
<label>Код подразделения</label>
|
||
<input type="text" id="fiz-passport_code" placeholder="770-001">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Юр. лицо -->
|
||
<div id="section-yur" style="display:none">
|
||
<div class="inner-section">
|
||
<div class="inner-title">Юридическое лицо</div>
|
||
<div class="fields">
|
||
<div class="field span2">
|
||
<label>Полное наименование</label>
|
||
<input type="text" id="yur-org_name" placeholder="ООО «Ромашка»">
|
||
</div>
|
||
<div class="field"><label>ИНН</label><input type="text" id="yur-inn" placeholder="7700000000"></div>
|
||
<div class="field"><label>КПП</label><input type="text" id="yur-kpp" placeholder="770001001"></div>
|
||
<div class="field"><label>ОГРН</label><input type="text" id="yur-ogrn" placeholder="1027700000000"></div>
|
||
<div class="field"><label>ОКПО</label><input type="text" id="yur-okpo" placeholder="00000000"></div>
|
||
<div class="field span2"><label>Юридический адрес</label><input type="text" id="yur-legal_address" placeholder="127000, г. Москва…"></div>
|
||
<div class="field span2"><label>Фактический адрес</label><input type="text" id="yur-actual_address" placeholder="если отличается"></div>
|
||
<div class="field"><label>Контактное лицо (ФИО)</label><input type="text" id="yur-contact_person" placeholder="Иванов Иван Иванович"></div>
|
||
<div class="field"><label>Должность</label><input type="text" id="yur-contact_position" placeholder="Директор"></div>
|
||
<div class="field"><label>Телефон</label><input type="tel" id="yur-phone" placeholder="+7 495 000-00-00"></div>
|
||
<div class="field"><label>Действует на основании</label><input type="text" id="yur-basis" placeholder="Устава"></div>
|
||
</div>
|
||
</div>
|
||
<div class="inner-section">
|
||
<div class="inner-title">Банковские реквизиты</div>
|
||
<div class="fields">
|
||
<div class="field span2"><label>Банк</label><input type="text" id="yur-bank_name" placeholder="ПАО Сбербанк, г. Москва"></div>
|
||
<div class="field"><label>Расчётный счёт (р/с)</label><input type="text" id="yur-bank_account" placeholder="40702810000000000000"></div>
|
||
<div class="field"><label>БИК</label><input type="text" id="yur-bank_bik" placeholder="044525225"></div>
|
||
<div class="field"><label>Корр. счёт (к/с)</label><input type="text" id="yur-bank_corr" placeholder="30101810400000000225"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /tab-details -->
|
||
|
||
<!-- ══ TAB: ИСТОРИЯ ══ -->
|
||
<div class="tab-pane" id="tab-history">
|
||
<div class="inner-section">
|
||
<div class="inner-title">
|
||
<span>История взаимодействий</span>
|
||
<span id="events-count" style="font-size:11px;color:#047857;font-weight:600"></span>
|
||
</div>
|
||
<div id="events-list"></div>
|
||
<div class="note-add-row">
|
||
<input type="text" class="note-input" id="note-input" placeholder="Добавить заметку консультанта…"
|
||
onkeydown="if(event.key==='Enter')addNote()">
|
||
<button class="btn-add" onclick="addNote()">+ Добавить</button>
|
||
</div>
|
||
</div>
|
||
</div><!-- /tab-history -->
|
||
|
||
<!-- ══ TAB: ФИНАНСЫ ══ -->
|
||
<div class="tab-pane" id="tab-finance">
|
||
|
||
<!-- Договор -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">
|
||
<span>Договор</span>
|
||
<span id="contract-chip"></span>
|
||
</div>
|
||
<div class="fields cols3" style="margin-bottom:16px">
|
||
<div class="field">
|
||
<label>Тариф</label>
|
||
<select id="fin-tariff" onchange="onTariffChange()">
|
||
<option value="express">Экспресс-диагностика (0 ₽)</option>
|
||
<option value="audit" selected>Полный аудит + план (150 000 ₽)</option>
|
||
<option value="impl">Внедрение под ключ (350 000 ₽)</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label>Сумма договора, ₽</label>
|
||
<input type="number" id="fin-price" value="150000" min="0" step="1000">
|
||
</div>
|
||
<div class="field" style="justify-content:flex-end;align-items:flex-end;display:flex;flex-direction:column">
|
||
<div style="display:flex;gap:8px;width:100%;justify-content:flex-end;align-items:flex-end;height:100%">
|
||
<button onclick="saveContract()" style="background:#F8FAFC;border:1.5px solid #E2E8F0;color:#64748B;padding:8px 14px;border-radius:8px;font-family:'Inter',sans-serif;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap">💾 Сохранить</button>
|
||
<button onclick="sendContract()" id="btn-send-contract" style="background:linear-gradient(135deg,#064E3B,#047857);color:#fff;border:none;padding:8px 16px;border-radius:8px;font-family:'Montserrat',sans-serif;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:5px">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="5 3 19 12 5 21 5 3"/></svg>Отправить клиенту
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="contract-sign-info" style="display:none;background:#ECFDF5;border:1.5px solid #A7F3D0;border-radius:10px;padding:12px 16px;font-size:13px;color:#047857;margin-top:4px"></div>
|
||
<div id="contract-send-msg" style="font-size:12px;color:#94A3B8;margin-top:6px"></div>
|
||
</div>
|
||
|
||
<!-- Просмотр договора -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">Документ</div>
|
||
<a href="/consult/admin/client/{{code}}/contract" target="_blank" style="display:inline-flex;align-items:center;gap:7px;background:#F8FAFC;border:1.5px solid #E2E8F0;color:#047857;padding:9px 16px;border-radius:8px;font-size:13px;font-weight:600;text-decoration:none;transition:.2s" onmouseover="this.style.background='#F0FDF4'" onmouseout="this.style.background='#F8FAFC'">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||
Открыть договор для просмотра / редактирования
|
||
</a>
|
||
</div>
|
||
|
||
<!-- График платежей -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">
|
||
<span>График платежей</span>
|
||
<span id="pay-summary-chip" style="font-size:11px;font-weight:600;color:#047857"></span>
|
||
</div>
|
||
<div id="payments-list"><div style="color:#94A3B8;font-size:13px">Договор ещё не отправлен клиенту</div></div>
|
||
</div>
|
||
|
||
<!-- Критерии приёмки -->
|
||
<div class="inner-section">
|
||
<div class="inner-title">Критерии приёмки услуг</div>
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:flex;align-items:flex-start;gap:10px;padding:10px 12px;background:#F8FAFC;border-radius:8px;border:1px solid #E2E8F0">
|
||
<span style="font-size:16px;flex-shrink:0">1️⃣</span>
|
||
<div><div style="font-size:12px;font-weight:700;color:#0f172a;margin-bottom:2px">Диагностика AS-IS</div><div style="font-size:12px;color:#64748B">Передан отчёт + клиент подтвердил получение</div></div>
|
||
</div>
|
||
<div style="display:flex;align-items:flex-start;gap:10px;padding:10px 12px;background:#F8FAFC;border-radius:8px;border:1px solid #E2E8F0">
|
||
<span style="font-size:16px;flex-shrink:0">2️⃣</span>
|
||
<div><div style="font-size:12px;font-weight:700;color:#0f172a;margin-bottom:2px">Аудит + TO-BE</div><div style="font-size:12px;color:#64748B">Согласован план внедрения (дорожная карта)</div></div>
|
||
</div>
|
||
<div style="display:flex;align-items:flex-start;gap:10px;padding:10px 12px;background:#F8FAFC;border-radius:8px;border:1px solid #E2E8F0">
|
||
<span style="font-size:16px;flex-shrink:0">3️⃣</span>
|
||
<div><div style="font-size:12px;font-weight:700;color:#0f172a;margin-bottom:2px">Внедрение</div><div style="font-size:12px;color:#64748B">Подписан итоговый Акт об оказанных услугах</div></div>
|
||
</div>
|
||
<div style="display:flex;align-items:flex-start;gap:10px;padding:10px 12px;background:#F8FAFC;border-radius:8px;border:1px solid #E2E8F0">
|
||
<span style="font-size:16px;flex-shrink:0">🔄</span>
|
||
<div><div style="font-size:12px;font-weight:700;color:#0f172a;margin-bottom:2px">Сопровождение</div><div style="font-size:12px;color:#64748B">Ежемесячный мини-акт по итогам периода</div></div>
|
||
</div>
|
||
<div style="background:#FFFBEB;border:1px solid #FDE68A;border-radius:8px;padding:10px 12px;font-size:12px;color:#92400E">
|
||
⚖️ По п. 3.4 договора: при отсутствии мотивированного отказа в течение 5 рабочих дней с момента направления Акта — услуги считаются принятыми.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /tab-finance -->
|
||
|
||
</div><!-- /content-body -->
|
||
</main>
|
||
</div><!-- /layout -->
|
||
|
||
<!-- ══ MODAL: Launch Interview ══ -->
|
||
<div class="modal-overlay" id="launch-modal" onclick="if(event.target===this)closeLaunchModal()">
|
||
<div class="modal-box">
|
||
<div class="modal-title">🚀 Запустить интервью</div>
|
||
<div class="modal-field">
|
||
<label class="modal-label">Фаза проекта</label>
|
||
<select id="modal-phase" class="modal-select">
|
||
<option value="discovery">Диагностика AS-IS (10 вопросов)</option>
|
||
<option value="audit">Аудит (5 вопросов)</option>
|
||
<option value="design">TO-BE Проектирование (3 вопроса)</option>
|
||
<option value="impl">Внедрение (3 вопроса)</option>
|
||
<option value="support">Поддержка (2 вопроса)</option>
|
||
</select>
|
||
</div>
|
||
<div class="modal-field">
|
||
<label class="modal-label">Формат</label>
|
||
<div class="modal-radio-row">
|
||
<label class="modal-radio-lbl" id="mode-qa-lbl">
|
||
<input type="radio" name="modal-mode" value="qa" checked style="accent-color:#047857">
|
||
📋 Анкета <span style="font-size:11px;color:#94A3B8">(бесплатно)</span>
|
||
</label>
|
||
<label class="modal-radio-lbl" id="mode-dialog-lbl">
|
||
<input type="radio" name="modal-mode" value="dialog" style="accent-color:#047857">
|
||
🤖 ИИ-Диалог <span style="font-size:11px;color:#94A3B8">(API)</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="modal-field">
|
||
<label class="modal-label">Контекст / первичное обращение <span style="font-weight:400;text-transform:none;color:#CBD5E1">(необязательно)</span></label>
|
||
<textarea id="modal-context" class="modal-textarea" placeholder="Кратко: откуда клиент, важный контекст для интервью…"></textarea>
|
||
</div>
|
||
<div class="modal-tip">Клиент увидит предложение пройти интервью при следующем открытии чата.</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-modal-cancel" onclick="closeLaunchModal()">Отмена</button>
|
||
<button class="btn-modal-ok" id="launch-btn" onclick="launchInterview()">Запустить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ MODAL: Suggest Questions ══ -->
|
||
<div class="suggest-overlay" id="suggest-modal" onclick="if(event.target===this)closeSuggestModal()">
|
||
<div class="suggest-box">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||
<div style="font-family:'Montserrat',sans-serif;font-size:15px;font-weight:800;color:#0f172a">🤖 AI-вопросы по чату</div>
|
||
<button onclick="closeSuggestModal()" style="background:#F1F5F9;border:none;width:28px;height:28px;border-radius:7px;cursor:pointer;font-size:15px;color:#64748B">×</button>
|
||
</div>
|
||
<div class="modal-field">
|
||
<label class="modal-label">Фаза</label>
|
||
<select id="suggest-phase" class="modal-select">
|
||
<option value="discovery">Диагностика AS-IS</option>
|
||
<option value="audit">Аудит</option>
|
||
<option value="design">TO-BE Проектирование</option>
|
||
<option value="impl">Внедрение</option>
|
||
<option value="support">Поддержка</option>
|
||
</select>
|
||
</div>
|
||
<div id="suggest-result" style="min-height:60px"></div>
|
||
<div class="modal-footer">
|
||
<button class="btn-modal-cancel" onclick="closeSuggestModal()">Закрыть</button>
|
||
<button class="btn-modal-blue" id="run-suggest-btn" onclick="runSuggest()">✨ Сформировать</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const CODE = '{{code}}';
|
||
const INIT_TYPE = '{{client_type}}';
|
||
const CARD = {{card_json}};
|
||
|
||
// ── Prefill ──────────────────────────────────────────────────────────────────
|
||
document.getElementById('f-name').value = '{{name}}';
|
||
document.getElementById('f-contract').value = '{{contract_no}}';
|
||
document.getElementById('f-email').value = '{{email}}';
|
||
document.getElementById('f-notes').value = CARD.notes || '';
|
||
document.getElementById('f-status').value = '{{status}}' || 'new';
|
||
document.getElementById('f-need').value = '{{need_text}}';
|
||
document.getElementById('f-work-format').value = '{{work_format}}';
|
||
document.getElementById('f-priority-override').value = '{{priority_override}}';
|
||
|
||
const _initPhase = CARD.project_phase || '';
|
||
if(_initPhase){ document.getElementById('f-project-phase').value = _initPhase; updateProcessFlow(_initPhase); }
|
||
Object.keys(CARD).forEach(k => {
|
||
const el = document.getElementById('fiz-'+k) || document.getElementById('yur-'+k);
|
||
if(el) el.value = CARD[k] || '';
|
||
});
|
||
|
||
// ── Hero ─────────────────────────────────────────────────────────────────────
|
||
function updateHero(){
|
||
const name = document.getElementById('f-name').value || CODE;
|
||
document.getElementById('hero-name').textContent = name;
|
||
document.getElementById('sb-name').textContent = name;
|
||
const initials = name.split(' ').map(w=>w[0]||'').join('').slice(0,2).toUpperCase() || CODE.slice(0,2);
|
||
document.getElementById('hero-avatar').textContent = initials;
|
||
|
||
const statusLabels = {new:'🔵 Лид',in_progress:'🟡 В работе',proposal:'🟠 Предложение',active:'🟢 Работаем',one_time:'🟣 Разовая',rejected:'🔴 Отказ'};
|
||
const statusColors = {new:'#EFF6FF:#1D4ED8',in_progress:'#FFFBEB:#D97706',proposal:'#FFF7ED:#EA580C',active:'#ECFDF5:#047857',one_time:'#F5F3FF:#7C3AED',rejected:'#FEF2F2:#DC2626'};
|
||
const st = document.getElementById('f-status').value;
|
||
const [sbg,stxt] = (statusColors[st]||'#F1F5F9:#64748B').split(':');
|
||
const prioMap = {urgent:'🔴 Срочно',high:'🟠 Важно',normal:'🟡 Обычный',low:'⚪ Низкий','':`⚙️ Авто`};
|
||
const prio = document.getElementById('f-priority-override').value;
|
||
const phase = document.getElementById('f-project-phase').value;
|
||
const phaseLabels = {discovery:'AS-IS',audit:'Аудит',design:'TO-BE',impl:'Внедрение',support:'Сопровождение'};
|
||
|
||
let html = `<span class="badge" style="background:${sbg};color:${stxt}">${statusLabels[st]||st}</span>`;
|
||
if(prio) html += `<span class="badge" style="background:#F1F5F9;color:#64748B">${prioMap[prio]}</span>`;
|
||
if(phase) html += `<span class="badge" style="background:#ECFDF5;color:#047857;border:1px solid #A7F3D0">📍 ${phaseLabels[phase]||phase}</span>`;
|
||
document.getElementById('hero-badges').innerHTML = html;
|
||
|
||
// type chip in breadcrumb
|
||
const t = currentType;
|
||
document.getElementById('hdr-type-chip').innerHTML = `<span style="font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:${t==='fiz'?'#F0FDF4':'#EFF6FF'};color:${t==='fiz'?'#059669':'#1D4ED8'};margin-left:8px">${t==='fiz'?'Физ. лицо':'Юр. лицо'}</span>`;
|
||
}
|
||
document.getElementById('f-status').addEventListener('change', ()=>{toggleWorkFormat();updateHero();});
|
||
document.getElementById('f-priority-override').addEventListener('change', updateHero);
|
||
document.getElementById('f-project-phase').addEventListener('change', updateHero);
|
||
document.getElementById('f-name').addEventListener('input', updateHero);
|
||
|
||
// ── Type ─────────────────────────────────────────────────────────────────────
|
||
let currentType = INIT_TYPE || 'fiz';
|
||
function setType(t){
|
||
currentType = t;
|
||
document.getElementById('section-fiz').style.display = t==='fiz'?'':'none';
|
||
document.getElementById('section-yur').style.display = t==='yur'?'':'none';
|
||
document.getElementById('btn-fiz').classList.toggle('active', t==='fiz');
|
||
document.getElementById('btn-yur').classList.toggle('active', t==='yur');
|
||
updateHero();
|
||
}
|
||
setType(currentType);
|
||
|
||
function toggleWorkFormat(){
|
||
const s = document.getElementById('f-status').value;
|
||
document.getElementById('wformat-field').style.display = (s==='active'||s==='proposal')?'':'none';
|
||
}
|
||
toggleWorkFormat();
|
||
|
||
// ── Tabs ─────────────────────────────────────────────────────────────────────
|
||
const TAB_IDS = ['work','interview','details','history','finance'];
|
||
function switchTab(id){
|
||
TAB_IDS.forEach(t=>{
|
||
document.getElementById('tab-'+t).classList.toggle('active', t===id);
|
||
document.getElementById('tbtn-'+t).classList.toggle('active', t===id);
|
||
const navItem = document.querySelector(`.nav-item[onclick*="'${t}'"]`);
|
||
if(navItem) navItem.classList.toggle('active', t===id);
|
||
});
|
||
if(id==='finance') loadFinances();
|
||
}
|
||
|
||
// ── Collapse (passport) ──────────────────────────────────────────────────────
|
||
function togglePassport(){
|
||
const body = document.getElementById('passport-body');
|
||
const arrow = document.getElementById('passport-arrow');
|
||
const open = body.classList.toggle('open');
|
||
arrow.classList.toggle('open', open);
|
||
}
|
||
|
||
// ── Process flow ──────────────────────────────────────────────────────────────
|
||
const PHASE_META = {
|
||
discovery:{label:'Диагностика',short:'AS-IS',bg:'#DBEAFE',color:'#1D4ED8',pct:20},
|
||
audit: {label:'Аудит', short:'Аудит', bg:'#FEF3C7',color:'#D97706',pct:40},
|
||
design: {label:'Проектирование',short:'TO-BE',bg:'#F3E8FF',color:'#7C3AED',pct:60},
|
||
impl: {label:'Внедрение', short:'CRM', bg:'#ECFDF5',color:'#047857',pct:80},
|
||
support: {label:'Сопровождение',short:'6мес',bg:'#F1F5F9',color:'#64748B',pct:100},
|
||
};
|
||
const PHASE_KEYS = ['discovery','audit','design','impl','support'];
|
||
|
||
function updateProcessFlow(phase){
|
||
const wrap = document.getElementById('process-flow-wrap');
|
||
const bar = document.getElementById('process-flow-bar');
|
||
const pctEl = document.getElementById('process-flow-pct');
|
||
const stepsEl= document.getElementById('process-flow-steps');
|
||
if(!phase){wrap.style.display='none';return;}
|
||
wrap.style.display='block';
|
||
const pm = PHASE_META[phase];
|
||
bar.style.width = pm.pct+'%';
|
||
pctEl.textContent = pm.pct+'%';
|
||
const activeIdx = PHASE_KEYS.indexOf(phase);
|
||
stepsEl.innerHTML = PHASE_KEYS.map((k,i)=>{
|
||
const m=PHASE_META[k];
|
||
const isDone=i<activeIdx, isActive=i===activeIdx;
|
||
const cbg=isDone?'#ECFDF5':isActive?'#047857':'#F9FAFB';
|
||
const cc =isDone?'#047857':isActive?'#fff':'#9CA3AF';
|
||
const cb =isDone?'2px solid #10B981':isActive?'none':'2px solid #E5E7EB';
|
||
const num=isDone?`<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="${cc}" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>`
|
||
:`<span style="font-size:11px;font-weight:800;color:${cc}">${i+1}</span>`;
|
||
return `<div style="flex:1;display:flex;flex-direction:column;align-items:center;position:relative;min-width:0">
|
||
${i>0?`<div style="position:absolute;left:-50%;top:17px;right:50%;height:1.5px;background:${isDone?'#10B981':'#E5E7EB'}"></div>`:''}
|
||
<div style="width:34px;height:34px;border-radius:50%;background:${cbg};border:${cb};display:flex;align-items:center;justify-content:center;z-index:1;box-shadow:${isActive?'0 4px 12px rgba(4,120,87,.3)':'none'}">${num}</div>
|
||
<div style="font-size:10px;font-weight:${isActive?700:500};color:${isActive?'#047857':isDone?'#6B7280':'#9CA3AF'};text-align:center;margin-top:5px;line-height:1.2">${m.short}</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// ── collectCard ───────────────────────────────────────────────────────────────
|
||
function collectCard(){
|
||
const prefix = currentType==='fiz'?'fiz-':'yur-';
|
||
const card = { notes: document.getElementById('f-notes').value, project_phase: document.getElementById('f-project-phase').value };
|
||
document.querySelectorAll('[id^="'+prefix+'"]').forEach(el=>{ card[el.id.replace(prefix,'')] = el.value; });
|
||
return card;
|
||
}
|
||
|
||
// ── Tasks ─────────────────────────────────────────────────────────────────────
|
||
const PRIO_LABELS = {high:'🔴 Важная',medium:'🟡 Обычная',low:'⚪ Низкая'};
|
||
async function loadTasks(){
|
||
const r = await fetch('/consult/api/tasks/'+CODE,{credentials:'include'});
|
||
if(!r.ok) return;
|
||
renderTasks(await r.json());
|
||
}
|
||
function renderTasks(tasks){
|
||
const open=tasks.filter(t=>!t.done), done=tasks.filter(t=>t.done);
|
||
document.getElementById('tasks-open-cnt').textContent = open.length?open.length+' открытых':'';
|
||
const el=document.getElementById('tasks-list');
|
||
if(!tasks.length){el.innerHTML='<div style="color:#94A3B8;font-size:13px;padding:4px 0">Задач нет</div>';return;}
|
||
el.innerHTML=[...open,...done].map(t=>`
|
||
<div class="task-row">
|
||
<input type="checkbox" class="task-cb" ${t.done?'checked':''} onchange="toggleTask(${t.id},this.checked)">
|
||
<span class="task-text ${t.done?'done-text':''}">${t.text}</span>
|
||
<span class="task-prio-tag" style="background:#F1F5F9;color:#64748B">${PRIO_LABELS[t.priority]||''}</span>
|
||
<button class="task-del" onclick="deleteTask(${t.id})">×</button>
|
||
</div>`).join('');
|
||
}
|
||
async function addTask(){
|
||
const inp=document.getElementById('task-input');
|
||
const text=inp.value.trim(); if(!text)return;
|
||
inp.value='';
|
||
await fetch('/consult/api/tasks',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({code:CODE,text,priority:document.getElementById('task-prio').value})});
|
||
await loadTasks();
|
||
}
|
||
async function toggleTask(id,done){
|
||
await fetch('/consult/api/tasks/'+id+'/done',{method:'PATCH',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({done})});
|
||
await loadTasks();
|
||
}
|
||
async function deleteTask(id){
|
||
if(!confirm('Удалить задачу?'))return;
|
||
await fetch('/consult/api/tasks/'+id,{method:'DELETE',credentials:'include'});
|
||
await loadTasks();
|
||
}
|
||
loadTasks();
|
||
|
||
// ── Interview ─────────────────────────────────────────────────────────────────
|
||
const PHASE_LABELS={discovery:'Диагностика AS-IS',audit:'Аудит',design:'TO-BE',impl:'Внедрение',support:'Поддержка'};
|
||
const STATUS_COLORS={pending:{bg:'#FEF3C7',color:'#D97706',text:'Ожидает'},active:{bg:'#DBEAFE',color:'#1D4ED8',text:'Активно'},completed:{bg:'#ECFDF5',color:'#047857',text:'Завершено'}};
|
||
|
||
async function loadInterview(){
|
||
const r=await fetch('/consult/api/admin/interview/'+CODE,{credentials:'include'});
|
||
if(!r.ok)return;
|
||
const sessions=await r.json();
|
||
const body=document.getElementById('interview-body');
|
||
const chip=document.getElementById('interview-status-chip');
|
||
if(!sessions.length){body.innerHTML='<div class="iv-empty">Интервью не запущено</div>';chip.innerHTML='';return;}
|
||
const latest=sessions[0], sc=STATUS_COLORS[latest.status]||STATUS_COLORS.pending;
|
||
chip.innerHTML=`<span style="font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:${sc.bg};color:${sc.color};margin-left:8px">${sc.text}</span>`;
|
||
body.innerHTML=sessions.map(s=>renderSessionCard(s)).join('');
|
||
}
|
||
function renderSessionCard(s){
|
||
const sc=STATUS_COLORS[s.status]||STATUS_COLORS.pending;
|
||
const blocks={};
|
||
s.questions.forEach(q=>{if(!blocks[q.block])blocks[q.block]=[];blocks[q.block].push(q);});
|
||
const blocksHtml=Object.entries(blocks).map(([block,qs])=>`
|
||
<div style="margin-bottom:14px">
|
||
<div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94A3B8;margin-bottom:7px;padding-bottom:3px;border-bottom:1px solid #F1F5F9">${block}</div>
|
||
${qs.map(q=>`<div style="margin-bottom:9px">
|
||
<div style="font-size:12px;font-weight:600;color:#475569;margin-bottom:3px">${q.text}</div>
|
||
${q.answer?`<div style="font-size:13px;color:#0f172a;background:#F8FAFC;border:1px solid #E2E8F0;border-radius:7px;padding:7px 10px;line-height:1.5">${escHtml(q.answer)}</div>`
|
||
:`<div style="font-size:12px;color:#CBD5E1;font-style:italic">Не отвечено</div>`}
|
||
</div>`).join('')}
|
||
</div>`).join('');
|
||
return `<div class="iv-session">
|
||
<div class="iv-session-hdr">
|
||
<div>
|
||
<span style="font-weight:700;font-size:13px;color:#0f172a">${PHASE_LABELS[s.phase]||s.phase}</span>
|
||
<span style="font-size:11px;color:#94A3B8;margin-left:8px">${s.mode==='dialog'?'🤖 ИИ-Диалог':'📋 Анкета'}</span>
|
||
<span style="font-size:11px;color:#94A3B8;margin-left:6px">${s.answered}/${s.total}</span>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<span style="font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:${sc.bg};color:${sc.color}">${sc.text}</span>
|
||
<button onclick="deleteInterview(${s.session_id})" style="background:#FEF2F2;border:1px solid #FECACA;color:#DC2626;padding:2px 8px;border-radius:6px;cursor:pointer;font-size:11px">🗑</button>
|
||
</div>
|
||
</div>
|
||
<div style="padding:16px">${s.answered>0?blocksHtml:'<div class="iv-empty">Ответов пока нет — ожидаем клиента</div>'}</div>
|
||
</div>`;
|
||
}
|
||
function escHtml(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g,'<br>');}
|
||
function openLaunchModal(){document.getElementById('launch-modal').style.display='flex';}
|
||
function closeLaunchModal(){document.getElementById('launch-modal').style.display='none';}
|
||
async function launchInterview(){
|
||
const phase=document.getElementById('modal-phase').value;
|
||
const mode=document.querySelector('input[name="modal-mode"]:checked').value;
|
||
const context=document.getElementById('modal-context').value.trim();
|
||
const btn=document.getElementById('launch-btn');
|
||
btn.disabled=true;btn.textContent='Запуск…';
|
||
try{
|
||
const r=await fetch('/consult/api/admin/interview/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({code:CODE,phase,mode,context}),credentials:'include'});
|
||
const d=await r.json();
|
||
closeLaunchModal();
|
||
if(d.ok){document.getElementById('modal-context').value='';await loadInterview();}
|
||
else if(d.error==='already_active') alert('У клиента уже есть активное интервью для этой фазы.');
|
||
}catch(e){alert('Ошибка запуска.');}
|
||
btn.disabled=false;btn.textContent='Запустить';
|
||
}
|
||
async function deleteInterview(sessionId){
|
||
if(!confirm('Удалить интервью и все ответы?'))return;
|
||
await fetch('/consult/api/admin/interview/'+sessionId,{method:'DELETE',credentials:'include'});
|
||
await loadInterview();
|
||
}
|
||
loadInterview();
|
||
|
||
// ── Intake Brief ──────────────────────────────────────────────────────────────
|
||
const INTAKE_STEPS_META=[
|
||
{icon:'🏢',title:'Бизнес',fields:['biz_sphere','biz_size','biz_model']},
|
||
{icon:'🔥',title:'Боль',fields:['pain_main','pain_cost','pain_tried']},
|
||
{icon:'⚙️',title:'Процесс',fields:['proc_steps','proc_bottleneck','proc_tools']},
|
||
{icon:'🎯',title:'Цель',fields:['goal_result','goal_metrics','goal_budget']},
|
||
{icon:'📍',title:'Контекст',fields:['ctx_team','ctx_deadline','ctx_decider']},
|
||
];
|
||
const INTAKE_LABELS={
|
||
biz_sphere:'Сфера',biz_size:'Размер',biz_model:'Бизнес-модель',
|
||
pain_main:'Проблема',pain_cost:'Цена боли',pain_tried:'Пробовали',
|
||
proc_steps:'Шаги',proc_bottleneck:'Узкое место',proc_tools:'Инструменты',
|
||
goal_result:'Результат',goal_metrics:'Метрики',goal_budget:'Бюджет',
|
||
ctx_team:'Команда',ctx_deadline:'Сроки',ctx_decider:'ЛПР',
|
||
};
|
||
async function loadIntakeBrief(){
|
||
try{
|
||
const r=await fetch('/consult/api/admin/intake/'+CODE,{credentials:'include'});
|
||
if(!r.ok)return;
|
||
const data=await r.json();
|
||
const body=document.getElementById('intake-body');
|
||
const chip=document.getElementById('intake-status-chip');
|
||
const allKeys=Object.keys(INTAKE_LABELS);
|
||
const filled=allKeys.filter(k=>data[k]&&data[k].value&&data[k].value.trim()).length;
|
||
if(!filled){body.innerHTML='<div class="iv-empty">Клиент ещё не заполнил бриф</div>';chip.innerHTML='';return;}
|
||
const pct=Math.round(filled/allKeys.length*100);
|
||
const isComplete=filled===allKeys.length;
|
||
chip.innerHTML=`<span style="font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:${isComplete?'#ECFDF5':'#FEF3C7'};color:${isComplete?'#047857':'#D97706'}">${isComplete?'✅ Заполнен':pct+'% заполнено'}</span>`;
|
||
body.innerHTML=`
|
||
<div style="height:4px;background:#F1F5F9;border-radius:4px;overflow:hidden;margin-bottom:16px">
|
||
<div style="height:100%;width:${pct}%;background:linear-gradient(90deg,#047857,#10B981);border-radius:4px"></div>
|
||
</div>
|
||
<div class="intake-grid">
|
||
${INTAKE_STEPS_META.map(step=>{
|
||
const hasAny=step.fields.some(k=>data[k]&&data[k].value&&data[k].value.trim());
|
||
if(!hasAny) return `<div class="intake-block empty"><div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#94A3B8;margin-bottom:6px">${step.icon} ${step.title}</div><div style="font-size:12px;color:#CBD5E1;font-style:italic">Не заполнено</div></div>`;
|
||
return `<div class="intake-block"><div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#047857;margin-bottom:10px">${step.icon} ${step.title}</div>
|
||
${step.fields.map(k=>{
|
||
const v=data[k];
|
||
if(!v||!v.value||!v.value.trim()) return `<div style="margin-bottom:5px"><span style="font-size:11px;color:#CBD5E1">${INTAKE_LABELS[k]}: <i>не указано</i></span></div>`;
|
||
return `<div style="margin-bottom:9px"><div class="intake-field-label">${INTAKE_LABELS[k]}</div><div class="intake-field-val">${escHtml(v.value)}</div></div>`;
|
||
}).join('')}
|
||
</div>`;
|
||
}).join('')}
|
||
</div>`;
|
||
}catch(e){}
|
||
}
|
||
loadIntakeBrief();
|
||
|
||
// ── Events ────────────────────────────────────────────────────────────────────
|
||
async function loadEvents(){
|
||
try{
|
||
const r=await fetch('/consult/api/admin/events/'+CODE,{credentials:'include'});
|
||
if(!r.ok)return;
|
||
const events=await r.json();
|
||
document.getElementById('events-count').textContent=events.length?events.length+' событий':'';
|
||
const list=document.getElementById('events-list');
|
||
if(!events.length){list.innerHTML='<div style="color:#94A3B8;font-size:13px;padding:4px 0">Активности пока нет</div>';return;}
|
||
list.innerHTML=events.map(e=>`
|
||
<div class="event-row">
|
||
<div class="event-icon">${evIcon(e.event_type)}</div>
|
||
<div><div class="event-title">${escHtml(e.title)}</div><div class="event-ts">${fmtTime(e.created_at)}</div></div>
|
||
</div>`).join('');
|
||
}catch(e){}
|
||
}
|
||
function evIcon(t){
|
||
if(t.startsWith('intake')) return t==='intake_completed'?'✅':'📝';
|
||
if(t.startsWith('interview')) return '📋';
|
||
if(t==='admin_note') return '📌';
|
||
if(t==='chat_message') return '💬';
|
||
if(t==='contract_viewed') return '📄';
|
||
return '📎';
|
||
}
|
||
function fmtTime(ts){
|
||
if(!ts)return'';
|
||
try{const d=new Date(ts.replace(' ','T'));return d.toLocaleDateString('ru',{day:'2-digit',month:'2-digit',year:'numeric'})+' '+d.toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'});}catch(e){return ts;}
|
||
}
|
||
async function addNote(){
|
||
const inp=document.getElementById('note-input');
|
||
const note=inp.value.trim();if(!note)return;
|
||
inp.value='';
|
||
await fetch('/consult/api/admin/events/'+CODE+'/note',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({note})});
|
||
await loadEvents();
|
||
}
|
||
loadEvents();
|
||
|
||
// ── Suggest Questions ─────────────────────────────────────────────────────────
|
||
function openSuggestModal(){
|
||
document.getElementById('suggest-result').innerHTML='<div style="font-size:13px;color:#94A3B8;text-align:center;padding:20px 0">Выберите фазу и нажмите «Сформировать»</div>';
|
||
document.getElementById('suggest-modal').style.display='flex';
|
||
}
|
||
function closeSuggestModal(){document.getElementById('suggest-modal').style.display='none';}
|
||
async function runSuggest(){
|
||
const phase=document.getElementById('suggest-phase').value;
|
||
const btn=document.getElementById('run-suggest-btn');
|
||
const res=document.getElementById('suggest-result');
|
||
btn.disabled=true;btn.textContent='⏳ Анализирую…';
|
||
res.innerHTML='<div style="font-size:13px;color:#64748B;padding:16px 0;text-align:center">AI читает переписку…</div>';
|
||
try{
|
||
const r=await fetch('/consult/api/admin/interview/suggest-questions',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({code:CODE,phase})});
|
||
const d=await r.json();
|
||
if(d.ok&&d.questions&&d.questions.length){
|
||
res.innerHTML=`<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#94A3B8;margin-bottom:10px">${d.questions.length} персонализированных вопросов</div>
|
||
${d.questions.map((q,i)=>`<div style="border:1.5px solid #E5E7EB;border-radius:10px;padding:11px 13px;margin-bottom:7px;background:#FAFAFA">
|
||
<div style="font-size:13px;font-weight:600;color:#0f172a;margin-bottom:3px">${i+1}. ${escHtml(q.text)}</div>
|
||
${q.why?`<div style="font-size:11px;color:#94A3B8">💡 ${escHtml(q.why)}</div>`:''}
|
||
</div>`).join('')}
|
||
<div style="background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;padding:9px 12px;margin-top:8px;font-size:12px;color:#1D4ED8">Скопируйте нужные вопросы для контекста интервью.</div>`;
|
||
} else if(d.error==='no_data'){
|
||
res.innerHTML='<div style="background:#FEF3C7;border:1px solid #FDE68A;border-radius:8px;padding:11px;font-size:13px;color:#92400E">У клиента ещё нет переписки. Попросите клиента написать первые сообщения.</div>';
|
||
} else {
|
||
res.innerHTML=`<div style="background:#FEF2F2;border:1px solid #FECACA;border-radius:8px;padding:11px;font-size:13px;color:#991B1B">Ошибка: ${escHtml(d.error||'неизвестная')}</div>`;
|
||
}
|
||
}catch(e){res.innerHTML='<div style="background:#FEF2F2;border:1px solid #FECACA;border-radius:8px;padding:11px;font-size:13px;color:#991B1B">Ошибка подключения.</div>';}
|
||
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 = `<span style="font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:${s.bg};color:${s.color};margin-left:8px">${s.text}</span>`;
|
||
// 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';
|
||
const sigImgHtml = c.signature_image
|
||
? `<div style="margin-top:10px"><div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#64748B;margin-bottom:6px">Подпись клиента</div><div style="border:1.5px solid #A7F3D0;border-radius:8px;background:#fff;display:inline-block;padding:4px"><img src="${c.signature_image}" style="max-width:280px;height:auto;display:block;border-radius:4px"></div></div>`
|
||
: '';
|
||
signInfo.innerHTML = `✅ <b>Подписан:</b> ${fmtTime(c.signed_at)} · IP: ${c.sign_ip||'—'}${sigImgHtml}`;
|
||
} else {
|
||
signInfo.style.display = 'none';
|
||
}
|
||
} else {
|
||
chip.innerHTML = `<span style="font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:#F1F5F9;color:#94A3B8;margin-left:8px">Не создан</span>`;
|
||
}
|
||
|
||
// Payments
|
||
const payList = document.getElementById('payments-list');
|
||
const payChip = document.getElementById('pay-summary-chip');
|
||
if(!d.payments || !d.payments.length){
|
||
payList.innerHTML = '<div style="color:#94A3B8;font-size:13px">Договор ещё не отправлен клиенту — график платежей сформируется автоматически</div>';
|
||
payChip.textContent = '';
|
||
return;
|
||
}
|
||
const paidColor = d.balance===0 ? '#047857' : '#D97706';
|
||
payChip.textContent = `${fmtRub(d.paid)} из ${fmtRub(d.total)}`;
|
||
payChip.style.color = paidColor;
|
||
|
||
payList.innerHTML = `
|
||
<div style="display:grid;grid-template-columns:1fr 100px 100px 90px 130px;gap:0;border:1.5px solid #E5E7EB;border-radius:10px;overflow:hidden">
|
||
<div style="display:contents;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#64748B">
|
||
<div style="padding:9px 14px;background:#F8FAFC;border-bottom:1px solid #E5E7EB">Этап</div>
|
||
<div style="padding:9px 10px;background:#F8FAFC;border-bottom:1px solid #E5E7EB;text-align:right">Сумма</div>
|
||
<div style="padding:9px 10px;background:#F8FAFC;border-bottom:1px solid #E5E7EB;text-align:right">Оплачено</div>
|
||
<div style="padding:9px 10px;background:#F8FAFC;border-bottom:1px solid #E5E7EB;text-align:center">Статус</div>
|
||
<div style="padding:9px 10px;background:#F8FAFC;border-bottom:1px solid #E5E7EB;text-align:center">Действие</div>
|
||
</div>
|
||
${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
|
||
? '<span style="font-size:10px;font-weight:700;padding:2px 7px;border-radius:10px;background:#ECFDF5;color:#047857">✓ Оплачен</span>'
|
||
: '<span style="font-size:10px;font-weight:700;padding:2px 7px;border-radius:10px;background:#FEF3C7;color:#D97706">Ожидает</span>';
|
||
const border = last?'':'border-bottom:1px solid #E5E7EB';
|
||
const action = isPaid
|
||
? `<button onclick="markPayment(${p.id},0)" style="font-size:11px;color:#94A3B8;background:none;border:none;cursor:pointer;padding:0">↩ Отменить</button>`
|
||
: `<button onclick="openMarkPayModal(${p.id},${p.amount})" style="font-size:11px;font-weight:600;color:#fff;background:#047857;border:none;padding:4px 10px;border-radius:6px;cursor:pointer">✓ Оплачен</button>`;
|
||
const noteSpan = p.note ? `<div style="font-size:11px;color:#94A3B8;margin-top:2px">${p.note}</div>` : '';
|
||
return `<div style="display:contents">
|
||
<div style="padding:10px 14px;background:${rowBg};${border}"><div style="font-size:13px;font-weight:600;color:#0f172a">${p.phase_label}</div>${noteSpan}${p.paid_at?`<div style="font-size:10px;color:#94A3B8">Оплачен: ${fmtTime(p.paid_at)}</div>`:''}</div>
|
||
<div style="padding:10px 10px;background:${rowBg};${border};text-align:right;font-size:13px;font-weight:600;color:#0f172a">${fmtRub(p.amount)}</div>
|
||
<div style="padding:10px 10px;background:${rowBg};${border};text-align:right;font-size:13px;color:${isPaid?'#047857':'#94A3B8'}">${fmtRub(p.paid_amount)}</div>
|
||
<div style="padding:10px 10px;background:${rowBg};${border};text-align:center;display:flex;align-items:center;justify-content:center">${statusBadge}</div>
|
||
<div style="padding:10px 10px;background:${rowBg};${border};text-align:center;display:flex;align-items:center;justify-content:center">${action}</div>
|
||
</div>`;
|
||
}).join('')}
|
||
<div style="display:contents;font-size:13px;font-weight:700;color:#0f172a">
|
||
<div style="padding:10px 14px;background:#F8FAFC;border-top:2px solid #E5E7EB">ИТОГО</div>
|
||
<div style="padding:10px 10px;background:#F8FAFC;border-top:2px solid #E5E7EB;text-align:right">${fmtRub(d.total)}</div>
|
||
<div style="padding:10px 10px;background:#F8FAFC;border-top:2px solid #E5E7EB;text-align:right;color:#047857">${fmtRub(d.paid)}</div>
|
||
<div style="padding:10px 10px;background:#F8FAFC;border-top:2px solid #E5E7EB;text-align:center;font-size:12px;color:${paidColor}">${d.balance===0?'✅ Закрыт':'−'+fmtRub(d.balance)}</div>
|
||
<div style="padding:10px 10px;background:#F8FAFC;border-top:2px solid #E5E7EB"></div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
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');
|
||
status.textContent='Сохраняю…';status.className='save-status';
|
||
const payload={
|
||
client_type:currentType,
|
||
name:document.getElementById('f-name').value,
|
||
contract_no:document.getElementById('f-contract').value,
|
||
email:document.getElementById('f-email').value,
|
||
status:document.getElementById('f-status').value,
|
||
need_text:document.getElementById('f-need').value,
|
||
work_format:document.getElementById('f-work-format').value,
|
||
priority_override:document.getElementById('f-priority-override').value,
|
||
card:collectCard()
|
||
};
|
||
try{
|
||
const res=await fetch('/consult/api/admin/update-client/'+CODE,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload),credentials:'include'});
|
||
if(!res.ok) throw new Error(res.status);
|
||
status.textContent='✓ Сохранено';status.className='save-status ok';
|
||
updateHero();
|
||
setTimeout(()=>{status.textContent='';},3000);
|
||
}catch(e){status.textContent='✗ Ошибка';status.className='save-status err';}
|
||
}
|
||
|
||
// ── Init hero ─────────────────────────────────────────────────────────────────
|
||
updateHero();
|
||
</script>
|
||
</body>
|
||
</html>
|