mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 17:44:46 +00:00
feat(платежи): нераспределённый остаток, сроки этапов (график), нал/безнал, клиент видит даты
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b965d689d8
commit
066a628695
@ -645,7 +645,7 @@ def update_crm():
|
|||||||
"pipeline": "lead", "deal_amount": 0, "paid_amount": 0,
|
"pipeline": "lead", "deal_amount": 0, "paid_amount": 0,
|
||||||
"contact": "", "source": "", "note": "", "payments": [], "billing_type": "paid"
|
"contact": "", "source": "", "note": "", "payments": [], "billing_type": "paid"
|
||||||
}
|
}
|
||||||
for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments", "billing_type", "payment_schedule", "stage_payments", "stage_prices", "complexity"):
|
for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments", "billing_type", "payment_schedule", "stage_payments", "stage_prices", "complexity", "stage_due"):
|
||||||
if k in data: crm[k] = data[k]
|
if k in data: crm[k] = data[k]
|
||||||
# paid_amount = сумма платежей (если есть реестр)
|
# paid_amount = сумма платежей (если есть реестр)
|
||||||
if "payments" in crm and isinstance(crm["payments"], list):
|
if "payments" in crm and isinstance(crm["payments"], list):
|
||||||
|
|||||||
@ -531,6 +531,8 @@ function renderEstimateCard(){
|
|||||||
const sp=crm.stage_prices;
|
const sp=crm.stage_prices;
|
||||||
if(!sp)return ''; // смета ещё не сформирована — не показываем
|
if(!sp)return ''; // смета ещё не сформирована — не показываем
|
||||||
const pays=crm.stage_payments||{};
|
const pays=crm.stage_payments||{};
|
||||||
|
const due=crm.stage_due||{};
|
||||||
|
const fmtD=d=>{if(!d)return'';const a=d.split('-');return a[2]+'.'+a[1]+'.'+a[0].slice(2);};
|
||||||
const total=Object.values(sp).reduce((a,b)=>a+(+b||0),0);
|
const total=Object.values(sp).reduce((a,b)=>a+(+b||0),0);
|
||||||
const paid=Object.values(pays).reduce((s,p)=>s+(p.amount||0),0);
|
const paid=Object.values(pays).reduce((s,p)=>s+(p.amount||0),0);
|
||||||
return `<div class="exp-bar" style="margin-bottom:14px">
|
return `<div class="exp-bar" style="margin-bottom:14px">
|
||||||
@ -538,14 +540,16 @@ function renderEstimateCard(){
|
|||||||
<div class="exp-d" style="margin-bottom:10px">Прозрачно: за что и сколько. Интервью — бесплатно, дальше помодульно. Готовое ТЗ — после полной оплаты.</div>
|
<div class="exp-d" style="margin-bottom:10px">Прозрачно: за что и сколько. Интервью — бесплатно, дальше помодульно. Готовое ТЗ — после полной оплаты.</div>
|
||||||
${EST_STAGES.map(s=>{
|
${EST_STAGES.map(s=>{
|
||||||
const price=+sp[s.key]||0, isFree=price<=0, isPaid=!!pays[s.key], done=estStageDone(s.key);
|
const price=+sp[s.key]||0, isFree=price<=0, isPaid=!!pays[s.key], done=estStageDone(s.key);
|
||||||
|
const dd=due[s.key]||'';
|
||||||
let tag='';
|
let tag='';
|
||||||
if(isFree)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">бесплатно</span>';
|
if(isFree)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">бесплатно</span>';
|
||||||
else if(isPaid)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">✓ оплачено</span>';
|
else if(isPaid)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">✓ оплачено</span>';
|
||||||
else if(done)tag='<span style="font-size:10px;font-weight:700;color:#92400E;background:#FEF3C7;padding:1px 7px;border-radius:5px">к оплате</span>';
|
else if(done)tag='<span style="font-size:10px;font-weight:700;color:#92400E;background:#FEF3C7;padding:1px 7px;border-radius:5px">к оплате</span>';
|
||||||
else tag='<span style="font-size:10px;font-weight:700;color:#9CA3AF;background:#F3F4F6;padding:1px 7px;border-radius:5px">в работе</span>';
|
else tag='<span style="font-size:10px;font-weight:700;color:#9CA3AF;background:#F3F4F6;padding:1px 7px;border-radius:5px">в работе</span>';
|
||||||
|
const dueChip=(dd&&!isPaid&&!isFree)?`<span style="font-size:10px;font-weight:600;color:#2563EB;background:#EFF6FF;padding:1px 7px;border-radius:5px">📅 до ${fmtD(dd)}</span>`:'';
|
||||||
return `<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-top:1px solid rgba(0,0,0,.06)">
|
return `<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-top:1px solid rgba(0,0,0,.06)">
|
||||||
<span style="font-size:15px">${s.icon}</span>
|
<span style="font-size:15px">${s.icon}</span>
|
||||||
<div style="flex:1;min-width:0"><div style="font-size:13px;font-weight:700;display:flex;align-items:center;gap:7px;flex-wrap:wrap">${s.name} ${tag}</div><div style="font-size:11px;color:#6b7280;margin-top:1px">${s.desc}</div></div>
|
<div style="flex:1;min-width:0"><div style="font-size:13px;font-weight:700;display:flex;align-items:center;gap:7px;flex-wrap:wrap">${s.name} ${tag} ${dueChip}</div><div style="font-size:11px;color:#6b7280;margin-top:1px">${s.desc}</div></div>
|
||||||
<span style="font-size:13px;font-weight:700;color:${isFree?'#9CA3AF':'#047857'};white-space:nowrap">${isFree?'0 ₽':money(price)}</span>
|
<span style="font-size:13px;font-weight:700;color:${isFree?'#9CA3AF':'#047857'};white-space:nowrap">${isFree?'0 ₽':money(price)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
|
|||||||
@ -450,9 +450,27 @@ function editStagePrice(k){
|
|||||||
state.crm=state.crm||{};
|
state.crm=state.crm||{};
|
||||||
state.crm.stage_prices=state.crm.stage_prices||{};
|
state.crm.stage_prices=state.crm.stage_prices||{};
|
||||||
state.crm.stage_prices[k]=n;
|
state.crm.stage_prices[k]=n;
|
||||||
state.crm.deal_amount=recalcDeal(state.crm.stage_prices);
|
// сумму сделки НЕ трогаем — разница показывается как «нераспределено»
|
||||||
saveEstimate();renderClient();
|
saveEstimate();renderClient();
|
||||||
}
|
}
|
||||||
|
// ── Сроки этапов (плановая дата оплаты) ──
|
||||||
|
function getStageDue(){return (state.crm&&state.crm.stage_due)||{};}
|
||||||
|
function setStageDue(k,v){
|
||||||
|
state.crm=state.crm||{};state.crm.stage_due=state.crm.stage_due||{};
|
||||||
|
if(v)state.crm.stage_due[k]=v; else delete state.crm.stage_due[k];
|
||||||
|
fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage_due:state.crm.stage_due})}).then(loadProjects);
|
||||||
|
}
|
||||||
|
// ── Подогнать сумму сделки под смету ──
|
||||||
|
function alignDealToEstimate(){
|
||||||
|
state.crm=state.crm||{};
|
||||||
|
state.crm.deal_amount=recalcDeal(getStagePrices()||{});
|
||||||
|
saveEstimate();renderClient();
|
||||||
|
}
|
||||||
|
// ── Способы оплаты ──
|
||||||
|
const PAY_METHODS=[["bank","🏦 Безнал"],["cash","💵 Наличные"],["sbp","📱 СБП"],["card","💳 Карта"]];
|
||||||
|
const METHOD_ICON={bank:"🏦",cash:"💵",sbp:"📱",card:"💳"};
|
||||||
|
const METHOD_NAME=Object.fromEntries(PAY_METHODS.map(m=>[m[0],m[1]]));
|
||||||
|
function methodSelect(id,cur){return `<select id="${id}" style="border:1.5px solid var(--border);border-radius:8px;padding:7px 9px;font-size:13px;font-family:Inter;background:var(--white)">${PAY_METHODS.map(([k,n])=>`<option value="${k}" ${cur===k?'selected':''}>${n}</option>`).join('')}</select>`;}
|
||||||
async function saveEstimate(){
|
async function saveEstimate(){
|
||||||
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage_prices:state.crm.stage_prices,complexity:state.crm.complexity,deal_amount:state.crm.deal_amount})});
|
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage_prices:state.crm.stage_prices,complexity:state.crm.complexity,deal_amount:state.crm.deal_amount})});
|
||||||
await loadProjects();
|
await loadProjects();
|
||||||
@ -477,7 +495,7 @@ function markStagePayInput(k){
|
|||||||
const wrap=document.createElement('div');
|
const wrap=document.createElement('div');
|
||||||
wrap.id=fid;
|
wrap.id=fid;
|
||||||
wrap.style.cssText='display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;align-items:center;padding:10px 12px;background:var(--bg);border-radius:10px;border:1.5px solid #D1FAE5';
|
wrap.style.cssText='display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;align-items:center;padding:10px 12px;background:var(--bg);border-radius:10px;border:1.5px solid #D1FAE5';
|
||||||
wrap.innerHTML=`<input type="number" id="spa-${k}" placeholder="Сумма ₽" value="${suggested}" style="width:110px;border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-size:13px;font-family:Inter"><input type="date" id="spd-${k}" value="${today}" style="border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-size:13px;font-family:Inter"><input id="spn-${k}" placeholder="Назначение" style="flex:1;min-width:120px;border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-size:13px;font-family:Inter"><button class="cp-btn cp-a" style="padding:7px 14px" onclick="confirmStagePay('${k}')">Сохранить</button><button class="cp-btn cp-r" style="padding:7px 12px" onclick="document.getElementById('${fid}').remove()">✕</button>`;
|
wrap.innerHTML=`<input type="number" id="spa-${k}" placeholder="Сумма ₽" value="${suggested}" style="width:110px;border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-size:13px;font-family:Inter"><input type="date" id="spd-${k}" value="${today}" style="border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-size:13px;font-family:Inter">${methodSelect('spm-'+k,'bank')}<input id="spn-${k}" placeholder="Назначение" style="flex:1;min-width:120px;border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-size:13px;font-family:Inter"><button class="cp-btn cp-a" style="padding:7px 14px" onclick="confirmStagePay('${k}')">Сохранить</button><button class="cp-btn cp-r" style="padding:7px 12px" onclick="document.getElementById('${fid}').remove()">✕</button>`;
|
||||||
const row=document.getElementById('stagerow-'+k);
|
const row=document.getElementById('stagerow-'+k);
|
||||||
if(row)row.parentNode.insertBefore(wrap,row.nextSibling);
|
if(row)row.parentNode.insertBefore(wrap,row.nextSibling);
|
||||||
}
|
}
|
||||||
@ -485,14 +503,15 @@ async function confirmStagePay(k){
|
|||||||
const amt=+document.getElementById('spa-'+k).value;
|
const amt=+document.getElementById('spa-'+k).value;
|
||||||
const date=document.getElementById('spd-'+k).value||new Date().toISOString().slice(0,10);
|
const date=document.getElementById('spd-'+k).value||new Date().toISOString().slice(0,10);
|
||||||
const note=document.getElementById('spn-'+k).value;
|
const note=document.getElementById('spn-'+k).value;
|
||||||
|
const method=(document.getElementById('spm-'+k)||{}).value||'bank';
|
||||||
if(!amt){alert('Укажите сумму');return;}
|
if(!amt){alert('Укажите сумму');return;}
|
||||||
const pays=getStagePays();if(pays[k])return;
|
const pays=getStagePays();if(pays[k])return;
|
||||||
const stageName=CLIENT_STAGES.find(s=>s.key===k).name;
|
const stageName=CLIENT_STAGES.find(s=>s.key===k).name;
|
||||||
pays[k]={amount:amt,date};
|
pays[k]={amount:amt,date,method};
|
||||||
state.crm=state.crm||{};
|
state.crm=state.crm||{};
|
||||||
state.crm.stage_payments=pays;
|
state.crm.stage_payments=pays;
|
||||||
state.crm.payments=state.crm.payments||[];
|
state.crm.payments=state.crm.payments||[];
|
||||||
state.crm.payments.push({date,amount:amt,note:note||'Этап: '+stageName,stage:k});
|
state.crm.payments.push({date,amount:amt,note:note||'Этап: '+stageName,stage:k,method});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage_payments:pays,payments:state.crm.payments})}),
|
fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage_payments:pays,payments:state.crm.payments})}),
|
||||||
loadProjects()
|
loadProjects()
|
||||||
@ -533,8 +552,12 @@ function renderPaymentPlan(){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const total=recalcDeal(sp);
|
const allocated=recalcDeal(sp);
|
||||||
const isUnlocked=billing==="free"||(total>0&&paidTotal>=total);
|
const deal=(state.crm&&state.crm.deal_amount)||0;
|
||||||
|
const target=deal||allocated; // сумма сделки = цель оплаты
|
||||||
|
const unalloc=target-allocated; // нераспределённый остаток
|
||||||
|
const due=getStageDue();
|
||||||
|
const isUnlocked=billing==="free"||(target>0&&paidTotal>=target);
|
||||||
const cx=getComplexity();
|
const cx=getComplexity();
|
||||||
// первый ещё не оплаченный платный выполненный модуль — «доступен к оплате»
|
// первый ещё не оплаченный платный выполненный модуль — «доступен к оплате»
|
||||||
box.innerHTML=`<div class="blk" style="margin-bottom:14px">
|
box.innerHTML=`<div class="blk" style="margin-bottom:14px">
|
||||||
@ -543,7 +566,7 @@ function renderPaymentPlan(){
|
|||||||
<span style="font-size:11px;font-weight:700;color:var(--primary);background:#ECFDF5;padding:3px 9px;border-radius:6px">${CX_SHORT[cx]}</span>
|
<span style="font-size:11px;font-weight:700;color:var(--primary);background:#ECFDF5;padding:3px 9px;border-radius:6px">${CX_SHORT[cx]}</span>
|
||||||
<button class="cp-btn cp-r" style="padding:4px 9px;margin-left:auto" onclick="buildEstimate()">↻ Пересчитать</button>
|
<button class="cp-btn cp-r" style="padding:4px 9px;margin-left:auto" onclick="buildEstimate()">↻ Пересчитать</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:11px;color:var(--muted);margin-bottom:12px">Получено <b style="color:var(--primary)">${money(paidTotal)}</b> из ${money(total)} · <b>✎</b> — изменить цену · <b style="color:#DC2626">↺</b> — отменить оплату · «↻ Пересчитать» сбрасывает к ставкам</div>
|
<div style="font-size:11px;color:var(--muted);margin-bottom:12px">Получено <b style="color:var(--primary)">${money(paidTotal)}</b> из ${money(target)} · <b>✎</b> цена · <b style="color:#DC2626">↺</b> отмена оплаты · 📅 срок оплаты этапа</div>
|
||||||
${CLIENT_STAGES.map(s=>{
|
${CLIENT_STAGES.map(s=>{
|
||||||
const done=stageIsDone(s.key);
|
const done=stageIsDone(s.key);
|
||||||
const paid=pays[s.key];
|
const paid=pays[s.key];
|
||||||
@ -567,23 +590,35 @@ function renderPaymentPlan(){
|
|||||||
sub=`<div style="font-size:11px;color:#CBD5E1;margin-top:2px">${esc(s.desc)}</div>`;
|
sub=`<div style="font-size:11px;color:#CBD5E1;margin-top:2px">${esc(s.desc)}</div>`;
|
||||||
action=`<span onclick="editStagePrice('${s.key}')" style="cursor:pointer;font-size:11px;font-weight:700;color:#9CA3AF;white-space:nowrap;border-bottom:1px dashed #CBD5E1">${money(price)}</span>`;
|
action=`<span onclick="editStagePrice('${s.key}')" style="cursor:pointer;font-size:11px;font-weight:700;color:#9CA3AF;white-space:nowrap;border-bottom:1px dashed #CBD5E1">${money(price)}</span>`;
|
||||||
}
|
}
|
||||||
|
const dd=due[s.key]||'';
|
||||||
|
const dueChip=dd?`<span style="font-size:10px;font-weight:600;color:#2563EB;background:#EFF6FF;padding:1px 6px;border-radius:4px;margin-left:6px">📅 до ${fmtDate(dd)}</span>`:'';
|
||||||
|
const dueInput=isFree?'':`<input type="date" value="${dd}" title="Срок оплаты этапа" onchange="setStageDue('${s.key}',this.value)" style="border:1.5px solid var(--border);border-radius:7px;padding:5px 7px;font-size:11px;font-family:Inter;color:#6B7280;width:130px">`;
|
||||||
return `<div id="stagerow-${s.key}" style="display:flex;align-items:center;gap:12px;padding:11px 0;border-top:1px solid var(--bg)">
|
return `<div id="stagerow-${s.key}" style="display:flex;align-items:center;gap:12px;padding:11px 0;border-top:1px solid var(--bg)">
|
||||||
<span style="width:32px;height:32px;border-radius:8px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:15px;background:${paid?'#D1FAE5':done||isFree?'#ECFDF5':'#F9FAFB'};border:1.5px solid ${paid?'#6EE7B7':done||isFree?'#A7F3D0':'#E5E7EB'}">${s.icon}</span>
|
<span style="width:32px;height:32px;border-radius:8px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:15px;background:${paid?'#D1FAE5':done||isFree?'#ECFDF5':'#F9FAFB'};border:1.5px solid ${paid?'#6EE7B7':done||isFree?'#A7F3D0':'#E5E7EB'}">${s.icon}</span>
|
||||||
<div style="flex:1;min-width:0">
|
<div style="flex:1;min-width:0">
|
||||||
<div style="font-weight:700;font-size:13px;color:${done||isFree?'var(--text)':'#9CA3AF'}">${s.name}${badge}</div>
|
<div style="font-weight:700;font-size:13px;color:${done||isFree?'var(--text)':'#9CA3AF'}">${s.name}${badge}${dueChip}</div>
|
||||||
${sub}
|
${sub}
|
||||||
</div>
|
</div>
|
||||||
|
${dueInput}
|
||||||
<div style="display:flex;align-items:center;gap:7px">${action}<span onclick="event.stopPropagation();editStagePrice('${s.key}')" title="Изменить цену этапа" style="cursor:pointer;color:#9CA3AF;font-size:14px;line-height:1">✎</span>${paid?`<span onclick="event.stopPropagation();unStagePay('${s.key}')" title="Отменить оплату" style="cursor:pointer;color:#DC2626;font-size:14px;line-height:1">↺</span>`:''}</div>
|
<div style="display:flex;align-items:center;gap:7px">${action}<span onclick="event.stopPropagation();editStagePrice('${s.key}')" title="Изменить цену этапа" style="cursor:pointer;color:#9CA3AF;font-size:14px;line-height:1">✎</span>${paid?`<span onclick="event.stopPropagation();unStagePay('${s.key}')" title="Отменить оплату" style="cursor:pointer;color:#DC2626;font-size:14px;line-height:1">↺</span>`:''}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("")}
|
}).join("")}
|
||||||
<div style="display:flex;align-items:center;gap:10px;padding:12px 0 2px;border-top:2px solid var(--bg);margin-top:4px">
|
<div style="display:flex;align-items:center;gap:10px;padding:12px 0 4px;border-top:2px solid var(--bg);margin-top:4px">
|
||||||
<span style="font-size:13px;font-weight:800;font-family:Montserrat">Итого по проекту</span>
|
<span style="font-size:13px;font-weight:800;font-family:Montserrat">Распределено по этапам</span>
|
||||||
<span style="margin-left:auto;font-size:18px;font-weight:800;color:var(--primary)">${money(total)}</span>
|
<span style="margin-left:auto;font-size:16px;font-weight:800;color:var(--text)">${money(allocated)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;padding:4px 0;font-size:12px;color:var(--muted)">
|
||||||
|
<span>Сумма сделки</span>
|
||||||
|
<span style="margin-left:auto;font-weight:700;color:var(--primary)">${money(target)}</span>
|
||||||
|
</div>
|
||||||
|
${unalloc!==0?`<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;margin-top:4px;border-radius:9px;background:${unalloc>0?'#FFF7ED':'#FEF2F2'};border:1px solid ${unalloc>0?'#FDE68A':'#FECACA'}">
|
||||||
|
<span style="font-size:12px;font-weight:700;color:${unalloc>0?'#92400E':'#DC2626'}">${unalloc>0?'⚠ Нераспределено':'⚠ Перебор сметы'}: ${money(Math.abs(unalloc))}</span>
|
||||||
|
<button class="cp-btn cp-r" style="margin-left:auto;padding:4px 10px" onclick="alignDealToEstimate()">Подогнать сделку под смету</button>
|
||||||
|
</div>`:''}
|
||||||
<div style="margin-top:12px;padding:11px 14px;border-radius:10px;font-size:12px;line-height:1.5;${isUnlocked?'background:#ECFDF5;border:1px solid #6EE7B7;color:#047857':'background:#FFF7ED;border:1px solid #FDE68A;color:#92400E'}">
|
<div style="margin-top:12px;padding:11px 14px;border-radius:10px;font-size:12px;line-height:1.5;${isUnlocked?'background:#ECFDF5;border:1px solid #6EE7B7;color:#047857':'background:#FFF7ED;border:1px solid #FDE68A;color:#92400E'}">
|
||||||
${isUnlocked
|
${isUnlocked
|
||||||
?`✅ <b>Оплата получена.</b> Клиент может скачать ТЗ и печатные документы.`
|
?`✅ <b>Оплата получена.</b> Клиент может скачать ТЗ и печатные документы.`
|
||||||
:`🔒 <b>Правило:</b> ТЗ (печать + выгрузка) выдаётся клиенту только после полной оплаты — ${money(total)}.`}
|
:`🔒 <b>Правило:</b> ТЗ (печать + выгрузка) выдаётся клиенту только после полной оплаты — ${money(target)}.`}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@ -619,6 +654,8 @@ function applyPrice(price){
|
|||||||
function renderPayments(){
|
function renderPayments(){
|
||||||
const crm=state.crm||{};const deal=crm.deal_amount||0;const pays=crm.payments||[];
|
const crm=state.crm||{};const deal=crm.deal_amount||0;const pays=crm.payments||[];
|
||||||
const paid=pays.reduce((s,p)=>s+(p.amount||0),0);const left=deal-paid;
|
const paid=pays.reduce((s,p)=>s+(p.amount||0),0);const left=deal-paid;
|
||||||
|
const cash=pays.filter(p=>p.method==='cash').reduce((s,p)=>s+(p.amount||0),0);
|
||||||
|
const noncash=paid-cash;
|
||||||
const st=paid<=0?["Не оплачено","#DC2626","#FEF2F2"]:left>0?["Частично","#92400E","#FEF3C7"]:["Оплачено","#047857","#ECFDF5"];
|
const st=paid<=0?["Не оплачено","#DC2626","#FEF2F2"]:left>0?["Частично","#92400E","#FEF3C7"]:["Оплачено","#047857","#ECFDF5"];
|
||||||
const sb=document.getElementById("payStatusBox");
|
const sb=document.getElementById("payStatusBox");
|
||||||
if(sb)sb.innerHTML=`<div class="cc-fl">Оплата</div><div style="display:flex;align-items:center;gap:8px"><span style="font-size:14px;font-weight:700;color:${st[1]}">${money(paid)}</span>${deal>0?`<span style="font-size:11px;font-weight:700;color:${st[1]};background:${st[2]};padding:2px 8px;border-radius:6px">${st[0]}</span>`:''}</div>`;
|
if(sb)sb.innerHTML=`<div class="cc-fl">Оплата</div><div style="display:flex;align-items:center;gap:8px"><span style="font-size:14px;font-weight:700;color:${st[1]}">${money(paid)}</span>${deal>0?`<span style="font-size:11px;font-weight:700;color:${st[1]};background:${st[2]};padding:2px 8px;border-radius:6px">${st[0]}</span>`:''}</div>`;
|
||||||
@ -628,16 +665,18 @@ function renderPayments(){
|
|||||||
${deal>0?`<span style="margin-left:auto;font-size:12px;color:var(--muted)">Сделка ${money(deal)} · Получено ${money(paid)} · <b style="color:${left>0?'#DC2626':'#047857'}">Остаток ${money(left)}</b></span>`:'<span style="margin-left:auto"></span>'}
|
${deal>0?`<span style="margin-left:auto;font-size:12px;color:var(--muted)">Сделка ${money(deal)} · Получено ${money(paid)} · <b style="color:${left>0?'#DC2626':'#047857'}">Остаток ${money(left)}</b></span>`:'<span style="margin-left:auto"></span>'}
|
||||||
${left>0?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="remindBalance(${left})">⏰ Задача на остаток</button>`:''}
|
${left>0?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="remindBalance(${left})">⏰ Задача на остаток</button>`:''}
|
||||||
${pays.length?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="exportPayments()">⬇ CSV</button>`:''}</div>
|
${pays.length?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="exportPayments()">⬇ CSV</button>`:''}</div>
|
||||||
|
${paid>0?`<div style="display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap"><span style="font-size:11px;font-weight:700;color:#047857;background:#ECFDF5;padding:3px 10px;border-radius:6px">💵 Наличные ${money(cash)}</span><span style="font-size:11px;font-weight:700;color:#2563EB;background:#EFF6FF;padding:3px 10px;border-radius:6px">🏦 Безнал ${money(noncash)}</span></div>`:''}
|
||||||
${pays.map((p,i)=>i===editPayIdx
|
${pays.map((p,i)=>i===editPayIdx
|
||||||
?`<div style="display:flex;align-items:center;gap:8px;padding:9px 10px;border-top:1px solid var(--bg);flex-wrap:wrap;background:var(--bg);border-radius:8px;margin-top:4px">
|
?`<div style="display:flex;align-items:center;gap:8px;padding:9px 10px;border-top:1px solid var(--bg);flex-wrap:wrap;background:var(--bg);border-radius:8px;margin-top:4px">
|
||||||
<input type="date" id="epd-${i}" value="${esc(p.date||'')}" style="border:1.5px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;font-family:Inter">
|
<input type="date" id="epd-${i}" value="${esc(p.date||'')}" style="border:1.5px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;font-family:Inter">
|
||||||
<input type="number" id="epa-${i}" value="${p.amount||''}" style="width:100px;border:1.5px solid var(--border);border-radius:8px;padding:6px 9px;font-size:13px;font-family:Inter">
|
<input type="number" id="epa-${i}" value="${p.amount||''}" style="width:100px;border:1.5px solid var(--border);border-radius:8px;padding:6px 9px;font-size:13px;font-family:Inter">
|
||||||
|
${methodSelect('epm-'+i,p.method||'bank')}
|
||||||
<input id="epn-${i}" value="${esc(p.note||'')}" placeholder="Назначение" style="flex:1;min-width:120px;border:1.5px solid var(--border);border-radius:8px;padding:6px 9px;font-size:13px;font-family:Inter">
|
<input id="epn-${i}" value="${esc(p.note||'')}" placeholder="Назначение" style="flex:1;min-width:120px;border:1.5px solid var(--border);border-radius:8px;padding:6px 9px;font-size:13px;font-family:Inter">
|
||||||
${p.stage?`<span style="font-size:10px;color:#9CA3AF;white-space:nowrap">этап: ${esc((CLIENT_STAGES.find(s=>s.key===p.stage)||{}).name||p.stage)}</span>`:''}
|
${p.stage?`<span style="font-size:10px;color:#9CA3AF;white-space:nowrap">этап: ${esc((CLIENT_STAGES.find(s=>s.key===p.stage)||{}).name||p.stage)}</span>`:''}
|
||||||
<button class="cp-btn cp-a" style="padding:6px 12px" onclick="confirmEditPayment(${i})">Сохранить</button>
|
<button class="cp-btn cp-a" style="padding:6px 12px" onclick="confirmEditPayment(${i})">Сохранить</button>
|
||||||
<button class="cp-btn cp-r" style="padding:6px 10px" onclick="cancelEditPayment()">✕</button>
|
<button class="cp-btn cp-r" style="padding:6px 10px" onclick="cancelEditPayment()">✕</button>
|
||||||
</div>`
|
</div>`
|
||||||
:`<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)"><span style="font-size:12px;color:var(--muted);min-width:70px">${esc(p.date||'')}</span><span style="flex:1;font-size:13px">${esc(p.note||'Платёж')}${p.stage?` <span style="font-size:10px;color:#9CA3AF">· этап</span>`:''}</span><span style="font-size:13px;font-weight:700;color:#047857">${money(p.amount)}</span><span onclick="editPayment(${i})" title="Изменить" style="cursor:pointer;color:#9CA3AF;font-size:13px">✎</span><button onclick="delPayment(${i})" title="Удалить" style="border:none;background:none;cursor:pointer;color:#cbd5e1;font-size:14px">✕</button></div>`).join("")||'<div style="font-size:12px;color:#cbd5e1;padding:4px">Платежей нет</div>'}
|
:`<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)"><span style="font-size:12px;color:var(--muted);min-width:70px">${esc(p.date||'')}</span><span title="${esc(METHOD_NAME[p.method||'bank']||'')}" style="font-size:13px">${METHOD_ICON[p.method||'bank']||'🏦'}</span><span style="flex:1;font-size:13px">${esc(p.note||'Платёж')}${p.stage?` <span style="font-size:10px;color:#9CA3AF">· этап</span>`:''}</span><span style="font-size:13px;font-weight:700;color:#047857">${money(p.amount)}</span><span onclick="editPayment(${i})" title="Изменить" style="cursor:pointer;color:#9CA3AF;font-size:13px">✎</span><button onclick="delPayment(${i})" title="Удалить" style="border:none;background:none;cursor:pointer;color:#cbd5e1;font-size:14px">✕</button></div>`).join("")||'<div style="font-size:12px;color:#cbd5e1;padding:4px">Платежей нет</div>'}
|
||||||
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;align-items:center">
|
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;align-items:center">
|
||||||
<input id="payDate" type="date" value="${new Date().toISOString().slice(0,10)}" style="border:1.5px solid var(--border);border-radius:8px;padding:8px;font-size:13px;font-family:Inter">
|
<input id="payDate" type="date" value="${new Date().toISOString().slice(0,10)}" style="border:1.5px solid var(--border);border-radius:8px;padding:8px;font-size:13px;font-family:Inter">
|
||||||
<input id="payAmt" type="number" placeholder="Сумма ₽" style="width:110px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter" onkeydown="if(event.key==='Enter')addPayment()">
|
<input id="payAmt" type="number" placeholder="Сумма ₽" style="width:110px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter" onkeydown="if(event.key==='Enter')addPayment()">
|
||||||
@ -645,6 +684,7 @@ function renderPayments(){
|
|||||||
<option value="">— без этапа —</option>
|
<option value="">— без этапа —</option>
|
||||||
${CLIENT_STAGES.filter(s=>stageIsDone(s.key)&&!getStagePays()[s.key]&&((getStagePrices()||{})[s.key]||0)>0).map(s=>`<option value="${s.key}">${s.name} · ${money((getStagePrices()||{})[s.key])}</option>`).join('')}
|
${CLIENT_STAGES.filter(s=>stageIsDone(s.key)&&!getStagePays()[s.key]&&((getStagePrices()||{})[s.key]||0)>0).map(s=>`<option value="${s.key}">${s.name} · ${money((getStagePrices()||{})[s.key])}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
|
${methodSelect('payMethod','bank')}
|
||||||
<input id="payNote" placeholder="Назначение" style="flex:1;min-width:120px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter" onkeydown="if(event.key==='Enter')addPayment()">
|
<input id="payNote" placeholder="Назначение" style="flex:1;min-width:120px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter" onkeydown="if(event.key==='Enter')addPayment()">
|
||||||
<button class="cp-btn cp-a" onclick="addPayment()">+ Платёж</button>
|
<button class="cp-btn cp-a" onclick="addPayment()">+ Платёж</button>
|
||||||
</div>
|
</div>
|
||||||
@ -662,9 +702,10 @@ function confirmEditPayment(i){
|
|||||||
if(!amt){alert('Укажите сумму');return;}
|
if(!amt){alert('Укажите сумму');return;}
|
||||||
const date=document.getElementById('epd-'+i).value||p.date;
|
const date=document.getElementById('epd-'+i).value||p.date;
|
||||||
const note=document.getElementById('epn-'+i).value;
|
const note=document.getElementById('epn-'+i).value;
|
||||||
p.amount=amt;p.date=date;p.note=note||'Платёж';
|
const method=(document.getElementById('epm-'+i)||{}).value||p.method||'bank';
|
||||||
|
p.amount=amt;p.date=date;p.note=note||'Платёж';p.method=method;
|
||||||
if(p.stage&&state.crm.stage_payments&&state.crm.stage_payments[p.stage]){
|
if(p.stage&&state.crm.stage_payments&&state.crm.stage_payments[p.stage]){
|
||||||
state.crm.stage_payments[p.stage]={amount:amt,date};
|
state.crm.stage_payments[p.stage]={amount:amt,date,method};
|
||||||
}
|
}
|
||||||
editPayIdx=-1;
|
editPayIdx=-1;
|
||||||
savePayments();renderClient();
|
savePayments();renderClient();
|
||||||
@ -674,13 +715,14 @@ function addPayment(){
|
|||||||
const date=document.getElementById("payDate").value;
|
const date=document.getElementById("payDate").value;
|
||||||
const note=document.getElementById("payNote").value;
|
const note=document.getElementById("payNote").value;
|
||||||
const stageKey=document.getElementById("payStage")?.value||"";
|
const stageKey=document.getElementById("payStage")?.value||"";
|
||||||
|
const method=document.getElementById("payMethod")?.value||"bank";
|
||||||
const stageName=stageKey?CLIENT_STAGES.find(s=>s.key===stageKey)?.name:"";
|
const stageName=stageKey?CLIENT_STAGES.find(s=>s.key===stageKey)?.name:"";
|
||||||
state.crm=state.crm||{};
|
state.crm=state.crm||{};
|
||||||
state.crm.payments=state.crm.payments||[];
|
state.crm.payments=state.crm.payments||[];
|
||||||
state.crm.payments.push({date,amount:amt,note:note||(stageName?"Этап: "+stageName:"Платёж"),stage:stageKey||null});
|
state.crm.payments.push({date,amount:amt,note:note||(stageName?"Этап: "+stageName:"Платёж"),stage:stageKey||null,method});
|
||||||
if(stageKey&&!getStagePays()[stageKey]){
|
if(stageKey&&!getStagePays()[stageKey]){
|
||||||
state.crm.stage_payments=state.crm.stage_payments||{};
|
state.crm.stage_payments=state.crm.stage_payments||{};
|
||||||
state.crm.stage_payments[stageKey]={amount:amt,date};
|
state.crm.stage_payments[stageKey]={amount:amt,date,method};
|
||||||
}
|
}
|
||||||
savePayments();renderClient();
|
savePayments();renderClient();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user