feat(CRM): мобильный адаптив — гамбургер-меню, drawer-сайдбар, KPI/гриды в 1-2 колонки

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
wasrusgen 2026-06-01 21:52:20 +03:00
parent 11486ce8a8
commit 0a9d924d58

View File

@ -130,12 +130,36 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
.spin{display:inline-block;animation:sp 1s linear infinite}@keyframes sp{to{transform:rotate(360deg)}} .spin{display:inline-block;animation:sp 1s linear infinite}@keyframes sp{to{transform:rotate(360deg)}}
.empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#cbd5e1;gap:10px;height:100%} .empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#cbd5e1;gap:10px;height:100%}
::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12);border-radius:4px} ::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12);border-radius:4px}
/* ── Мобильный (CRM с телефона) ── */
.hdr-burger{display:none;background:none;border:none;color:#fff;font-size:22px;cursor:pointer;padding:0 2px;line-height:1;flex-shrink:0}
.sb-backdrop{display:none;position:fixed;inset:54px 0 0 0;background:rgba(0,0,0,.55);z-index:40}
.sb-backdrop.show{display:block}
@media(max-width:680px){
.hdr{padding:0 12px;gap:8px}
.hdr-burger{display:block}
.sb{position:fixed;left:-260px;top:54px;bottom:0;z-index:50;width:240px;transition:left .25s ease;box-shadow:3px 0 18px rgba(0,0,0,.45)}
.sb.open{left:0}
.main{width:100%}
.scroll{padding:14px}
.kpis{grid-template-columns:1fr 1fr;gap:8px}
.kpi{padding:11px 13px}
.kpi-v{font-size:21px}
.cc-grid{grid-template-columns:1fr 1fr}
.deal-overview{grid-template-columns:1fr!important}
.canvas-grid{grid-template-columns:1fr 1fr}
.mtabs{flex-wrap:nowrap;overflow-x:auto}
.mtab{min-width:auto;padding:9px 12px}
.cc-top{flex-wrap:wrap}
.cc-name{font-size:17px}
.sec-h{font-size:14px}
}
</style> </style>
</head> </head>
<body> <body>
<header class="hdr"><div class="hdr-ic">@</div><div class="hdr-t">wasrusgen1<span class="hdr-sep"></span><b>КОНСАЛТИНГ</b></div><div class="hdr-badge">CRM</div><div class="hdr-r"><span style="width:8px;height:8px;border-radius:50%;background:var(--mid)"></span>Руслан</div></header> <header class="hdr"><button class="hdr-burger" onclick="toggleSb()" aria-label="Меню"></button><div class="hdr-ic">@</div><div class="hdr-t">wasrusgen1<span class="hdr-sep"></span><b>КОНСАЛТИНГ</b></div><div class="hdr-badge">CRM</div><div class="hdr-r"><span style="width:8px;height:8px;border-radius:50%;background:var(--mid)"></span>Руслан</div></header>
<div class="layout"> <div class="layout">
<aside class="sb"> <div class="sb-backdrop" id="sbBackdrop" onclick="toggleSb()"></div>
<aside class="sb" id="sbNav">
<div class="sb-nav"> <div class="sb-nav">
<div class="nav-item active" id="nav-dash" onclick="setView('dashboard')"><span class="ic">📊</span> Дашборд</div> <div class="nav-item active" id="nav-dash" onclick="setView('dashboard')"><span class="ic">📊</span> Дашборд</div>
<div class="nav-item" id="nav-pipe" onclick="setView('pipeline')"><span class="ic">🎯</span> Воронка</div> <div class="nav-item" id="nav-pipe" onclick="setView('pipeline')"><span class="ic">🎯</span> Воронка</div>
@ -155,6 +179,9 @@ const pipeMap=Object.fromEntries(PIPE.map(p=>[p[0],p]));
function esc(s){return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")} function esc(s){return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")} function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")}
function money(n){return (n||0).toLocaleString("ru-RU")+" ₽"} function money(n){return (n||0).toLocaleString("ru-RU")+" ₽"}
// ── Мобильное меню ──
function toggleSb(){const sb=document.getElementById('sbNav'),bd=document.getElementById('sbBackdrop');const o=sb.classList.toggle('open');bd.classList.toggle('show',o);}
function closeSb(){const sb=document.getElementById('sbNav'),bd=document.getElementById('sbBackdrop');if(sb)sb.classList.remove('open');if(bd)bd.classList.remove('show');}
// ── UI-состояние: сворачивание секций + фильтры (запоминается) ── // ── UI-состояние: сворачивание секций + фильтры (запоминается) ──
const ui=(()=>{try{return JSON.parse(localStorage.getItem('crm_ui'))||{}}catch(e){return{}}})(); const ui=(()=>{try{return JSON.parse(localStorage.getItem('crm_ui'))||{}}catch(e){return{}}})();
ui.collapsed=ui.collapsed||{revenue:true}; // динамика свёрнута по умолчанию ui.collapsed=ui.collapsed||{revenue:true}; // динамика свёрнута по умолчанию
@ -195,9 +222,9 @@ async function newClient(){
await fetch(`${API}/api/project/profile`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:d.token,client_name:name,niche,description:""})}); await fetch(`${API}/api/project/profile`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:d.token,client_name:name,niche,description:""})});
await loadProjects();openClient(d.token); await loadProjects();openClient(d.token);
} }
function setView(v){view=v;current=null;document.getElementById("nav-dash").classList.toggle("active",v==="dashboard");document.getElementById("nav-pipe").classList.toggle("active",v==="pipeline");renderClientList();render();} function setView(v){view=v;current=null;if(window.innerWidth<=680)closeSb();document.getElementById("nav-dash").classList.toggle("active",v==="dashboard");document.getElementById("nav-pipe").classList.toggle("active",v==="pipeline");renderClientList();render();}
async function openClient(token){ async function openClient(token){
current=token;view="client";mainTab="deal";editPayIdx=-1;document.querySelectorAll(".nav-item").forEach(n=>n.classList.remove("active")); current=token;view="client";mainTab="deal";editPayIdx=-1;if(window.innerWidth<=680)closeSb();document.querySelectorAll(".nav-item").forEach(n=>n.classList.remove("active"));
const r=await fetch(`${API}/api/project/${token}`);state=await r.json();renderClientList();render();syncPaymentReminders(); const r=await fetch(`${API}/api/project/${token}`);state=await r.json();renderClientList();render();syncPaymentReminders();
} }
function render(){ function render(){
@ -384,7 +411,7 @@ function renderMainPanel(){
<div class="cc-field" id="payStatusBox"></div> <div class="cc-field" id="payStatusBox"></div>
</div> </div>
<div class="cc-actions"><button class="btn btn-p" onclick="inviteLink()">🔗 Ссылка клиенту</button><button class="btn btn-p" style="background:#0088cc" onclick="inviteTelegram()">В Telegram</button><a class="btn btn-g" href="cabinet.html?t=${current}" target="_blank">👁 Открыть кабинет</a><button class="btn btn-g" style="margin-left:auto;border-color:#FECACA;color:#DC2626" onclick="deleteClient()">🗑 Удалить</button></div> <div class="cc-actions"><button class="btn btn-p" onclick="inviteLink()">🔗 Ссылка клиенту</button><button class="btn btn-p" style="background:#0088cc" onclick="inviteTelegram()">В Telegram</button><a class="btn btn-g" href="cabinet.html?t=${current}" target="_blank">👁 Открыть кабинет</a><button class="btn btn-g" style="margin-left:auto;border-color:#FECACA;color:#DC2626" onclick="deleteClient()">🗑 Удалить</button></div>
<div style="display:grid;grid-template-columns:1.35fr 1fr;gap:14px;align-items:start"> <div class="deal-overview" style="display:grid;grid-template-columns:1.35fr 1fr;gap:14px;align-items:start">
<div class="blk" style="margin:0"> <div class="blk" style="margin:0">
<div style="display:flex;align-items:center;margin-bottom:6px"><b style="font-size:13px">📍 Прогресс проекта</b><span style="margin-left:auto;font-size:11px;color:var(--muted)">${doneCnt}/5 этапов</span></div> <div style="display:flex;align-items:center;margin-bottom:6px"><b style="font-size:13px">📍 Прогресс проекта</b><span style="margin-left:auto;font-size:11px;color:var(--muted)">${doneCnt}/5 этапов</span></div>
${CLIENT_STAGES.map((s,i)=>{ ${CLIENT_STAGES.map((s,i)=>{