@@ -22,8 +22,8 @@ function renderPlan() {
const months = displayMonths . map ( d => d . key ) ;
const monthLabels = displayMonths . map ( d => d . label ) ;
const signed = pfs . filter ( x => x . status === "已签约" ) ;
const pending = pfs . filter ( x => x . status === "待签约" ) ;
const signed = pfs ;
const pending = pfs ;
const sumSign = Math . round ( signed . reduce ( ( s , x ) => s + ( x . sign _amount || 0 ) , 0 ) ) ;
const sumPending = Math . round ( pending . reduce ( ( s , x ) => s + ( x . sign _amount || 0 ) , 0 ) ) ;
@@ -95,31 +95,32 @@ function renderPlan() {
}
} ) ( ) ;
const sm = pf . sign _month || "" ;
const signMonthCell = ` <td class="p-2 text-center align-middle text-sm"><span class="pf-sm-text cursor-pointer hover:text-blue-600" id="pf-sm- ${ pf . id } " onclick="event.stopPropagation(); e ditPf SignMonth(event, ${ pf . id } )"> ${ sm || '—' } </span></td> ` ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="o penPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center align-middle"> ${ esc ( pf . customer _name ) } </td> ${ signMonthCell } <td class="p-2 text-center align-middle text-sm"> ${ money ( pf . sign _amount ) } </td> ${ mCols } ${ totalCol } </tr> ` ;
const signMonthCell = ` <td class="p-2 text-center align-middle text-sm"><span class="pf-sm-text cursor-pointer hover:text-blue-600" id="pf-sm- ${ pf . id } " onclick="event.stopPropagation(); planE ditSignMonth(event, ${ pf . id } )"> ${ sm || '—' } </span></td> ` ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planO penPfEditModal( ${ pf . id } )"><td class="p-2 text-center align-middle text-sm"> ${ esc ( pf . client _name || "" ) } </td><td class="p-2 text-sm font-medium text-center align-middle"> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-center align-middle text-sm"> ${ esc ( pf . weight || "20%" ) } </td><td class="p-2 text-center align-middle text-sm"> ${ esc ( pf . project _type || "待签约" ) } </td> ${ signMonthCell } <td class="p-2 text-center align-middle text-sm"> ${ money ( pf . sign _amount ) } </td> ${ mCols } ${ totalCol } </tr> ` ;
} ;
const now2 = new Date ( ) ;
const defaultMonth2 = now2 . getFullYear ( ) + "-" + String ( now2 . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
if ( ! state . planMonth ) state . planMonth = defaultMonth2 ;
if ( state . planQuarter === undefined ) state . planQuarter = Math . floor ( now2 . getMonth ( ) / 3 ) ;
const monthSet2 = new Set ( [ defaultMonth2 ] ) ;
pfs . forEach ( pf => { let bd = [ ] ; try { bd = JSON . parse ( pf . budget _data || "[]" ) ; } catch ( e ) { } bd . forEach ( b => { if ( b . month ) monthSet2 . add ( b . month ) ; } ) ; } ) ;
const allMonths = [ ... monthSet2 ] . sort ( ) . revers e( ) ;
const qLabels2 = [ "Q1 (1-3月)" , "Q2 (4-6月)" , "Q3 (7-9月)" , "Q4 (10-12月)" ] ;
const toolMonthSelect = all Months. map ( m => '<option value="' + m + '" ' + ( m === state . planMonth ? 'selected' : '' ) + '>' + m + '</option>' ) . join ( "" ) ;
const toolQuarterSelect = qLabels2 . map ( ( l , i ) => '<option value="' + i + '" ' + ( i === state . planQuarter ? 'selec ted ': '' ) + '>' + l + '</option>' ) . join ( "" ) ;
const toolDateSelect = state . planView === 'monthly' ? '<span class="text-sm text-slate-500 ml-2">月份:</span><select onchange="state.planMonth=this.value;renderPlan()" class="text-sm font-medium py-1 px-2.5 cursor-pointer" style="appearance:none;-webkit-appearance:none;background-color:transparent;border:0;outline:none;background:url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpolyline points=%226 9 12 15 18 9%22%3E%3C/polyline%3E%3C/svg%3E') no-repeat right 4px center;padding-right:22px;min-height:30px">' + toolMonthSelect + '</select>' : state . planView === 'quarterly' ? '<span class="text-sm text-slate-500 ml-2">季度:</span><select onchange="state.planQuarter=parseInt(this.value);localStorage.setItem(\'opc-fin-quarter\',this.value);renderPlan()" class="text-sm font-medium py-1 px-2.5 cursor-pointer" style="appearance:none;-webkit-appearance:none;background-color:transparent;border:0;outline:none;background:url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpolyline points=%226 9 12 15 18 9%22%3E%3C/polyline%3E%3C/svg%3E') no-repeat right 4px center;padding-right:22px;min-height:30px">' + toolQuarterSelect + '</select>' : '' ;
const finHeaderBase = ` <span class="text-sm text-slate-500">视图:</span><select onchange="setPlanView(this.value)" class="text-sm font-medium py-1 px-2.5 cursor-pointer" style="appearance:none;-webkit-appearance:none;background-color:transparent;border:0;outline:none;background:url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpolyline points=%226 9 12 15 18 9%22%3E%3C/polyline%3E%3C/svg%3E') no-repeat right 4px center;padding-right:22px;min-height:30px"><option value="overview" ${ state . planView === 'overview' ? 'selected' : '' } >总视图</option><option value="quarterly" ${ state . planView === 'quarterly' ? 'selected' : '' } >季度视图</option><option value="monthly" ${ state . planView === 'monthly' ? 'selected' : '' } >月度视图</option></select> ${ toolDateSelect } ` ;
const qLabels = [ 'Q1' , 'Q2' , 'Q3' , 'Q4' ] ;
const qMonths = [ [ 1 , 2 , 3 ] , [ 4 , 5 , 6 ] , [ 7 , 8 , 9 ] , [ 10 , 11 , 12 ] ] ;
const nowD = new Dat e( ) ;
const activeQ = state . planQuarter !== undefined ? state . planQuarter : Math . floor ( nowD . getMonth ( ) / 3 ) ;
const activeMonths = q Months[ activeQ ] ;
const finHeaderBase = ` <div class="flex items-center gap-2"><button onclick="setPlanView('overview')" class="px-3 py-1 rounded text-sm font-medium ${ state . planView === 'overview' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200' } ">总视图</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按季度:</span> ${ qLabels . map ( ( q , i ) => ` <button onclick="setPlanQuarter( ${ i } )" class="px-3 py-1 rounded text-sm font-medium ${ state . planView === 'quarterly' && activeQ === i ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200' } "> ${ q } </button> ` ) . join ( '' ) } <span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按月度:</span> ${ activeMonths . map ( m => { const ms = ` 2026- ${ String ( m ) . padStart ( 2 , '0' ) } ` ; return ` <button onclick="setPlanMonth(' ${ ms } ')" class="px-3 py-1 rounded text-sm font-medium ${ state . planView === 'monthly' && state . planMonth === ms ? 'bg-blue-600 text-whi te': 'bg-slate-100 text-slate-600 hover:bg-slate-200' } "> ${ m } 月</button> ` ; } ).join('')}</div> ` ;
const finAddBtn = ` <button class="btn btn-primary btn-sm" onclick="openPlanModal()">新增财务项目</button> ` ;
const planFilterTabs = ` <div class="finance-tabs" style="padding:0 0 0 0;border-bottom:1px solid #e2e8f0;margin-bottom:0;display:flex;gap:4px"><button onclick="switchPlanFilter('overview')" class="finance-tab ${ state . planFilter === 'overview' ? ' active' : '' } " style="font-size:14px;font-weight:600;padding:8px 16px;margin-right:0">总览</button><button onclick="switchPlanFilter('待签约 ')" class="finance-tab ${ state . planFilter === '待签约 ' ? ' active' : '' } " style="font-size:14px;font-weight:600">项目计划 ( ${ pfs . filter ( x => x . status === '待签约' ) . length} )</button><button onclick="switchPlanFilter('expense')" class="finance-tab ${ state . planFilter === 'expense' ? ' active' : '' } " style="font-size:14px;font-weight:600">平台费用计划</button></div> ` ;
const planFilterTabs = ` <div class="finance-tabs" style="padding:0 0 0 0;border-bottom:1px solid #e2e8f0;margin-bottom:0;display:flex;gap:4px"><button onclick="switchPlanFilter('overview')" class="finance-tab ${ state . planFilter === 'overview' ? ' active' : '' } " style="font-size:14px;font-weight:600;padding:8px 16px;margin-right:0">总览</button><button onclick="switchPlanFilter('projects ')" class="finance-tab ${ state . planFilter === 'projects ' ? ' active' : '' } " style="font-size:14px;font-weight:600">项目计划 ( ${ pfs . length } )</button><button onclick="switchPlanFilter('expense')" class="finance-tab ${ state . planFilter === 'expense' ? ' active' : '' } " style="font-size:14px;font-weight:600">平台费用计划</button></div> ` ;
document . querySelector ( "#plan" ) . innerHTML = ` <div class="grid gap-4">
${ planFilterTabs }
<div class="flex items-center gap-3 px-4 py-3 rounded-lg border" style="background:linear-gradient(135deg,#dbeafe,#bfdbfe);border-color:#3b82f6">
<i data-lucide="lightbulb" class="text-blue-600 flex-shrink-0" style="width:18px;height:18px"></i>
<span class="text-sm text-blue-900"><strong>计划提醒:</strong>计划也是预算,只有真正能够列出计划的预算,才有靠谱的落地可能性</span>
</div>
${ state . planFilter !== 'expense' && state . planFilter !== 'overview' ? card ( ` <div class="flex justify-between items-center"><div class="flex items-center gap-2"> ${ finHeaderBase } </div> ${ finAddBtn } </div> ` , "p-4" ) : '' }
<div id="finance Modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closePlanModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="finance ModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="finance DeleteBtn" onclick="deletePlanItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closePlanModal()"><i data-lucide="x"></i></button></div></div>
<div id="plan Modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closePlanModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="plan ModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="plan DeleteBtn" onclick="deletePlanItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closePlanModal()"><i data-lucide="x"></i></button></div></div>
<div class="finance-tabs">
<button class="finance-tab active" data-tab="info" onclick="switchPlanTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
<button class="finance-tab" data-tab="revpay" onclick="switchPlanTab('revpay')"><i data-lucide="dollar-sign" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>确收与回款</button>
@@ -128,9 +129,9 @@ function renderPlan() {
<button class="finance-tab" data-tab="tasks" onclick="switchPlanTab('tasks')"><i data-lucide="list-checks" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>任务管理</button>
<button class="finance-tab" data-tab="activity" onclick="switchPlanTab('activity')"><i data-lucide="message-square" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>活动与跟进</button>
</div>
<form onsubmit="createPlanFinance(event)" class="plan -form" novalidate><input type="hidden" name="pf_id" id="pf-id-input" value="">
<div class="plan -tab-body">
<div id="financeTabInfo">
<form onsubmit="createPlanFinance(event)" class="finance -form" novalidate><input type="hidden" name="pf_id" id="plan_ pf-id-input" value="">
<div class="finance -tab-body">
<div id="plan_ financeTabInfo">
<div class="grid gap-4">
<div class="grid grid-cols-3 gap-3">
<label class="block"><span class="fin-label">部门</span><input type="hidden" name="project_id" value=" ${ state . tenant } "><input class="form-ctrl bg-slate-50 cursor-not-allowed" value=" ${ state . tenant } " disabled></label>
@@ -140,7 +141,7 @@ function renderPlan() {
<div class="grid grid-cols-3 gap-3">
<label class="block"><span class="fin-label">签约金额(元) <span class="text-red-500">*</span></span><input name="sign_amount" class="form-ctrl" placeholder="必须大于 0"></label>
<label class="block"><span class="fin-label">签约月份 <span class="text-red-500">*</span></span><select name="sign_month" required class="form-ctrl bg-white"><option value="">选择</option> ${ monthOptions ( '' ) } </select></label>
<label class="block"><span class="fin-label">项目状态</span><select name="status" class="form-ctrl bg-white"><option>待签约</option></select></label>
</div>
<div class="grid grid-cols-3 gap-3">
<label class="block"><span class="fin-label">商务负责人 <span class="text-red-500">*</span></span><input name="sales_person" required class="form-ctrl" placeholder="请输入商务负责人"></label>
@@ -149,12 +150,12 @@ function renderPlan() {
</div>
<div class="grid grid-cols-3 gap-3">
<label class="block"><span class="fin-label">联系电话</span><input name="contact_phone" class="form-ctrl" placeholder="请输入联系电话"></label>
<label class="block"><span class="fin-label">其他</span><input name="other_info" class="form-ctrl" placeholder="备注信息"></label>
<label class="block"><span class="fin-label">其他</span><input name="other_info" class="form-ctrl" placeholder="备注信息"></label></div><div class="grid grid-cols-3 gap-3"><label class="block"><span class="fin-label">客户名称</span><input name="client_name" class="form-ctrl" placeholder="客户名称"></label><label class="block"><span class="fin-label">项目类型</span><select name="project_type" class="form-ctrl bg-white"><option>待签约</option><option>已签约</option></select></label><label class="block"><span class="fin-label">加权</span><select name="weight" class="form-ctrl bg-white"><option>20%</option><option>40%</option><option>60%</option><option>80%</option><option>100%</option></select></label>
</div>
<div class="block"><span class="fin-label">业务类型</span><div class="flex flex-wrap gap-2 mt-1" id="businessTypeChecks"> ${ fmTypes . map ( t => ` <label class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-slate-200 cursor-pointer hover:border-blue-300 text-sm bt-chip" onclick="t oggleBtChip(this)"><input type="checkbox" name="business_type[]" value=" ${ t } " class="hidden"><span> ${ t } </span></label> ` ) . join ( "" ) } </div></div>
<div class="block"><span class="fin-label">业务类型</span><div class="flex flex-wrap gap-2 mt-1" id="plan_ businessTypeChecks"> ${ fmTypes . map ( t => ` <label class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-slate-200 cursor-pointer hover:border-blue-300 text-sm bt-chip" onclick="planT oggleBtChip(this)"><input type="checkbox" name="business_type[]" value=" ${ t } " class="hidden"><span> ${ t } </span></label> ` ) . join ( "" ) } </div></div>
</div>
</div>
<div id="financeTabExec" class="hidden">
<div id="plan_ financeTabExec" class="hidden">
<div class="fin-field-group">
<p class="fin-section-label">执行信息</p>
<div class="grid grid-cols-3 gap-4">
@@ -165,53 +166,53 @@ function renderPlan() {
< / d i v >
< / d i v >
< / d i v >
< div id = "financeTabRevpay" class = "hidden" >
< div class = "grid grid-cols-3 gap-3 mb-4" id = "revpaySummary" >
< div id = "plan_ financeTabRevpay" class = "hidden" >
< div class = "grid grid-cols-3 gap-3 mb-4" id = "plan_ revpaySummary" >
< div class = "bg-blue-50 rounded-lg p-3 text-center border border-blue-100" >
< p class = "text-xs text-blue-600 font-medium" > 总确收 < / p >
< p class = "text-lg font-bold text-blue-700" id = "revpayTotalRev" > ¥ 0 < / p >
< p class = "text-lg font-bold text-blue-700" id = "plan_ revpayTotalRev" > ¥ 0 < / p >
< / d i v >
< div class = "bg-green-50 rounded-lg p-3 text-center border border-green-100" >
< p class = "text-xs text-green-600 font-medium" > 总毛利 < / p >
< p class = "text-lg font-bold text-green-700" id = "revpayTotalGross" > ¥ 0 < / p >
< p class = "text-lg font-bold text-green-700" id = "plan_ revpayTotalGross" > ¥ 0 < / p >
< / d i v >
< div class = "bg-amber-50 rounded-lg p-3 text-center border border-amber-100" >
< p class = "text-xs text-amber-600 font-medium" > 总回款 < / p >
< p class = "text-lg font-bold text-amber-700" id = "revpayTotalPayment" > ¥ 0 < / p >
< p class = "text-lg font-bold text-amber-700" id = "plan_ revpayTotalPayment" > ¥ 0 < / p >
< / d i v >
< / d i v >
< table class = "w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id = "revpayTable" >
< table class = "w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id = "plan_ revpayTable" >
< thead > < tr class = "bg-slate-50 border-b border-slate-200" > < th class = "p-2.5 text-left font-medium text-slate-500" style = "min-width:140px" > 月份 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 确 收 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 毛 利 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 回 款 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - l e f t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 备 注 < / t h > < t h c l a s s = " p - 2 . 5 w - 8 " > < / t h > < / t r > < / t h e a d >
< tbody id = "revpayTbody" > < / t b o d y >
< tbody id = "plan_ revpayTbody" > < / t b o d y >
< / t a b l e >
< button type = "button" class = "btn btn-ghost btn-sm mt-3" onclick = "a ddRevpayRow()" > < i data - lucide = "plus" > < / i > 添 加 月 份 < / b u t t o n >
< button type = "button" class = "btn btn-ghost btn-sm mt-3" onclick = "planA ddRevpayRow()" > < i data - lucide = "plus" > < / i > 添 加 月 份 < / b u t t o n >
< / d i v >
< div id = "financeTabCost" class = "hidden" >
< div class = "grid grid-cols-2 gap-3 mb-4" id = "costSummary" >
< div id = "plan_ financeTabCost" class = "hidden" >
< div class = "grid grid-cols-2 gap-3 mb-4" id = "plan_ costSummary" >
< div class = "bg-rose-50 rounded-lg p-3 text-center border border-rose-100" >
< p class = "text-xs text-rose-600 font-medium" > 总成本 < / p >
< p class = "text-lg font-bold text-rose-700" id = "costTotalCost" > ¥ 0 < / p >
< p class = "text-lg font-bold text-rose-700" id = "plan_ costTotalCost" > ¥ 0 < / p >
< / d i v >
< div class = "bg-purple-50 rounded-lg p-3 text-center border border-purple-100" >
< p class = "text-xs text-purple-600 font-medium" > 总已付 < / p >
< p class = "text-lg font-bold text-purple-700" id = "costTotalPaid" > ¥ 0 < / p >
< p class = "text-lg font-bold text-purple-700" id = "plan_ costTotalPaid" > ¥ 0 < / p >
< / d i v >
< / d i v >
< table class = "w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id = "costTable" >
< table class = "w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id = "plan_ costTable" >
< thead > < tr class = "bg-slate-50 border-b border-slate-200" > < th class = "p-2.5 text-left font-medium text-slate-500" style = "min-width:140px" > 月份 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - l e f t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " s t y l e = " m i n - w i d t h : 1 2 0 p x " > 费 用 类 型 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 成 本 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 已 付 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - l e f t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 备 注 < / t h > < t h c l a s s = " p - 2 . 5 w - 8 " > < / t h > < / t r > < / t h e a d >
< tbody id = "costTbody" > < / t b o d y >
< tbody id = "plan_ costTbody" > < / t b o d y >
< / t a b l e >
< button type = "button" class = "btn btn-ghost btn-sm mt-3" onclick = "a ddCostRow()" > < i data - lucide = "plus" > < / i > 添 加 月 份 < / b u t t o n >
< button type = "button" class = "btn btn-ghost btn-sm mt-3" onclick = "planA ddCostRow()" > < i data - lucide = "plus" > < / i > 添 加 月 份 < / b u t t o n >
< / d i v >
< div id = "financeTabTasks" class = "hidden" >
< table class = "w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id = "taskTable" >
< div id = "plan_ financeTabTasks" class = "hidden" >
< table class = "w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id = "plan_ taskTable" >
< thead > < tr class = "bg-slate-50 border-b border-slate-200" > < th class = "p-2.5 text-left font-medium text-slate-500" style = "min-width:120px" > 月份 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - l e f t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " s t y l e = " m i n - w i d t h : 1 2 0 p x " > 任 务 类 型 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 任 务 数 量 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 已 执 行 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 差 额 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 单 价 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 执 行 金 额 < / t h > < t h c l a s s = " p - 2 . 5 t e x t - r i g h t f o n t - m e d i u m t e x t - s l a t e - 5 0 0 " > 未 执 行 金 额 < / t h > < t h c l a s s = " p - 2 . 5 w - 8 " > < / t h > < / t r > < / t h e a d >
< tbody id = "taskTbody" > < / t b o d y >
< tbody id = "plan_ taskTbody" > < / t b o d y >
< / t a b l e >
< button type = "button" class = "btn btn-ghost btn-sm mt-3" onclick = "a ddTaskRow()" > < i data - lucide = "plus" > < / i > 添 加 任 务 < / b u t t o n >
< button type = "button" class = "btn btn-ghost btn-sm mt-3" onclick = "planA ddTaskRow()" > < i data - lucide = "plus" > < / i > 添 加 任 务 < / b u t t o n >
< / d i v >
< div id = "financeTabActivity" class = "hidden" >
< div class = "grid gap-2" id = "finActivityList" > < / d i v >
< div id = "plan_ financeTabActivity" class = "hidden" >
< div class = "grid gap-2" id = "plan_ finActivityList" > < / d i v >
< div class = "comment-box mt-3" >
< div class = "squire-toolbar" >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('bold')" title = "加粗" > < i data - lucide = "bold" > < / i > < / b u t t o n >
@@ -226,24 +227,24 @@ function renderPlan() {
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('undo')" title = "撤销" > < i data - lucide = "undo" > < / i > < / b u t t o n >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('redo')" title = "重做" > < i data - lucide = "redo" > < / i > < / b u t t o n >
< / d i v >
< div class = "squire-editor" id = "squire_finance" placeholder = "添加评论" > < / d i v >
< div class = "squire-editor" id = "plan_ squire_finance" placeholder = "添加评论" > < / d i v >
< div class = "comment-toolbar" >
< span class = "comment-hint" > 支持富文本编辑 < / s p a n >
< button class = "btn btn-primary btn-sm comment-submit" type = "button" onclick = "s ubmitFin Comment()" > 评论 < / b u t t o n >
< button class = "btn btn-primary btn-sm comment-submit" type = "button" onclick = "planS ubmitComment()" > 评论 < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
< / d i v > < d i v c l a s s = " f l e x j u s t i f y - e n d g a p - 3 p t - 2 p l a n - f o r m - a c t i o n s " > < b u t t o n t y p e = " b u t t o n " c l a s s = " b t n b t n - g h o s t b t n - s m p x - 6 " o n c l i c k = " c l o s e P l a n M o d a l ( ) " > 取 消 < / b u t t o n > < b u t t o n t y p e = " s u b m i t " c l a s s = " b t n b t n - p r i m a r y b t n - s m p x - 8 " > 保 存 < / b u t t o n > < / d i v > < / f o r m > < / d i v > < / d i v >
< / d i v > < d i v c l a s s = " f l e x j u s t i f y - e n d g a p - 3 p t - 2 f i n a n c e -f o r m - a c t i o n s " > < b u t t o n t y p e = " b u t t o n " c l a s s = " b t n b t n - g h o s t b t n - s m p x - 6 " o n c l i c k = " c l o s e P l a n M o d a l ( ) " > 取 消 < / b u t t o n > < b u t t o n t y p e = " s u b m i t " c l a s s = " b t n b t n - p r i m a r y b t n - s m p x - 8 " > 保 存 < / b u t t o n > < / d i v > < / f o r m > < / d i v > < / d i v >
$ { ( ( ) => {
if ( state . planFilter === 'overview' ) {
setTimeout ( ( ) => renderPlanOverview ( ) , 10 ) ;
return ` <div id="finance Overview"></div> ` ;
return ` <div id="plan Overview"></div> ` ;
}
if ( state . planFilter === 'expense' ) {
setTimeout ( ( ) => renderPlanExpense ( ) , 10 ) ;
return ` <div id="planExpenseModule"></div> ` ;
}
const isUnsigned = state . planFilter === '待签约' ;
const isUnsigned = false ;
const METRIC _CARDS = [
{ label : '签约金额' , value : ctx => moneyWan ( ctx . signTotal ) , color : 'text-slate-700' } ,
{ label : '已确收' , hideUnsigned : true , value : ctx => moneyWan ( ctx . sumRev ) , color : 'text-blue-700' } ,
@@ -257,7 +258,7 @@ function renderPlan() {
{ label : '毛利率' , hideUnsigned : true , value : ctx => ctx . sumRev && ctx . sumGross ? Math . round ( ctx . sumGross / ctx . sumRev * 100 ) + '%' : '—' , color : 'text-slate-500' } ,
{ label : '回款率' , hideUnsigned : true , value : ctx => ctx . sumRev && ctx . sumPay ? Math . round ( ctx . sumPay / ctx . sumRev * 100 ) + '%' : '—' , color : 'text-slate-500' } ,
{ label : '付款率' , hideUnsigned : true , value : ctx => ctx . sumCost && ctx . sumPaid ? Math . round ( ctx . sumPaid / ctx . sumCost * 100 ) + '%' : '—' , color : 'text-slate-500' } ,
] . filter ( c => state . planFilter !== '待签约' || ! c . hideUnsigned ) ;
] . filter ( c => true ) ;
// 列排序
if ( ! state . planSort ) state . planSort = { key : null , asc : true } ;
@@ -284,8 +285,10 @@ function renderPlan() {
return h ;
} ;
const SIGNED _COLS = [
{ key : 'client_name' , label : '客户' } ,
{ key : 'customer_name' , label : '项目名称' } ,
{ key : 'status ' , label : '状态 ' } ,
{ key : 'weight ' , label : '加权 ' } ,
{ key : 'project_type' , label : '项目类型' } ,
{ key : 'sign_amount' , label : '签约金额' } ,
{ key : 'rev' , label : '已确收' } ,
{ key : 'gross' , label : '毛利' } ,
@@ -295,13 +298,13 @@ function renderPlan() {
{ key : 'paid' , label : '已付' } ,
{ key : 'cashflow' , label : '现金流' } ,
{ key : 'exe_rate' , label : '执行率' } ,
{ key : 'gross_rate' , label : '毛利率' } ,
{ key : 'pay_rate' , label : '回款率' } ,
{ key : 'paid_rate' , label : '付款率' } ,
] ;
const UNSIGNED _COLS = [
{ key : 'client_name' , label : '客户' } ,
{ key : 'customer_name' , label : '项目名称' } ,
{ key : 'status ' , label : '状态 ' } ,
{ key : 'weight ' , label : '加权 ' } ,
{ key : 'project_type' , label : '项目类型' } ,
{ key : 'sign_amount' , label : '签约金额' } ,
{ key : 'sign_month' , label : '签约月份' } ,
{ key : 'gross' , label : '毛利' } ,
@@ -316,13 +319,13 @@ function renderPlan() {
return typeof v === 'object' ? [ c . label , v . val , v . cls ] : [ c . label , v , c . color ] ;
} ) ;
const thead = isUnsigned ? buildThead ( UNSIGNED _COLS , 'uns' ) : buildThead ( SIGNED _COLS , 'sig' ) ;
const theadCols = isUnsigned ? 7 : 14 ;
const theadCols = isUnsigned ? 9 : 14 ;
const tbody = rows . length ? ` <tbody> ${ rows . join ( "" ) } </tbody> ` : ` <tr><td colspan=" ${ theadCols } " class="p-6 text-center text-slate-400"> ${ emptyText } </td></tr> ` ;
const cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-6' ;
const subtitle = isUnsigned
? '合同 → 毛利 → 成本 → 项目利润'
: '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流 → 执行率 → 毛利率 → 回款率 → 付款率 ' ;
: '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流' ;
return card ( ` <div class="grid ${ cardGrid } gap-3 mb-3"> ${ cards . map ( ( [ l , v , c ] ) => '<div class="metric-card p-3"><span class="flex items-center gap-2 text-sm text-slate-500">' + l + '</span><strong class="mt-2 block text-xl ' + c + '">' + v + '</strong></div>' ) . join ( "" ) } </div> ` , "p-4" ) +
card ( ` <h3 class="text-sm font-bold text-slate-700">项目财务明细</h3><p class="text-xs text-slate-400 mb-3"> ${ subtitle } </p><div class="overflow-x-auto"><table class="w-full text-sm"> ${ thead } ${ tbody } </table></div> ` , "p-4" ) ;
} ;
@@ -336,25 +339,23 @@ function renderPlan() {
const payR = payRVal !== null ? '<span class="' + payRCls + ' font-semibold">' + payRVal + '%</span>' : '<span class="text-slate-300">—</span>' ;
const paidR = cost && paid ? Math . round ( paid / cost * 100 ) + '%' : '<span class="text-slate-300">—</span>' ;
const grossR = rev && gross ? Math . round ( gross / rev * 100 ) + '%' : '<span class="text-slate-300">—</span>' ;
const st = '<span class="text-amber-6 00">待签约 </span>' ;
const cf = cashflow ? money ( cashflow ) : '<span class="text-slate-300">—</span>' ;
const cf = cashflow ? money ( cashflow ) : '<span class="text-slate-3 00">— </span>' ;
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600' ;
const profit = gross ;
const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600' ;
const pfVal = profit ? money ( profit ) : '<span class="text-slate-300">—</span>' ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="o penPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title=" ${ esc ( pf . customer _name ) } "> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-center align-middle text-sm"> ${ st } </td><td class="p-2 text-center align-middle text-sm font-medium"> ${ money ( pf . sign _amount ) } </td><td class="p-2 text-center text-blue-700 align-middle"> ${ fmtNoUnit ( rev ) } </td><td class="p-2 text-center align-middle text-sm font-medium"> ${ fmtNoUnit ( gross ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmtNoUnit ( cost ) } </td><td class="p-2 text-center font-semibold align-middle ${ pfCls } "> ${ pfVal } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ fmtNoUnit ( payment ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ fmtNoUnit ( paid ) } </td><td class="p-2 text-center font-semibold align-middle ${ cfCls } "> ${ cf } </td><td class="p-2 text-center align-middle text-sm"> ${ exe } </td><td class="p-2 text-center align-middle text-sm"> ${ grossR } </td><td class="p-2 text-center align-middle text-sm"> ${ payR } </td><td class="p-2 text-center align-middle text-sm"> ${ paid R} </td></tr> ` ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planO penPfEditModal( ${ pf . id } )"><td class="p-2 text-center align-middle text-sm"> ${ esc ( pf . client _name || "" ) } </td><td class="p-2 text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title=" ${ esc ( pf . customer _name ) } "> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()"> ${ planInlineSelect ( 'weight' , pf . id , 'weight' , pf . weight || '20%' , [ '20%' , '40%' , '60%' , '80%' , '100%' ] ) } </td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()"> ${ planInlineSelect ( 'ptype' , pf . id , 'project_type' , pf . project _type || '待签约' , [ '待签约' , '已签约' ] ) } </td><td class="p-2 text-center align-middle text-sm font-medium"> ${ money ( pf . sign _amount ) } </td><td class="p-2 text-center text-blue-700 align-middle"> ${ fmtNoUnit ( rev ) } </td><td class="p-2 text-center align-middle text-sm font-medium"> ${ fmtNoUnit ( gross ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmtNoUnit ( cost ) } </td><td class="p-2 text-center font-semibold align-middle ${ pfCls } "> ${ pfVal } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ fmtNoUnit ( payment ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ fmtNoUnit ( paid ) } </td><td class="p-2 text-center font-semibold align-middle ${ cfCls } "> ${ cf } </td><td class="p-2 text-center align-middle text-sm"> ${ exe } </td><td class="p-2 text-center align-middle text-sm"> ${ pay R} </td></tr> ` ;
} ;
// 待签约简版行
const tdRowUnsigned = ( pf , cost , gross ) => {
const s t = '<span class="text-amber-600">待签约</span>' ;
const profit = gross ;
const profi t = gross ;
const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600' ;
const pfVal = profit ? money ( profit ) : '<span class="text-slate-300">—</span>' ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="o penPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title=" ${ esc ( pf . customer _name ) } "> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-center align-middle text-sm"> ${ st } </td><td class="p-2 text-center align-middle text-sm font-medium"> ${ money ( pf . sign _amount ) } </td><td class="p-2 text-center align-middle text-sm"> ${ pf . sign _month || '—' } </td><td class="p-2 text-center align-middle text-sm font-medium"> ${ fmtNoUnit ( gross ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmtNoUnit ( cost ) } </td><td class="p-2 text-center font-semibold align-middle ${ pfCls } "> ${ pfVal } </td></tr> ` ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planO penPfEditModal( ${ pf . id } )"><td class="p-2 text-center align-middle text-sm"> ${ esc ( pf . client _name || "" ) } </td><td class="p-2 text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title=" ${ esc ( pf . customer _name ) } "> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()"> ${ planInlineSelect ( 'weight' , pf . id , 'weight' , pf . weight || '20%' , [ '20%' , '40%' , '60%' , '80%' , '100%' ] ) } </td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()"> ${ planInlineSelect ( 'ptype' , pf . id , 'project_type' , pf . project _type || '待签约' , [ '待签约' , '已签约' ] ) } </td><td class="p-2 text-center align-middle text-sm font-medium"> ${ money ( pf . sign _amount ) } </td><td class="p-2 text-center align-middle text-sm"> ${ pf . sign _month || '—' } </td><td class="p-2 text-center align-middle text-sm font-medium"> ${ fmtNoUnit ( gross ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmtNoUnit ( cost ) } </td><td class="p-2 text-center font-semibold align-middle ${ pfCls } "> ${ pfVal } </td></tr> ` ;
} ;
if ( state . planView === 'monthly' ) {
const allPfs = pfs . filter ( x => x . status === state . planFilter ) ;
const allPfs = pfs ;
const now = new Date ( ) ;
const defaultMonth = now . getFullYear ( ) + "-" + String ( now . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
if ( ! state . planMonth ) state . planMonth = defaultMonth ;
@@ -374,8 +375,7 @@ function renderPlan() {
dataItems . push ( {
pf : pf ,
customer _name : pf . customer _name || '' ,
status : pf . status || '' ,
sign _amount : pf . sign _amount || 0 ,
sign _amount : pf . sign _amount || 0 ,
sign _month : pf . sign _month || '' ,
rev : rev , gross : gross , cost : cost ,
profit : gross , payment : payment , paid : paid ,
@@ -395,12 +395,12 @@ function renderPlan() {
rows . push ( isUnsigned ? tdRowUnsigned ( d . pf , d . cost , d . gross ) : tdRow ( d . pf , d . rev , d . payment , d . cost , d . paid , d . gross ) ) ;
} ) ;
const signTotal = Math . round ( allPfs . filter ( x => ( x . sign _month || "" ) . substring ( 0 , 7 ) === selMonth ) . reduce ( ( a , p ) => a + ( p . sign _amount || 0 ) , 0 ) ) ;
const signCnt = allPfs . filter ( x => x . status === "已签约" && ( x . sign _month || "" ) . substring ( 0 , 7 ) === selMonth ) . length ;
const signCnt = allPfs . filter ( x => ( x . sign _month || "" ) . substring ( 0 , 7 ) === selMonth ) . length ;
return renderView ( rows , sumRev , sumPay , sumCost , sumPaid , sumGross , signTotal , signCnt , '该月份暂无数据' ) ;
}
if ( state . planView === 'quarterly' ) {
const allPfs = pfs . filter ( x => x . status === state . planFilter ) ;
const allPfs = pfs ;
const qRanges = [ [ 1 , 2 , 3 ] , [ 4 , 5 , 6 ] , [ 7 , 8 , 9 ] , [ 10 , 11 , 12 ] ] ;
const now = new Date ( ) ;
if ( state . planQuarter === undefined ) {
@@ -430,8 +430,7 @@ function renderPlan() {
dataItems . push ( {
pf : pf ,
customer _name : pf . customer _name || '' ,
status : pf . status || '' ,
sign _amount : pf . sign _amount || 0 ,
sign _amount : pf . sign _amount || 0 ,
sign _month : pf . sign _month || '' ,
rev : rev , gross : gross , cost : cost ,
profit : gross , payment : payment , paid : paid ,
@@ -451,7 +450,7 @@ function renderPlan() {
rows . push ( isUnsigned ? tdRowUnsigned ( d . pf , d . cost , d . gross ) : tdRow ( d . pf , d . rev , d . payment , d . cost , d . paid , d . gross ) ) ;
} ) ;
const signTotal = Math . round ( allPfs . filter ( x => { const m = parseInt ( ( x . sign _month || "0" ) . substring ( 5 ) ) || 0 ; return qRange . includes ( m ) ; } ) . reduce ( ( a , p ) => a + ( p . sign _amount || 0 ) , 0 ) ) ;
const signCnt = allPfs . filter ( x => x . status === "已签约" && qRange. includes ( parseInt ( ( x . sign _month || "0" ) . substring ( 5 ) ) || 0 ) ) . length ;
const signCnt = allPfs . filter ( x => qRange . includes ( parseInt ( ( x . sign _month || "0" ) . substring ( 5 ) ) || 0 ) ) . length ;
return renderView ( rows , sumRev , sumPay , sumCost , sumPaid , sumGross , signTotal , signCnt , '该季度暂无数据' ) ;
}
@@ -465,15 +464,14 @@ function renderPlan() {
expense . forEach ( e => { cost += parseFloat ( e . cost || 0 ) || 0 ; paid += parseFloat ( e . paid || 0 ) || 0 ; } ) ;
return { rev : Math . round ( rev ) , payment : Math . round ( payment ) , cost : Math . round ( cost ) , paid : Math . round ( paid ) , gross : Math . round ( gross ) } ;
} ;
const allPfs = pfs . filter ( x => x . status === state . planFilter ) ;
const allPfs = pfs ;
var dataItems = allPfs . map ( pf => {
var t = calcTotals ( pf ) ;
var cashflow = t . payment - t . paid ;
return {
pf : pf ,
customer _name : pf . customer _name || '' ,
status : pf . status || '' ,
sign _amount : pf . sign _amount || 0 ,
sign _amount : pf . sign _amount || 0 ,
rev : t . rev , gross : t . gross , cost : t . cost ,
profit : t . gross , payment : t . payment , paid : t . paid ,
cashflow : cashflow ,
@@ -493,7 +491,7 @@ function renderPlan() {
rows . push ( isUnsigned ? tdRowUnsigned ( d . pf , d . cost , d . gross ) : tdRow ( d . pf , d . rev , d . payment , d . cost , d . paid , d . gross ) ) ;
} ) ;
const signTotal = Math . round ( allPfs . reduce ( ( a , p ) => a + ( p . sign _amount || 0 ) , 0 ) ) ;
const signCnt = allPfs . filter ( x => x . status === "已签约" ) . length;
const signCnt = allPfs . length ;
return renderView ( rows , sumRev , sumPay , sumCost , sumPaid , sumGross , signTotal , signCnt , '暂无数据' ) ;
} ) ( ) }
< / d i v > ` ;
@@ -508,46 +506,46 @@ window.openPlanModal = () => {
if ( dept ) dept . value = state . tenant ;
const pfIdInput = form . querySelector ( '[name="pf_id"]' ) ;
if ( ! pfIdInput || ! pfIdInput . value ) {
i nitRevpayTable( null ) ;
i nitCostTable( null ) ;
i nitTaskTable( null ) ;
planI nitRevpayTable( null ) ;
planI nitCostTable( null ) ;
planI nitTaskTable( null ) ;
document . querySelector ( "#planDeleteBtn" ) . classList . add ( "hidden" ) ;
}
modal . classList . remove ( "hidden" ) ;
} ;
window . a ddRevpayRow = ( month = '' , rev = '' , gross = '' , payment = '' , rev _note = '' ) => {
const tbody = document . querySelector ( "#revpayTbody" ) ;
window . planA ddRevpayRow = ( month = '' , rev = '' , gross = '' , payment = '' , rev _note = '' ) => {
const tbody = document . querySelector ( "#plan_ revpayTbody" ) ;
if ( ! tbody ) return ;
const row = document . createElement ( "tr" ) ;
row . innerHTML = ` <td><select name="budget_month[]" class="form-ctrl form-ctrl-sm w-full" style="min-width:140px" onchange="u pdateRevpaySummary()"> ${ monthOptions ( month ) } </select></td>
<td><input name="budget_rev[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ rev } " oninput="u pdateRevpaySummary()"></td>
<td><input name="budget_gross[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ gross } " oninput="u pdateRevpaySummary()"></td>
<td><input name="budget_payment[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ payment } " oninput="u pdateRevpaySummary()"></td>
row . innerHTML = ` <td><select name="budget_month[]" class="form-ctrl form-ctrl-sm w-full" style="min-width:140px" onchange="planU pdateRevpaySummary()"> ${ monthOptions ( month ) } </select></td>
<td><input name="budget_rev[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ rev } " oninput="planU pdateRevpaySummary()"></td>
<td><input name="budget_gross[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ gross } " oninput="planU pdateRevpaySummary()"></td>
<td><input name="budget_payment[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ payment } " oninput="planU pdateRevpaySummary()"></td>
<td><input name="budget_rev_note[]" class="form-ctrl form-ctrl-sm w-full" placeholder="备注" value=" ${ rev _note } "></td>
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove();u pdateRevpaySummary()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td> ` ;
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove();planU pdateRevpaySummary()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td> ` ;
tbody . appendChild ( row ) ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
} ;
window . a ddCostRow = ( month = '' , expense _type = '' , cost = '' , paid = '' , exp _note = '' ) => {
const tbody = document . querySelector ( "#costTbody" ) ;
window . planA ddCostRow = ( month = '' , expense _type = '' , cost = '' , paid = '' , exp _note = '' ) => {
const tbody = document . querySelector ( "#plan_ costTbody" ) ;
if ( ! tbody ) return ;
const row = document . createElement ( "tr" ) ;
row . innerHTML = ` <td><select name="expense_month[]" class="form-ctrl form-ctrl-sm w-full" style="min-width:140px" onchange="u pdateCostSummary()"> ${ monthOptions ( month ) } </select></td>
row . innerHTML = ` <td><select name="expense_month[]" class="form-ctrl form-ctrl-sm w-full" style="min-width:140px" onchange="planU pdateCostSummary()"> ${ monthOptions ( month ) } </select></td>
<td><input name="expense_type[]" class="form-ctrl form-ctrl-sm w-full" placeholder="费用类型" value=" ${ expense _type } "></td>
<td><input name="expense_cost[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ cost } " oninput="u pdateCostSummary()"></td>
<td><input name="expense_paid[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ paid } " oninput="u pdateCostSummary()"></td>
<td><input name="expense_cost[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ cost } " oninput="planU pdateCostSummary()"></td>
<td><input name="expense_paid[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value=" ${ paid } " oninput="planU pdateCostSummary()"></td>
<td><input name="expense_note[]" class="form-ctrl form-ctrl-sm w-full" placeholder="备注" value=" ${ exp _note } "></td>
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove();u pdateCostSummary()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td> ` ;
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove();planU pdateCostSummary()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td> ` ;
tbody . appendChild ( row ) ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
} ;
window . u pdateRevpaySummary = ( ) => {
const revEl = document . querySelector ( "#revpayTotalRev" ) ;
const grossEl = document . querySelector ( "#revpayTotalGross" ) ;
const paymentEl = document . querySelector ( "#revpayTotalPayment" ) ;
window . planU pdateRevpaySummary = ( ) => {
const revEl = document . querySelector ( "#plan_ revpayTotalRev" ) ;
const grossEl = document . querySelector ( "#plan_ revpayTotalGross" ) ;
const paymentEl = document . querySelector ( "#plan_ revpayTotalPayment" ) ;
if ( ! revEl || ! paymentEl ) return ;
const revInputs = document . querySelectorAll ( '[name="budget_rev[]"]' ) ;
const grossInputs = document . querySelectorAll ( '[name="budget_gross[]"]' ) ;
@@ -561,9 +559,9 @@ window.updateRevpaySummary = () => {
paymentEl . textContent = money ( totalPayment ) ;
} ;
window . u pdateCostSummary = ( ) => {
const costEl = document . querySelector ( "#costTotalCost" ) ;
const paidEl = document . querySelector ( "#costTotalPaid" ) ;
window . planU pdateCostSummary = ( ) => {
const costEl = document . querySelector ( "#plan_ costTotalCost" ) ;
const paidEl = document . querySelector ( "#plan_ costTotalPaid" ) ;
if ( ! costEl || ! paidEl ) return ;
const costInputs = document . querySelectorAll ( '[name="expense_cost[]"]' ) ;
const paidInputs = document . querySelectorAll ( '[name="expense_paid[]"]' ) ;
@@ -574,27 +572,27 @@ window.updateCostSummary = () => {
paidEl . textContent = money ( totalPaid ) ;
} ;
window . i nitRevpayTable = ( budgetData ) => {
const tbody = document . querySelector ( "#revpayTbody" ) ;
window . planI nitRevpayTable = ( budgetData ) => {
const tbody = document . querySelector ( "#plan_ revpayTbody" ) ;
if ( ! tbody ) return ;
tbody . innerHTML = "" ;
const rows = budgetData || [ ] ;
rows . forEach ( r => a ddRevpayRow( r . month || '' , r . rev || '' , r . gross || '' , r . payment || '' , r . rev _note || '' ) ) ;
setTimeout ( ( ) => u pdateRevpaySummary( ) , 50 ) ;
rows . forEach ( r => planA ddRevpayRow( r . month || '' , r . rev || '' , r . gross || '' , r . payment || '' , r . rev _note || '' ) ) ;
setTimeout ( ( ) => planU pdateRevpaySummary( ) , 50 ) ;
} ;
window . i nitCostTable = ( expenseData ) => {
const tbody = document . querySelector ( "#costTbody" ) ;
window . planI nitCostTable = ( expenseData ) => {
const tbody = document . querySelector ( "#plan_ costTbody" ) ;
if ( ! tbody ) return ;
tbody . innerHTML = "" ;
const rows = expenseData || [ ] ;
rows . forEach ( r => a ddCostRow( r . month || '' , r . expense _type || '' , r . cost || '' , r . paid || '' , r . exp _note || '' ) ) ;
setTimeout ( ( ) => u pdateCostSummary( ) , 50 ) ;
rows . forEach ( r => planA ddCostRow( r . month || '' , r . expense _type || '' , r . cost || '' , r . paid || '' , r . exp _note || '' ) ) ;
setTimeout ( ( ) => planU pdateCostSummary( ) , 50 ) ;
} ;
// ---------- 任务管理 tab ----------
function t askMonthOptions( selected ) {
function planT askMonthOptions( selected ) {
const now = new Date ( ) ;
const opts = [ ] ;
for ( let i = - 2 ; i <= 12 ; i ++ ) {
@@ -605,36 +603,36 @@ function taskMonthOptions(selected) {
return opts . join ( "" ) ;
}
window . a ddTaskRow = ( taskMonth = '' , taskType = '' , taskCount = '' , executedCount = '' , unitPrice = '' ) => {
const tbody = document . querySelector ( "#taskTbody" ) ;
window . planA ddTaskRow = ( taskMonth = '' , taskType = '' , taskCount = '' , executedCount = '' , unitPrice = '' ) => {
const tbody = document . querySelector ( "#plan_ taskTbody" ) ;
if ( ! tbody ) return ;
const row = document . createElement ( "tr" ) ;
const defaultMonth = ( ( ) => { const n = new Date ( ) ; return n . getFullYear ( ) + "-" + String ( n . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ; } ) ( ) ;
const isPreset = TASK _TYPES . includes ( taskType ) ;
const typeCell = isPreset || ! taskType
? ` <select name="task_type[]" class="form-ctrl form-ctrl-sm w-full" onchange="o nTaskTypeChange(this)"><option value="">选择</option> ${ TASK _TYPES . map ( t => ` <option ${ t === taskType ? 'selected' : '' } > ${ t } </option> ` ) . join ( "" ) } <option value="__custom__" ${ ! isPreset && taskType ? 'selected' : '' } >自定义...</option></select> `
: ` <input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" value=" ${ esc ( taskType ) } " placeholder="输入自定义类型"><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="r evertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button> ` ;
row . innerHTML = ` <td><select name="task_month[]" class="form-ctrl form-ctrl-sm w-full"> ${ t askMonthOptions( taskMonth || defaultMonth ) } </select></td>
? ` <select name="task_type[]" class="form-ctrl form-ctrl-sm w-full" onchange="planO nTaskTypeChange(this)"><option value="">选择</option> ${ TASK _TYPES . map ( t => ` <option ${ t === taskType ? 'selected' : '' } > ${ t } </option> ` ) . join ( "" ) } <option value="__custom__" ${ ! isPreset && taskType ? 'selected' : '' } >自定义...</option></select> `
: ` <input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" value=" ${ esc ( taskType ) } " placeholder="输入自定义类型"><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="planR evertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button> ` ;
row . innerHTML = ` <td><select name="task_month[]" class="form-ctrl form-ctrl-sm w-full"> ${ planT askMonthOptions( taskMonth || defaultMonth ) } </select></td>
<td class="flex items-center"> ${ typeCell } </td>
<td><input name="task_count[]" type="number" step="1" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:100px" placeholder="0" value=" ${ taskCount } " oninput="u pdateTaskDiff(this)"></td>
<td><input name="task_executed[]" type="number" step="1" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:100px" placeholder="0" value=" ${ executedCount } " oninput="u pdateTaskDiff(this)"></td>
<td><input name="task_count[]" type="number" step="1" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:100px" placeholder="0" value=" ${ taskCount } " oninput="planU pdateTaskDiff(this)"></td>
<td><input name="task_executed[]" type="number" step="1" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:100px" placeholder="0" value=" ${ executedCount } " oninput="planU pdateTaskDiff(this)"></td>
<td><input name="task_diff[]" type="number" class="form-ctrl form-ctrl-sm text-right" style="width:80px" placeholder="—" disabled></td>
<td><input name="task_unit_price[]" type="number" step="0.01" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:90px" placeholder="0" value=" ${ unitPrice } " oninput="u pdateTaskDiff(this)"></td>
<td><input name="task_unit_price[]" type="number" step="0.01" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:90px" placeholder="0" value=" ${ unitPrice } " oninput="planU pdateTaskDiff(this)"></td>
<td><input name="task_exec_amount[]" type="number" class="form-ctrl form-ctrl-sm text-right" style="width:140px" placeholder="—" disabled></td>
<td><input name="task_unexec_amount[]" type="number" class="form-ctrl form-ctrl-sm text-right" style="width:140px" placeholder="—" disabled></td>
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td> ` ;
tbody . appendChild ( row ) ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
u pdateRowCalc( row ) ;
planU pdateRowCalc( row ) ;
} ;
window . u pdateTaskDiff = ( el ) => {
window . planU pdateTaskDiff = ( el ) => {
const row = el . closest ( 'tr' ) ;
if ( ! row ) return ;
u pdateRowCalc( row ) ;
planU pdateRowCalc( row ) ;
} ;
function u pdateRowCalc( row ) {
function planU pdateRowCalc( row ) {
const countInput = row . querySelector ( '[name="task_count[]"]' ) ;
const execInput = row . querySelector ( '[name="task_executed[]"]' ) ;
const priceInput = row . querySelector ( '[name="task_unit_price[]"]' ) ;
@@ -655,7 +653,7 @@ function updateRowCalc(row) {
unexecAmtInput . value = unexecAmt ? unexecAmt . toFixed ( 2 ) : '' ;
}
window . t oggleBtChip = ( chip ) => {
window . planT oggleBtChip = ( chip ) => {
const cb = chip . querySelector ( 'input' ) ;
cb . checked = ! cb . checked ;
chip . classList . toggle ( 'bg-blue-50' , cb . checked ) ;
@@ -663,35 +661,35 @@ window.toggleBtChip = (chip) => {
chip . classList . toggle ( 'text-blue-600' , cb . checked ) ;
} ;
window . i nitTaskTable = ( taskData ) => {
const tbody = document . querySelector ( "#taskTbody" ) ;
window . planI nitTaskTable = ( taskData ) => {
const tbody = document . querySelector ( "#plan_ taskTbody" ) ;
if ( ! tbody ) return ;
tbody . innerHTML = "" ;
const rows = taskData || [ ] ;
rows . forEach ( r => a ddTaskRow( r . task _month || '' , r . task _type || '' , r . task _count || '' , r . task _executed || '' , r . unit _price || '' ) ) ;
rows . forEach ( r => planA ddTaskRow( r . task _month || '' , r . task _type || '' , r . task _count || '' , r . task _executed || '' , r . unit _price || '' ) ) ;
} ;
window . o nTaskTypeChange = ( sel ) => {
window . planO nTaskTypeChange = ( sel ) => {
if ( sel . value !== '__custom__' ) return ;
const td = sel . parentElement ;
const oldVal = sel . value ;
td . innerHTML = ` <input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" placeholder="输入自定义类型" autofocus><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="r evertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button> ` ;
td . innerHTML = ` <input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" placeholder="输入自定义类型" autofocus><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="planR evertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button> ` ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
td . querySelector ( 'input' ) . focus ( ) ;
} ;
window . r evertTaskType = ( btn ) => {
window . planR evertTaskType = ( btn ) => {
const td = btn . parentElement ;
td . innerHTML = ` <select name="task_type[]" class="form-ctrl form-ctrl-sm w-full" onchange="o nTaskTypeChange(this)"><option value="">选择</option> ${ TASK _TYPES . map ( t => ` <option> ${ t } </option> ` ) . join ( "" ) } <option value="__custom__">自定义...</option></select> ` ;
td . innerHTML = ` <select name="task_type[]" class="form-ctrl form-ctrl-sm w-full" onchange="planO nTaskTypeChange(this)"><option value="">选择</option> ${ TASK _TYPES . map ( t => ` <option> ${ t } </option> ` ) . join ( "" ) } <option value="__custom__">自定义...</option></select> ` ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
} ;
window . closePlanModal = ( ) => {
const modal = document . querySelector ( "#planModal" ) ;
modal . classList . add ( "hidden" ) ;
if ( modal ) modal . classList . add ( "hidden" ) ;
} ;
window . e ditPf SignMonth = ( event , pfId ) => {
window . planE ditSignMonth = ( event , pfId ) => {
event . stopPropagation ( ) ;
const pf = ( state . data . planFinances || [ ] ) . find ( x => x . id === pfId ) ;
if ( ! pf ) return ;
@@ -707,11 +705,11 @@ window.editPfSignMonth = (event, pfId) => {
try {
await api ( ` /api/planFinances/ ${ pfId } ` , { method : "PUT" , body : JSON . stringify ( { data : { sign _month : newValue } } ) } ) ;
pf . sign _month = newValue ;
td . innerHTML = ` <span class="pf-sm-text cursor-pointer hover:text-blue-600" onclick="e ditPf SignMonth(event, ${ pfId } )"> ${ newValue || '—' } </span> ` ;
td . innerHTML = ` <span class="pf-sm-text cursor-pointer hover:text-blue-600" onclick="planE ditSignMonth(event, ${ pfId } )"> ${ newValue || '—' } </span> ` ;
} catch ( e ) { toast ( "修改失败:" + e . message , "error" ) ; }
} ) ;
select . addEventListener ( "blur" , ( ) => {
td . innerHTML = ` <span class="pf-sm-text cursor-pointer hover:text-blue-600" onclick="e ditPf SignMonth(event, ${ pfId } )"> ${ currentValue || '—' } </span> ` ;
td . innerHTML = ` <span class="pf-sm-text cursor-pointer hover:text-blue-600" onclick="planE ditSignMonth(event, ${ pfId } )"> ${ currentValue || '—' } </span> ` ;
} ) ;
td . innerHTML = "" ;
td . appendChild ( select ) ;
@@ -720,25 +718,25 @@ window.editPfSignMonth = (event, pfId) => {
window . switchPlanTab = ( tab ) => {
document . querySelectorAll ( ".plan-tab" ) . forEach ( b => b . classList . toggle ( "active" , b . dataset . tab === tab ) ) ;
document . querySelector ( "#financeTabInfo" ) . classList . toggle ( "hidden" , tab !== "info" ) ;
document . querySelector ( "#financeTabRevpay" ) . classList . toggle ( "hidden" , tab !== "revpay" ) ;
document . querySelector ( "#financeTabCost" ) . classList . toggle ( "hidden" , tab !== "cost" ) ;
document . querySelector ( "#financeTabExec" ) . classList . toggle ( "hidden" , tab !== "exec" ) ;
document . querySelector ( "#financeTabTasks" ) . classList . toggle ( "hidden" , tab !== "tasks" ) ;
document . querySelector ( "#financeTabActivity" ) . classList . toggle ( "hidden" , tab !== "activity" ) ;
document . querySelector ( "#plan_ financeTabInfo" ) . classList . toggle ( "hidden" , tab !== "info" ) ;
document . querySelector ( "#plan_ financeTabRevpay" ) . classList . toggle ( "hidden" , tab !== "revpay" ) ;
document . querySelector ( "#plan_ financeTabCost" ) . classList . toggle ( "hidden" , tab !== "cost" ) ;
document . querySelector ( "#plan_ financeTabExec" ) . classList . toggle ( "hidden" , tab !== "exec" ) ;
document . querySelector ( "#plan_ financeTabTasks" ) . classList . toggle ( "hidden" , tab !== "tasks" ) ;
document . querySelector ( "#plan_ financeTabActivity" ) . classList . toggle ( "hidden" , tab !== "activity" ) ;
document . querySelector ( ".plan-form-actions" ) . classList . toggle ( "hidden" , tab === "activity" ) ;
if ( tab === "activity" ) initFin Squire( ) ;
if ( tab === "activity" ) planInit Squire( ) ;
} ;
// ---------- 活动与跟进 ----------
async function loadFin Followups( pfId ) {
const list = document . querySelector ( "#finActivityList" ) ;
async function planLoad Followups( pfId ) {
const list = document . querySelector ( "#plan_ finActivityList" ) ;
if ( ! list || ! pfId ) return ;
try {
const fups = await api ( ` /api/followups/project_finance/ ${ pfId } ` ) ;
list . innerHTML = fups . length
? fups . map ( f => ` <div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span> ${ esc ( f . follower ) } · ${ esc ( f . follow _up _method ) } </span><span> ${ esc ( f . followed _at ) } </span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html=" ${ encodeURIComponent ( f . content || '' ) } "></div> ${ f . next _action ? ` <p class="mt-1 leading-5 text-blue-700">下一步: ${ text ( f . next _action ) } </p> ` : "" } </div><button class="activity-delete" type="button" onclick="d eleteFinF ollowup(event, ${ f . id } )" title="删除评论"><i data-lucide="trash-2"></i></button></div> ` ) . join ( "" )
? fups . map ( f => ` <div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span> ${ esc ( f . follower ) } · ${ esc ( f . follow _up _method ) } </span><span> ${ esc ( f . followed _at ) } </span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html=" ${ encodeURIComponent ( f . content || '' ) } "></div> ${ f . next _action ? ` <p class="mt-1 leading-5 text-blue-700">下一步: ${ text ( f . next _action ) } </p> ` : "" } </div><button class="activity-delete" type="button" onclick="planD eleteFollowup(event, ${ f . id } )" title="删除评论"><i data-lucide="trash-2"></i></button></div> ` ) . join ( "" )
: '<p class="text-sm text-slate-400 py-4 text-center">暂无跟进记录</p>' ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
list . querySelectorAll ( ".rich-content" ) . forEach ( el => {
@@ -748,8 +746,8 @@ async function loadFinFollowups(pfId) {
} catch ( e ) { /* ignore */ }
}
function initFin Squire( ) {
const ed = document . querySelector ( "#squire_finance" ) ;
function planInit Squire( ) {
const ed = document . querySelector ( "#plan_ squire_finance" ) ;
if ( ! ed || ! window . Squire ) return ;
if ( window . squireInstances [ "squire_finance" ] ) { window . squireInstances [ "squire_finance" ] . destroy ( ) ; }
const sq = new Squire ( ed , { blockTag : "P" } ) ;
@@ -758,8 +756,8 @@ function initFinSquire() {
ed . addEventListener ( "blur" , ( ) => { if ( ! ed . textContent . trim ( ) ) ed . classList . remove ( "focused" ) ; } ) ;
}
window . s ubmitFin Comment = async ( ) => {
const pfId = document . querySelector ( "#pf-id-input" ) . value ;
window . planS ubmitComment = async ( ) => {
const pfId = document . querySelector ( "#plan_ pf-id-input" ) . value ;
if ( ! pfId ) return ;
const sq = window . squireInstances [ "squire_finance" ] ;
const content = sq ? sq . getHTML ( ) . trim ( ) : "" ;
@@ -771,24 +769,24 @@ window.submitFinComment = async () => {
sq . setHTML ( "" ) ;
btn . disabled = false ;
btn . textContent = "评论" ;
await loadFin Followups( pfId ) ;
await planLoad Followups( pfId ) ;
} ;
window . d eleteFinF ollowup = async ( event , followupId ) => {
window . planD eleteFollowup = async ( event , followupId ) => {
event . stopPropagation ( ) ;
if ( ! confirm ( "确认删除这条评论?" ) ) return ;
await api ( ` /api/followups/ ${ followupId } ` , { method : "DELETE" } ) ;
const pfId = document . querySelector ( "#pf-id-input" ) . value ;
if ( pfId ) await loadFin Followups( pfId ) ;
const pfId = document . querySelector ( "#plan_ pf-id-input" ) . value ;
if ( pfId ) await planLoad Followups( pfId ) ;
} ;
window . o penPfEditModal = ( pfId ) => {
window . planO penPfEditModal = ( pfId ) => {
const pf = ( state . data . planFinances || [ ] ) . find ( x => x . id === pfId ) ;
if ( ! pf ) return ;
document . querySelector ( "#pf-id-input" ) . value = pf . id ;
document . querySelector ( "#finance ModalTitle" ) . textContent = "编辑项目财务" ;
document . querySelector ( "#plan_ pf-id-input" ) . value = pf . id ;
document . querySelector ( "#plan ModalTitle" ) . textContent = "编辑项目财务" ;
document . querySelector ( "#planDeleteBtn" ) . classList . remove ( "hidden" ) ;
const form = document . querySelector ( "#finance Modal form" ) ;
const form = document . querySelector ( "#plan Modal form" ) ;
form . querySelector ( '[name="project_id"]' ) . value = pf . project _id || "" ;
const deptDisplay = form . querySelector ( '.bg-slate-50 [disabled]' ) ;
if ( deptDisplay ) deptDisplay . value = pf . project _id || "" ;
@@ -813,7 +811,6 @@ window.openPfEditModal = (pfId) => {
signMonthEl . innerHTML = monthOptions ( signMonthValue ) ;
signMonthEl . value = signMonthValue ;
}
form . querySelector ( '[name="status"]' ) . value = pf . status || "待签约" ;
form . querySelector ( '[name="sales_person"]' ) . value = pf . sales _person || "" ;
form . querySelector ( '[name="owner"]' ) . value = pf . owner || "" ;
setVal ( "start_date" , pf . start _date ) ;
@@ -825,17 +822,20 @@ window.openPfEditModal = (pfId) => {
setVal ( "contact_name" , pf . contact _name ) ;
setVal ( "contact_phone" , pf . contact _phone ) ;
setVal ( "other_info" , pf . other _info ) ;
setVal ( "client_name" , pf . client _name ) ;
form . querySelector ( '[name="project_type"]' ) . value = pf . project _type || "待签约" ;
form . querySelector ( '[name="weight"]' ) . value = pf . weight || "20%" ;
let budgetData = [ ] ;
try { budgetData = JSON . parse ( pf . budget _data || "[]" ) ; } catch ( e ) { budgetData = [ ] ; }
i nitRevpayTable( budgetData . length ? budgetData : null ) ;
planI nitRevpayTable( budgetData . length ? budgetData : null ) ;
let expenseData = [ ] ;
try { expenseData = JSON . parse ( pf . expense _data || "[]" ) ; } catch ( e ) { expenseData = [ ] ; }
i nitCostTable( expenseData . length ? expenseData : null ) ;
planI nitCostTable( expenseData . length ? expenseData : null ) ;
let taskData = [ ] ;
try { taskData = JSON . parse ( pf . task _data || "[]" ) ; } catch ( e ) { taskData = [ ] ; }
i nitTaskTable( taskData . length ? taskData : null ) ;
planI nitTaskTable( taskData . length ? taskData : null ) ;
setTimeout ( ( ) => updateBudgetSummary ( ) , 100 ) ;
loadFin Followups( pf . id ) ;
planLoad Followups( pf . id ) ;
openPlanModal ( ) ;
} ;
@@ -936,8 +936,8 @@ window.createPlanFinance = async (event) => {
if ( result . id && data . customer _name ) logActivity ( "finance" , result . id , "创建了「" + data . customer _name + "」的财务项目" ) ;
}
form . reset ( ) ;
document . querySelector ( "#pf-id-input" ) . value = "" ;
document . querySelector ( "#finance ModalTitle" ) . textContent = "新增项目财务" ;
document . querySelector ( "#plan_ pf-id-input" ) . value = "" ;
document . querySelector ( "#plan ModalTitle" ) . textContent = "新增项目财务" ;
closePlanModal ( ) ;
await load ( ) ;
} catch ( error ) {
@@ -946,7 +946,7 @@ window.createPlanFinance = async (event) => {
} ;
window . deletePlanItem = async ( ) => {
const pfId = document . querySelector ( "#pf-id-input" ) . value ;
const pfId = document . querySelector ( "#plan_ pf-id-input" ) . value ;
if ( ! pfId ) return ;
const pf = ( state . data . planFinances || [ ] ) . find ( x => x . id === parseInt ( pfId ) ) ;
const name = pf ? ( pf . customer _name || "此项目" ) : "此项目" ;
@@ -966,46 +966,50 @@ function renderPlanOverview() {
const { summary , financeMonthly } = state . data ;
if ( ! summary ) return ;
const m = summary . metrics ;
const moneyIntL = ( v ) => ` ${ Math . round ( Number ( v || 0 ) ) . toLocaleString ( "zh-CN" ) } 元 ` ;
const moneyIntL = ( v ) => Math . round ( Number ( v || 0 ) ) . toLocaleString ( "zh-CN" ) ;
const moneyWan = ( v ) => {
const n = Number ( v || 0 ) ;
return n >= 10000 ? ( n / 10000 ) . toFixed ( 0 ) + "万" : n . toLocaleString ( "zh-CN" ) ;
} ;
const card = ( body , cls ) => '<div class="card ' + ( cls || 'p-4' ) + '">' + body + '</div>' ;
const rows1 = [ [ "年度累计" , moneyIntL ( m . signed _annual || m . signed _amount ) ] , [ "本季度累计" , moneyIntL ( m . signed _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . signed _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . signed _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . signed _prev _month || 0 ) ] ] ;
const rows2 = [ [ "年度累计" , moneyIntL ( m . revenue _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . revenue _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . monthly _revenue || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . revenue _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . revenue _prev _month || 0 ) ] ] ;
const rows3 = [ [ "年度累计" , moneyIntL ( m . gross _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . gross _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . monthly _net _profit || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . gross _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . gross _prev _month || 0 ) ] ] ;
const rows4 = [ [ "年度累计" , moneyIntL ( m . cost _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . cost _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . cost _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . cost _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . cost _prev _month || 0 ) ] ] ;
const rows5 = [ [ "年度累计" , moneyIntL ( m . profit _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . profit _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . profit _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . profit _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . profit _prev _month || 0 ) ] ] ;
const rows7 = [ [ "年度累计" , moneyIntL ( m . payment _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . payment _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . payment _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . payment _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . payment _prev _month || 0 ) ] ] ;
const rows8 = [ [ "年度累计" , moneyIntL ( m . paid _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . paid _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . paid _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . paid _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . paid _prev _month || 0 ) ] ] ;
const rows9 = [ [ "年度累计" , moneyIntL ( m . cashflow _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . cashflow _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . cashflow _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . cashflow _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . cashflow _prev _month || 0 ) ] ] ;
const qoq = ( cur , prev ) => { if ( ! prev ) return '<span class="text-slate-300">—</span>' ; const pct = Math . round ( ( cur - prev ) / prev * 100 ) ; const cls = pct >= 0 ? 'text-green-600' : 'text-red-600' ; const sign = pct >= 0 ? '+' : '' ; return '<span class="' + cls + '">' + sign + pct + '%</span>' ; } ;
// 月度数据 (financeMonthly 有12个月)
const fm = financeMonthly || [ ] ;
const mLbl = [ '1月' , '2月' , '3月' , '4月' , '5月' , '6月' , '7月' , '8月' , '9月' , '10月' , '11月' , '12月' ] ;
const LABELS = [ '合同金额' , '确收金额' , '确收毛利' , '成本' , '项目利润' , '回款金额' , '已付' , '现金流' ] ;
const Q _FIELDS = [ m . signed _q2 || 0 , m . revenue _q2 , m . gross _q2 , m . cost _q2 || 0 , m . profit _q2 || 0 , m . payment _q2 || 0 , m . paid _q2 || 0 , m . cashflow _q2 || 0 ] ;
const Q _PREV = [ m . signed _prev _q || 0 , m . revenue _prev _q || 0 , m . gross _prev _q || 0 , m . cost _prev _q || 0 , m . profit _prev _q || 0 , m . payment _prev _q || 0 , m . paid _prev _q || 0 , m . cashflow _prev _q || 0 ] ;
const M _FIELDS = [ m . signed _month || 0 , m . monthly _revenue , m . monthly _net _profit , m . cost _month || 0 , m . profit _month || 0 , m . payment _month || 0 , m . paid _month || 0 , m . cashflow _ month || 0 ] ;
const M _PREV = [ m . signed _prev _month || 0 , m . revenue _prev _month || 0 , m . gross _prev _month || 0 , m . cost _prev _month || 0 , m . profit _prev _month || 0 , m . payment _prev _month || 0 , m . paid _prev _month || 0 , m . cashflow _prev _month || 0 ] ;
const ROWS = [ rows1 , rows2 , rows3 , rows4 , rows5 , rows7 , rows8 , rows9 ] ;
const CF _RAW = [ m . cashflow _annual || 0 , m . cashflow _q2 || 0 , m . cashflow _month || 0 , m . cashflow _prev _q || 0 , m . cashflow _prev _month || 0 ] ;
const PF _RAW = [ m . profit _annual || 0 , m . profit _q2 || 0 , m . profit _month || 0 , m . profit _prev _q || 0 , m . profit _prev _month || 0 ] ;
var tdVal = function ( ri , col , val ) {
if ( ri === 4 ) return '<td class="py-2 text-right font-semibold ' + ( PF _RAW [ col ] >= 0 ? 'text-green-600' : 'text-red-600' ) + '">' + val + '</td>' ;
if ( ri === 7 ) return '<td class="py-2 text-right font-semibold ' + ( CF _RAW [ col ] >= 0 ? 'text-green-600' : 'text-red-600' ) + '">' + val + '</td>' ;
return '<td class="py-2 text-right font-semibold text-slate-800">' + val + '</td>' ;
} ;
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500">指标</th><th class="py-2 text-right font-semibold text-slate-700">年度累计</th><th class="py-2 text-right font-semibold text-slate-700">上季度累计</th><th class="py-2 text-right font-semibold text-slate-700">上月累计</th><th class="py-2 text-right font-semibold text-slate-700">本季度累计</th><th class="py-2 text-right font-semibold text-slate-400">季环比</th><th class="py-2 text-right font-semibold text-slate-700">本月累计</th><th class="py-2 text-right font-semibold text-slate-400">月环比</th></tr >' ;
// 每行的年度累计 + 12个月值
const annualVals = [
m . signed _annual || m . signed _a mou nt || 0 ,
m . revenue _annual || 0 ,
m . gross _annual || 0 ,
m . cost _annual || 0 ,
m . profit _annual || 0 ,
m . payment _annual || 0 ,
m . paid _annual || 0 ,
m . cashflow _annual || 0 ,
] ;
// 字段映射: sign, revenue, gross, cost, profit(=gross-cost), payment, paid, cashflow(=payment-paid)
const fmFields = [ 'sign' , 'revenue' , 'gross' , 'cost' , null , 'payment' , 'paid' , null ] ;
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-2 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th >' ;
for ( var mi = 0 ; mi < 12 ; mi ++ ) { thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl [ mi ] + '</th>' ; }
thead += '</tr>' ;
var tbody = '' ;
for ( var ri = 0 ; ri < 8 ; ri ++ ) {
var vals = ROW S[ ri ] ;
tbody += '<tr class="border-b border-slate-100 last:border-0">< td class="py-2 text-lef t font-semibold text-slate-7 00">' + LABELS [ ri ] + '</td>' +
tdVal ( ri , 0 , vals [ 0 ] [ 1 ] ) + tdVal ( ri , 3 , vals [ 3 ] [ 1 ] ) + tdVal ( ri , 4 , vals [ 4 ] [ 1 ] ) + tdVal ( ri , 1 , vals [ 1 ] [ 1 ] ) +
'<td class="py-2 text-right">' + qoq ( Q _FIELDS [ ri ] , Q _PREV [ ri ] ) + '</td>' +
tdVal ( ri , 2 , vals [ 2 ] [ 1 ] ) +
'<td class="py-2 text-right">' + qoq ( M _FIELDS [ r i] , M _PREV [ r i] ) + '</td></tr>' ;
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700 whitespace-nowrap">' + LABEL S[ ri ] + '</td>' ;
rowHtml += '<td class="py-2 text-righ t font-semibold text-slate-8 00 whitespace-nowrap">' + moneyIntL ( annualVals [ ri ] ) + '</td>' ;
for ( var mi = 0 ; mi < 12 ; mi ++ ) {
var val = 0 ;
if ( fm [ mi ] ) {
if ( ri === 4 ) { val = ( fm [ m i] . gross || 0 ) - ( fm [ m i] . cost || 0 ) ; }
else if ( ri === 7 ) { val = ( fm [ mi ] . payment || 0 ) - ( fm [ mi ] . paid || 0 ) ; }
else { var fk = fmFields [ ri ] ; if ( fk ) val = fm [ mi ] [ fk ] || 0 ; }
}
var cls = 'text-slate-700' ;
if ( ri === 4 ) cls = val >= 0 ? 'text-green-600' : 'text-red-600' ;
if ( ri === 7 ) cls = val >= 0 ? 'text-green-600' : 'text-red-600' ;
rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + cls + '">' + moneyIntL ( val ) + '</td>' ;
}
tbody += rowHtml + '</tr>' ;
}
document . querySelector ( "#planOverview" ) . innerHTML = '<div class="grid gap-5">' +
@@ -1020,7 +1024,7 @@ function renderPlanOverview() {
if ( window . lucide ) window . lucide . createIcons ( ) ;
// 渲染图表(复用 home.js 的 moneyTick / chartOptions / monthLabels)
if ( financeMonthly && window . Chart ) {
renderOverview Charts ( financeMonthly ) ;
renderPlan Charts ( financeMonthly ) ;
}
}
@@ -1030,24 +1034,42 @@ function renderPlanCharts(data) {
const baseOpts = typeof chartOptions === 'function' ? chartOptions ( moneyTick ) : { responsive : true , maintainAspectRatio : false } ;
var iconf = { type : "line" , options : baseOpts , data : { labels : labels } } ;
var c1 = document . querySelector ( "#fi nChartSign" ) ;
var c1 = document . querySelector ( "#pla nChartSign" ) ;
if ( c1 && window . Chart ) {
if ( state . planChart1 ) state . planChart1 . destroy ( ) ;
state . planChart1 = new Chart ( c1 , Object . assign ( { } , iconf , { data : { labels : labels , datasets : [ { label : "签约金额" , data : data . map ( function ( x ) { return x . sign || 0 ; } ) , borderColor : "#6366f1" , backgroundColor : "rgba(99,102,241,0.06)" , fill : true , tension : 0.3 } ] } } ) ) ;
}
var c2 = document . querySelector ( "#fi nChartRev" ) ;
var c2 = document . querySelector ( "#pla nChartRev" ) ;
if ( c2 && window . Chart ) {
if ( state . planChart2 ) state . planChart2 . destroy ( ) ;
state . planChart2 = new Chart ( c2 , Object . assign ( { } , iconf , { data : { labels : labels , datasets : [ { label : "确收" , data : data . map ( function ( x ) { return x . revenue || 0 ; } ) , borderColor : "#2563eb" , backgroundColor : "rgba(37,99,235,0.06)" , fill : true , tension : 0.3 } , { label : "毛利" , data : data . map ( function ( x ) { return x . gross || 0 ; } ) , borderColor : "#059669" , backgroundColor : "rgba(5,150,105,0.06)" , fill : true , tension : 0.3 } ] } } ) ) ;
}
var c3 = document . querySelector ( "#fi nChartCash" ) ;
var c3 = document . querySelector ( "#pla nChartCash" ) ;
if ( c3 && window . Chart ) {
if ( state . planChart3 ) state . planChart3 . destroy ( ) ;
state . planChart3 = new Chart ( c3 , Object . assign ( { } , iconf , { data : { labels : labels , datasets : [ { label : "回款" , data : data . map ( function ( x ) { return x . payment || 0 ; } ) , borderColor : "#d97706" , backgroundColor : "rgba(217,119,6,0.06)" , fill : true , tension : 0.3 } , { label : "已付" , data : data . map ( function ( x ) { return x . cost || 0 ; } ) , borderColor : "#7c3aed" , backgroundColor : "rgba(124,58,237,0.06)" , fill : true , tension : 0.3 } ] } } ) ) ;
}
var c4 = document . querySelector ( "#fi nChartProfit" ) ;
var c4 = document . querySelector ( "#pla nChartProfit" ) ;
if ( c4 && window . Chart ) {
if ( state . planChart4 ) state . planChart4 . destroy ( ) ;
state . planChart4 = new Chart ( c4 , Object . assign ( { } , iconf , { data : { labels : labels , datasets : [ { label : "利润" , data : data . map ( function ( x ) { return x . gross || 0 ; } ) , borderColor : "#6366f1" , backgroundColor : "rgba(99,102,241,0.06)" , fill : true , tension : 0.3 } ] } } ) ) ;
}
} ;
function planInlineSelect ( name , pfId , field , currentVal , options ) {
var opts = '' ;
for ( var i = 0 ; i < options . length ; i ++ ) {
opts += '<option value="' + options [ i ] + '"' + ( options [ i ] === currentVal ? ' selected' : '' ) + '>' + options [ i ] + '</option>' ;
}
return '<select onchange="planInlineUpdate(' + pfId + ', \'' + field + '\'' + ', this.value)" class="text-sm bg-transparent border-0 cursor-pointer text-center" style="appearance:auto;-webkit-appearance:auto;vertical-align:middle">' + opts + '</select>' ;
}
window . planInlineUpdate = async ( pfId , field , value ) => {
try {
await api ( "/api/planFinances/" + pfId , { method : "PUT" , body : JSON . stringify ( { data : { [ field ] : value } } ) } ) ;
await load ( ) ;
toast ( "已更新" , "success" ) ;
} catch ( error ) {
toast ( "更新失败:" + error . message , "error" ) ;
}
} ;