@@ -24,8 +24,8 @@ function renderFinance() {
const months = displayMonths . map ( d => d . key ) ;
const months = displayMonths . map ( d => d . key ) ;
const monthLabels = displayMonths . map ( d => d . label ) ;
const monthLabels = displayMonths . map ( d => d . label ) ;
const signed = pfs . filter ( x => x . status === "已签约" ) ;
const signed = pfs ;
const pending = pfs . filter ( x => x . status === "待签约" ) ;
const pending = pfs ;
const sumSign = Math . round ( signed . reduce ( ( s , x ) => s + ( x . sign _amount || 0 ) , 0 ) ) ;
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 ) ) ;
const sumPending = Math . round ( pending . reduce ( ( s , x ) => s + ( x . sign _amount || 0 ) , 0 ) ) ;
@@ -98,29 +98,26 @@ function renderFinance() {
} ) ( ) ;
} ) ( ) ;
const sm = pf . sign _month || "" ;
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(); editPfSignMonth(event, ${ pf . id } )"> ${ sm || '—' } </span></td> ` ;
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(); editPfSignMonth(event, ${ pf . id } )"> ${ sm || '—' } </span></td> ` ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ 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> ` ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ 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> ${ signMonthCell } <td class="p-2 text-center align-middle text-sm"> ${ money ( pf . sign _amount ) } </td> ${ mCols } ${ totalCol } </tr> ` ;
} ;
} ;
const now2 = new Date ( ) ;
const now2 = new Date ( ) ;
const defaultMonth2 = now2 . getFullYear ( ) + "-" + String ( now2 . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
const defaultMonth2 = now2 . getFullYear ( ) + "-" + String ( now2 . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
if ( ! state . finMonth ) state . finMonth = defaultMonth2 ;
if ( ! state . finMonth ) state . finMonth = defaultMonth2 ;
if ( state . finQuarter === undefined ) state . finQuarter = Math . floor ( now2 . getMonth ( ) / 3 ) ;
if ( state . finQuarter === undefined ) state . finQuarter = Math . floor ( now2 . getMonth ( ) / 3 ) ;
const monthSet2 = new Set ( [ defaultMonth2 ] ) ;
const qLabels = [ 'Q1' , 'Q2' , 'Q3' , 'Q4' ] ;
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 qMonths = [ [ 1 , 2 , 3 ] , [ 4 , 5 , 6 ] , [ 7 , 8 , 9 ] , [ 10 , 11 , 12 ] ] ;
const allMonths = [ ... monthSet2 ] . sort ( ) . revers e( ) ;
const nowD = new Dat e( ) ;
const qLabels2 = [ "Q1 (1-3月)" , "Q2 (4-6月)" , "Q3 (7-9月)" , "Q4 (10-12月)" ] ;
const activeQ = state . finQuarter !== undefined ? state . finQuarter : Math . floor ( nowD . getMonth ( ) / 3 ) ;
const toolMonthSelect = all Months. map ( m => '<option value="' + m + '" ' + ( m === state . finMonth ? 'selected' : '' ) + '>' + m + '</option>' ) . join ( "" ) ;
const activeMonths = q Months[ activeQ ] ;
const toolQuarterSelect = qLabels2 . map ( ( l , i ) => '<option value="' + i + '" ' + ( i === state . finQuarter ? 'selected' : '' ) + '> ' + l + '</option>' ) . join ( "" ) ;
const finHeaderBase = ` <div class="flex items-center gap-2"><button onclick="setFinView('overview')" class="px-3 py-1 rounded text-sm font-medium ${ state . finView === '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="setFinQuarter( ${ i } )" class="px-3 py-1 rounded text-sm font-medium ${ state . finView === '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="setFinMonth(' ${ ms } ')" class="px-3 py-1 rounded text-sm font-medium ${ state . finView === 'monthly' && state . finMonth === ms ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200' } "> ${ m } 月</button> ` ; } ).join('')}</div> ` ;
const toolDateSelect = state . finView === 'monthly' ? '<span class="text-sm text-slate-500 ml-2">月份:</span><select onchange="state.finMonth=this.value;renderFinance()" 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 . finView === 'quarterly' ? '<span class="text-sm text-slate-500 ml-2">季度:</span><select onchange="state.finQuarter=parseInt(this.value);localStorage.setItem(\'opc-fin-quarter\',this.value);renderFinance()" 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="setFinView(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 . finView === 'overview' ? 'selected' : '' } >总视图</option><option value="quarterly" ${ state . finView === 'quarterly' ? 'selected' : '' } >季度视图</option><option value="monthly" ${ state . finView === 'monthly' ? 'selected' : '' } >月度视图</option></select> ${ toolDateSelect } ` ;
const finAddBtn = ` <button class="btn btn-primary btn-sm" onclick="openFinanceModal()">新增财务项目</button> ` ;
const finAddBtn = ` <button class="btn btn-primary btn-sm" onclick="openFinanceModal()">新增财务项目</button> ` ;
const finFilterTabs = ` <div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('overview')" class="finance-tab ${ state . finFilter === 'overview' ? ' active' : '' } " style="font-size:14px;font-weight:600">总览 </button><button onclick="switchFinFilter('已签约 ')" class="finance-tab ${ state . finFilter === '已签约 ' ? ' active' : '' } " style="font-size:14px;font-weight:600">项目 ( ${ pfs . filter ( x => x . status === '已签约' ) . length } ) </button><button onclick="switchFinFilter('待签约 ')" class="finance-tab ${ state . finFilter === '待签约 ' ? ' active' : '' } " style="font-size:14px;font-weight:600">待签约 ( ${ pfs . filter ( x => x . status === '待签约' ) . length} )</button><button onclick="switchFinFilter('expense')" class="finance-tab ${ state . finFilter === 'expense' ? ' active' : '' } " style="font-size:14px;font-weight:600">平台费用</button></div> ` ;
const finFilterTabs = ` <div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('overview')" class="finance-tab ${ state . finFilter === 'overview' ? ' active' : '' } " style="font-size:14px;font-weight:600">经营月报 </button><button onclick="switchFinFilter('quarterlyOverview ')" class="finance-tab ${ state . finFilter === 'quarterlyOverview ' ? ' active' : '' } " style="font-size:14px;font-weight:600">季度月报 </button><button onclick="switchFinFilter('projects ')" class="finance-tab ${ state . finFilter === 'projects ' ? ' active' : '' } " style="font-size:14px;font-weight:600">项目 ( ${ pfs . length } )</button><button onclick="switchFinFilter('expense')" class="finance-tab ${ state . finFilter === 'expense' ? ' active' : '' } " style="font-size:14px;font-weight:600">平台费用</button></div> ` ;
document . querySelector ( "#finance" ) . innerHTML = ` <div class="grid gap-4">
document . querySelector ( "#finance" ) . innerHTML = ` <div class="grid gap-4">
${ finFilterTabs }
${ finFilterTabs }
${ state . finFilter !== 'expense' && state . finFilter !== 'overview' ? card ( ` <div class="flex justify-between items-center"><div class="flex items-center gap-2"> ${ finHeaderBase } </div> ${ finAddBtn } </div> ` , "p-4" ) : '' }
${ state . finFilter !== 'expense' && state . finFilter !== 'overview' && state . finFilter !== 'quarterlyOverview' ? card ( ` <div class="flex justify-between items-center"><div class="flex items-center gap-2"> ${ finHeaderBase } </div> ${ finAddBtn } </div> ` , "p-4" ) : '' }
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><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="financeModalTitle">新增项目财务</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="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><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="financeModalTitle">新增项目财务</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="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
<div class="finance-tabs">
<div class="finance-tabs">
<button class="finance-tab active" data-tab="info" onclick="switchFinanceTab('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 active" data-tab="info" onclick="switchFinanceTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
@@ -137,12 +134,12 @@ function renderFinance() {
<div class="grid grid-cols-3 gap-3">
<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>
<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>
<label class="block"><span class="fin-label">项目编号</span><input name="project_code" class="form-ctrl" placeholder="如: KP-2026-001"></label>
<label class="block"><span class="fin-label">项目编号</span><input name="project_code" class="form-ctrl" placeholder="如: KP-2026-001"></label>
<label class="block"><span class="fin-label">项目名称 <span class="text-red-500">*</span></span><input name="customer_name" required class="form-ctrl" placeholder="请输入项目名称"></label>
<label class="block"><span class="fin-label">项目名称 <span class="text-red-500">*</span></span><input name="customer_name" required class="form-ctrl" placeholder="请输入项目名称"></label><label class="block"><span class="fin-label">客户名称</span><input name="client_name" class="form-ctrl" placeholder="客户名称"></label>
</div>
</div>
<div class="grid grid-cols-3 gap-3">
<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><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 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><option>待签约</option></select></label>
</div>
</div>
<div class="grid grid-cols-3 gap-3">
<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>
<label class="block"><span class="fin-label">商务负责人 <span class="text-red-500">*</span></span><input name="sales_person" required class="form-ctrl" placeholder="请输入商务负责人"></label>
@@ -195,12 +192,12 @@ function renderFinance() {
< p class = "text-lg font-bold text-rose-700" id = "costTotalCost" > ¥ 0 < / p >
< p class = "text-lg font-bold text-rose-700" id = "costTotalCost" > ¥ 0 < / p >
< / d i v >
< / d i v >
< div class = "bg-purple-50 rounded-lg p-3 text-center border border-purple-100" >
< 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-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 = "costTotalPaid" > ¥ 0 < / p >
< / d i v >
< / d i v >
< / 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 = "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 >
< 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 = "costTbody" > < / t b o d y >
< / t a b l e >
< / t a b l e >
< button type = "button" class = "btn btn-ghost btn-sm mt-3" onclick = "addCostRow()" > < i data - lucide = "plus" > < / i > 添 加 月 份 < / b u t t o n >
< button type = "button" class = "btn btn-ghost btn-sm mt-3" onclick = "addCostRow()" > < i data - lucide = "plus" > < / i > 添 加 月 份 < / b u t t o n >
@@ -241,25 +238,27 @@ function renderFinance() {
setTimeout ( ( ) => renderFinanceOverview ( ) , 10 ) ;
setTimeout ( ( ) => renderFinanceOverview ( ) , 10 ) ;
return ` <div id="financeOverview"></div> ` ;
return ` <div id="financeOverview"></div> ` ;
}
}
if ( state . finFilter === 'quarterlyOverview' ) {
setTimeout ( ( ) => renderFinanceQuarterlyOverview ( ) , 10 ) ;
return ` <div id="financeQuarterlyOverview"></div> ` ;
}
if ( state . finFilter === 'expense' ) {
if ( state . finFilter === 'expense' ) {
setTimeout ( ( ) => renderExpense ( ) , 10 ) ;
setTimeout ( ( ) => renderExpense ( ) , 10 ) ;
return ` <div id="financeExpense"></div> ` ;
return ` <div id="financeExpense"></div> ` ;
}
}
const isUnsigned = state . finFilter === '待签约' ;
const isUnsigned = false ;
const METRIC _CARDS = [
const METRIC _CARDS = [
{ label : '签约金额' , value : ctx => moneyWan ( ctx . signTotal ) , color : 'text-slate-700' } ,
{ label : '签约金额' , value : ctx => moneyWan ( ctx . signTotal ) , color : 'text-slate-700' } ,
{ label : '已确收' , hideUnsigned : true , value : ctx => moneyWan ( ctx . sumRev ) , color : 'text-blue-700' } ,
{ label : '已确收' , hideUnsigned : true , value : ctx => moneyWan ( ctx . sumRev ) , color : 'text-blue-700' } ,
{ label : '毛利' , value : ctx => money ( ctx . sumGross ) , color : 'text-green-700' } ,
{ label : '毛利' , value : ctx => money ( ctx . sumGross ) , color : 'text-green-700' } ,
{ label : '成本' , value : ctx => moneyWan ( ctx . sumCost ) , color : 'text-rose-700' } ,
{ label : '成本' , value : ctx => moneyWan ( ctx . sumCost ) , color : 'text-rose-700' } ,
{ label : '项目利润' , value : ctx => { const v = ctx . sumGross ; return { val : moneyWan ( v ) , cls : v >= 0 ? 'text-green-600' : 'text-red-600' } ; } , color : null } ,
{ label : '已回款' , hideUnsigned : true , value : ctx => moneyWan ( ctx . sumPay ) , color : 'text-amber-700' } ,
{ label : '已回款' , hideUnsigned : true , value : ctx => moneyWan ( ctx . sumPay ) , color : 'text-amber-700' } ,
{ label : '已付 ' , hideUnsigned : true , value : ctx => moneyWan ( ctx . sumPaid ) , color : 'text-purple-700' } ,
{ label : '支出 ' , hideUnsigned : true , value : ctx => moneyWan ( ctx . sumPaid ) , color : 'text-purple-700' } ,
{ label : '现金流' , hideUnsigned : true , value : ctx => { const v = ctx . sumPay - ctx . sumPaid ; return { val : money ( v ) , cls : v >= 0 ? 'text-green-600' : 'text-red-600' } ; } , color : null } ,
{ label : '现金流' , hideUnsigned : true , value : ctx => { const v = ctx . sumPay - ctx . sumPaid ; return { val : money ( v ) , cls : v >= 0 ? 'text-green-600' : 'text-red-600' } ; } , color : null } ,
{ label : '执行率' , hideUnsigned : true , value : ctx => ctx . sumRev && ctx . signTotal ? Math . round ( ctx . sumRev / ctx . signTotal * 100 ) + '%' : '—' , color : 'text-slate-500' } ,
{ 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 . 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 . 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' } ,
{ label : '付款率' , hideUnsigned : true , value : ctx => ctx . sumCost && ctx . sumPaid ? Math . round ( ctx . sumPaid / ctx . sumCost * 100 ) + '%' : '—' , color : 'text-slate-500' } ,
] . filter ( c => state . finFilter !== '待签约' || ! c . hideUnsigned ) ;
] . filter ( c => true ) ;
// 列排序
// 列排序
if ( ! state . finSort ) state . finSort = { key : null , asc : true } ;
if ( ! state . finSort ) state . finSort = { key : null , asc : true } ;
@@ -285,16 +284,16 @@ function renderFinance() {
return h ;
return h ;
} ;
} ;
const SIGNED _COLS = [
const SIGNED _COLS = [
{ key : 'customer _name' , label : '项目名称 ' } , { key : 'status ' , label : '状态 ' } , { key : 'sign_amount' , label : '签约金额' } ,
{ key : 'client _name' , label : '客户 ' } , { key : 'customer_name ' , label : '项目名称 ' } , { key : 'sign_amount' , label : '签约金额' } ,
{ key : 'rev' , label : '已确收' } , { key : 'gross' , label : '毛利' } , { key : 'cost ' , label : '成本 ' } ,
{ key : 'rev' , label : '已确收' } , { key : 'gross' , label : '毛利' } , { key : 'gross_rate ' , label : '毛利率 ' } ,
{ key : 'profit' , label : '项目利润' } , { key : 'payment' , label : '已 回款' } , { key : 'paid ' , label : '已付 ' } ,
{ key : 'payment' , label : '回款' } , { key : 'pay_rate ' , label : '回款率 ' } ,
{ key : 'cashflow ' , label : '现金流 ' } , { key : 'exe_rate ' , label : '执行率 ' } , { key : 'gross _rate' , label : '毛利 率' } ,
{ key : 'cost ' , label : '成本 ' } , { key : 'paid ' , label : '支出 ' } , { key : 'paid _rate' , label : '付款 率' } ,
{ key : 'pay_rate' , label : '回款率' } , { key : 'paid_rate ' , label : '付款率 ' } ,
{ key : 'cashflow ' , label : '现金流 ' } ,
] ;
] ;
const UNSIGNED _COLS = [
const UNSIGNED _COLS = [
{ key : 'customer _name' , label : '项目名称 ' } , { key : 'status ' , label : '状态 ' } , { key : 'sign_amount' , label : '签约金额' } ,
{ key : 'client _name' , label : '客户 ' } , { key : 'customer_name ' , label : '项目名称 ' } , { key : 'sign_amount' , label : '签约金额' } ,
{ key : 'sign_month' , label : '签约月份' } , { key : 'gross' , label : '毛利' } , { key : 'cost' , label : '成本' } ,
{ key : 'sign_month' , label : '签约月份' } , { key : 'gross' , label : '毛利' } , { key : 'cost' , label : '成本' } ,
{ key : 'profit' , label : '项目利润' } ,
,
] ;
] ;
const renderView = ( rows , sumRev , sumPay , sumCost , sumPaid , sumGross , signTotal , signCnt , emptyText ) => {
const renderView = ( rows , sumRev , sumPay , sumCost , sumPaid , sumGross , signTotal , signCnt , emptyText ) => {
@@ -304,13 +303,13 @@ function renderFinance() {
return typeof v === 'object' ? [ c . label , v . val , v . cls ] : [ c . label , v , c . color ] ;
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 thead = isUnsigned ? buildThead ( UNSIGNED _COLS , 'uns' ) : buildThead ( SIGNED _COLS , 'sig' ) ;
const theadCols = isUnsigned ? 7 : 14 ;
const theadCols = isUnsigned ? 6 : 12 ;
const tbody = rows . length ? ` <tbody> ${ rows . join ( "" ) } </tbody> ` : ` <tr><td colspan=" ${ theadCols } " class="p-6 text-center text-slate-400"> ${ emptyText } </td></tr> ` ;
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 cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-6' ;
const subtitle = isUnsigned
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" ) +
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" ) ;
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" ) ;
} ;
} ;
@@ -324,25 +323,17 @@ function renderFinance() {
const payR = payRVal !== null ? '<span class="' + payRCls + ' font-semibold">' + payRVal + '%</span>' : '<span class="text-slate-300">—</span>' ;
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 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 grossR = rev && gross ? Math . round ( gross / rev * 100 ) + '%' : '<span class="text-slate-300">—</span>' ;
const st = pf . status === '已签约' ? '<span class="text-green-600">已签约</span>' : '<span class="text-amber-6 00">待签约 </span>' ;
const cf = cashflow ? money ( cashflow ) : '<span class="text-slate-3 00">— </span>' ;
const cf = cashflow ? money ( cashflow ) : '<span class="text-slate-300">—</span>' ;
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600' ;
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600' ;
const profit = gross ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ 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 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 align-middle text-sm"> ${ grossR } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ fmtNoUnit ( payment ) } </td><td class="p-2 text-center align-middle text-sm"> ${ payR } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmtNoUnit ( cost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ fmtNoUnit ( paid ) } </td><td class="p-2 text-center align-middle text-sm"> ${ paidR } </td><td class="p-2 text-center font-semibold align-middle ${ cfCls } "> ${ cf } </td></tr> ` ;
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="openPfEditModal( ${ 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"> ${ paidR } </td></tr> ` ;
} ;
} ;
// 待签约简版行
// 待签约简版行
const tdRowUnsigned = ( pf , cost , gross ) => {
const tdRowUnsigned = ( pf , cost , gross ) => {
const st = pf . status === '待签约' ? '<span class="text-amber-600">待签约</span>' : '<span class="text-green-600">已签约</span>' ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ 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 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></tr> ` ;
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="openPfEditModal( ${ 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> ` ;
} ;
} ;
if ( state . finView === 'monthly' ) {
if ( state . finView === 'monthly' ) {
const allPfs = pfs . filter ( x => x . status === state . finFilter ) ;
const allPfs = pfs ;
const now = new Date ( ) ;
const now = new Date ( ) ;
const defaultMonth = now . getFullYear ( ) + "-" + String ( now . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
const defaultMonth = now . getFullYear ( ) + "-" + String ( now . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
if ( ! state . finMonth ) state . finMonth = defaultMonth ;
if ( ! state . finMonth ) state . finMonth = defaultMonth ;
@@ -360,9 +351,8 @@ function renderFinance() {
const paid = Math . round ( parseFloat ( e . paid || 0 ) || 0 ) ;
const paid = Math . round ( parseFloat ( e . paid || 0 ) || 0 ) ;
if ( ! rev && ! payment && ! cost && ! paid && ! gross ) return ;
if ( ! rev && ! payment && ! cost && ! paid && ! gross ) return ;
dataItems . push ( {
dataItems . push ( {
pf : pf , customer _name : pf . customer _name || '' , status : pf . status || '' ,
pf : pf , customer _name : pf . customer _name || '' , sign _amount : pf . sign _amount || 0 , sign _month : pf . sign _month || '' ,
sign _amount : pf . sign _amount || 0 , sign _mo nth : pf . sign _month || '' ,
rev : rev , gross : gross , cost : cost , payme nt: payment , paid : paid ,
rev : rev , gross : gross , cost : cost , profit : gross , payment : payment , paid : paid ,
cashflow : payment - paid ,
cashflow : payment - paid ,
exe _rate : rev && pf . sign _amount ? Math . round ( rev / pf . sign _amount * 100 ) : 0 ,
exe _rate : rev && pf . sign _amount ? Math . round ( rev / pf . sign _amount * 100 ) : 0 ,
gross _rate : rev && gross ? Math . round ( gross / rev * 100 ) : 0 ,
gross _rate : rev && gross ? Math . round ( gross / rev * 100 ) : 0 ,
@@ -379,12 +369,12 @@ function renderFinance() {
rows . push ( isUnsigned ? tdRowUnsigned ( d . pf , d . cost , d . gross ) : tdRow ( d . pf , d . rev , d . payment , d . cost , d . paid , d . gross ) ) ;
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 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 , '该月份暂无数据' ) ;
return renderView ( rows , sumRev , sumPay , sumCost , sumPaid , sumGross , signTotal , signCnt , '该月份暂无数据' ) ;
}
}
if ( state . finView === 'quarterly' ) {
if ( state . finView === 'quarterly' ) {
const allPfs = pfs . filter ( x => x . status === state . finFilter ) ;
const allPfs = pfs ;
const qRanges = [ [ 1 , 2 , 3 ] , [ 4 , 5 , 6 ] , [ 7 , 8 , 9 ] , [ 10 , 11 , 12 ] ] ;
const qRanges = [ [ 1 , 2 , 3 ] , [ 4 , 5 , 6 ] , [ 7 , 8 , 9 ] , [ 10 , 11 , 12 ] ] ;
const now = new Date ( ) ;
const now = new Date ( ) ;
if ( state . finQuarter === undefined ) {
if ( state . finQuarter === undefined ) {
@@ -412,9 +402,8 @@ function renderFinance() {
const paid = sumExpense ( pf , "paid" ) ;
const paid = sumExpense ( pf , "paid" ) ;
if ( ! rev && ! payment && ! cost && ! paid && ! gross ) return ;
if ( ! rev && ! payment && ! cost && ! paid && ! gross ) return ;
dataItems . push ( {
dataItems . push ( {
pf : pf , customer _name : pf . customer _name || '' , status : pf . status || '' ,
pf : pf , customer _name : pf . customer _name || '' , sign _amount : pf . sign _amount || 0 , sign _month : pf . sign _month || '' ,
sign _amount : pf . sign _amount || 0 , sign _mo nth : pf . sign _month || '' ,
rev : rev , gross : gross , cost : cost , payme nt: payment , paid : paid ,
rev : rev , gross : gross , cost : cost , profit : gross , payment : payment , paid : paid ,
cashflow : payment - paid ,
cashflow : payment - paid ,
exe _rate : rev && pf . sign _amount ? Math . round ( rev / pf . sign _amount * 100 ) : 0 ,
exe _rate : rev && pf . sign _amount ? Math . round ( rev / pf . sign _amount * 100 ) : 0 ,
gross _rate : rev && gross ? Math . round ( gross / rev * 100 ) : 0 ,
gross _rate : rev && gross ? Math . round ( gross / rev * 100 ) : 0 ,
@@ -431,7 +420,7 @@ function renderFinance() {
rows . push ( isUnsigned ? tdRowUnsigned ( d . pf , d . cost , d . gross ) : tdRow ( d . pf , d . rev , d . payment , d . cost , d . paid , d . gross ) ) ;
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 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 , '该季度暂无数据' ) ;
return renderView ( rows , sumRev , sumPay , sumCost , sumPaid , sumGross , signTotal , signCnt , '该季度暂无数据' ) ;
}
}
@@ -445,13 +434,12 @@ function renderFinance() {
expense . forEach ( e => { cost += parseFloat ( e . cost || 0 ) || 0 ; paid += parseFloat ( e . paid || 0 ) || 0 ; } ) ;
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 ) } ;
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 . finFilter ) ;
const allPfs = pfs ;
var dataItems = allPfs . map ( pf => {
var dataItems = allPfs . map ( pf => {
var t = calcTotals ( pf ) ;
var t = calcTotals ( pf ) ;
return {
return {
pf : pf , customer _name : pf . customer _name || '' , status : pf . status || '' ,
pf : pf , customer _name : pf . customer _name || '' , sign _amount : pf . sign _amount || 0 , sign _month : pf . sign _month || '' ,
sign _amount : pf . sign _amount || 0 , sign _month : pf . sign _month || '' ,
rev : t . rev , gross : t . gross , cost : t . cost ,
rev : t . rev , gross : t . gross , cost : t . cost , profit : t . gross ,
payment : t . payment , paid : t . paid , cashflow : t . payment - t . paid ,
payment : t . payment , paid : t . paid , cashflow : t . payment - t . paid ,
exe _rate : t . rev && pf . sign _amount ? Math . round ( t . rev / pf . sign _amount * 100 ) : 0 ,
exe _rate : t . rev && pf . sign _amount ? Math . round ( t . rev / pf . sign _amount * 100 ) : 0 ,
gross _rate : t . rev && t . gross ? Math . round ( t . gross / t . rev * 100 ) : 0 ,
gross _rate : t . rev && t . gross ? Math . round ( t . gross / t . rev * 100 ) : 0 ,
@@ -468,7 +456,7 @@ function renderFinance() {
rows . push ( isUnsigned ? tdRowUnsigned ( d . pf , d . cost , d . gross ) : tdRow ( d . pf , d . rev , d . payment , d . cost , d . paid , d . gross ) ) ;
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 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 , '暂无数据' ) ;
return renderView ( rows , sumRev , sumPay , sumCost , sumPaid , sumGross , signTotal , signCnt , '暂无数据' ) ;
} ) ( ) }
} ) ( ) }
< / d i v > ` ;
< / d i v > ` ;
@@ -780,6 +768,7 @@ window.openPfEditModal = (pfId) => {
}
}
} ) ;
} ) ;
form . querySelector ( '[name="customer_name"]' ) . value = pf . customer _name || "" ;
form . querySelector ( '[name="customer_name"]' ) . value = pf . customer _name || "" ;
const clientInput = form . querySelector ( '[name="client_name"]' ) ; if ( clientInput ) clientInput . value = pf . client _name || "" ;
const setVal = ( name , val ) => { const el = form . querySelector ( ` [name=" ${ name } "] ` ) ; if ( el ) el . value = val || "" ; } ;
const setVal = ( name , val ) => { const el = form . querySelector ( ` [name=" ${ name } "] ` ) ; if ( el ) el . value = val || "" ; } ;
setVal ( "project_code" , pf . project _code ) ;
setVal ( "project_code" , pf . project _code ) ;
form . querySelector ( '[name="sign_amount"]' ) . value = pf . sign _amount || "" ;
form . querySelector ( '[name="sign_amount"]' ) . value = pf . sign _amount || "" ;
@@ -789,7 +778,6 @@ window.openPfEditModal = (pfId) => {
signMonthEl . innerHTML = monthOptions ( signMonthValue ) ;
signMonthEl . innerHTML = monthOptions ( signMonthValue ) ;
signMonthEl . value = signMonthValue ;
signMonthEl . value = signMonthValue ;
}
}
form . querySelector ( '[name="status"]' ) . value = pf . status || "待签约" ;
form . querySelector ( '[name="sales_person"]' ) . value = pf . sales _person || "" ;
form . querySelector ( '[name="sales_person"]' ) . value = pf . sales _person || "" ;
form . querySelector ( '[name="owner"]' ) . value = pf . owner || "" ;
form . querySelector ( '[name="owner"]' ) . value = pf . owner || "" ;
setVal ( "start_date" , pf . start _date ) ;
setVal ( "start_date" , pf . start _date ) ;
@@ -937,69 +925,175 @@ window.deleteFinanceItem = async () => {
}
}
}
}
// 总览 渲染
// 经营月报 渲染
function renderFinanceOverview ( ) {
function renderFinanceOverview ( ) {
const { summary , financeMonthly } = state . data ;
const { summary , financeMonthly } = state . data ;
if ( ! summary ) return ;
if ( ! summary ) return ;
const m = summary . metrics ;
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 moneyWan = ( v ) => { const n = Number ( v || 0 ) ; return n >= 10000 ? ( n / 10000 ) . toFixed ( 0 ) + "万" : n . toLocaleString ( "zh-CN" ) ; } ;
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 card = ( body , cls ) => '<div class="card ' + ( cls || 'p-4' ) + '">' + body + '</div>' ;
const fm = financeMonthly || [ ] ;
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 mLbl = [ '1月' , '2月' , '3月' , '4月' , '5月' , '6月' , '7月' , '8月' , '9月' , '10月' , '11月' , '12月' ] ;
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 LABELS = [ '合同金额' , '确收金额' , '确收毛利' , '成本' , '支出' , '回款' , '项目现金流' ] ;
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 annualVals = [ m . signed _annual || m . signed _amount || 0 , m . revenue _annual || 0 , m . gross _annual || 0 , m . cost _annual || 0 , m . paid _annual || 0 , m . payment _annual || 0 , ( m . payment _annual || 0 ) - ( m . paid _annual || 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 fmFields = [ 'sign' , 'revenue' , 'gross' , 'cost' , 'paid' , 'payment' , null ] ;
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 ) ] ] ;
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>' ;
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 ) ] ] ;
for ( var mi = 0 ; mi < 12 ; mi ++ ) { thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl [ mi ] + '</th>' ; }
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 ) ] ] ;
thead += '</tr>' ;
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>' ; } ;
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>' ;
var tbody = '' ;
var tbody = '' ;
for ( var ri = 0 ; ri < 8 ; ri ++ ) {
for ( var ri = 0 ; ri < 7 ; ri ++ ) {
var vals = ROW S[ ri ] ;
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>' ;
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>' +
rowHtml += '<td class="py-2 text-righ t font-semibold text-slate-8 00 whitespace-nowrap">' + moneyIntL ( annualVals [ 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 ] ) +
for ( var mi = 0 ; mi < 12 ; mi ++ ) {
'<td class="py-2 text-right">' + qoq ( Q _FIELDS [ ri ] , Q _PREV [ ri ] ) + '</td>' +
var val = 0 ;
tdVal ( ri , 2 , vals [ 2 ] [ 1 ] ) +
if ( fm [ mi ] ) {
'<td class="py-2 text-right">' + qoq ( M _FIELDS [ r i] , M _PREV [ r i] ) + '</td></tr>' ;
if ( ri === 6 ) { val = ( fm [ m i] . payment || 0 ) - ( fm [ m i] . paid || 0 ) ; }
else { var fk = fmFields [ ri ] ; if ( fk ) val = fm [ mi ] [ fk ] || 0 ; }
}
var cls = 'text-slate-700' ;
if ( ri === 6 ) 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>' ;
}
}
// 业务线经营情况卡片
var finExp = state . data . expense || [ ] ;
var expMonthly = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ;
for ( var ei = 0 ; ei < finExp . length ; ei ++ ) {
if ( ( finExp [ ei ] . status || '' ) === '暂不计入' ) continue ;
var em = ( finExp [ ei ] . expense _month || '' ) . substring ( 0 , 7 ) ;
var mi2 = parseInt ( em . substring ( 5 ) ) - 1 ;
if ( mi2 >= 0 && mi2 < 12 ) expMonthly [ mi2 ] += parseFloat ( finExp [ ei ] . amount || 0 ) ;
}
var expAnnual = 0 ; for ( var ei = 0 ; ei < 12 ; ei ++ ) expAnnual += expMonthly [ ei ] ;
var grossAnnual = 0 ; for ( var ei = 0 ; ei < 12 ; ei ++ ) grossAnnual += ( fm [ ei ] ? fm [ ei ] . gross : 0 ) ;
var deptThead = '<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 dmi = 0 ; dmi < 12 ; dmi ++ ) { deptThead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl [ dmi ] + '</th>' ; }
deptThead += '</tr>' ;
var deptRows = [
{ label : '毛利' , vals : fm . map ( function ( x ) { return x ? x . gross : 0 ; } ) , annual : grossAnnual } ,
{ label : '平台费用' , vals : expMonthly , annual : expAnnual } ,
{ label : '利润' , vals : fm . map ( function ( x , i ) { return ( x ? x . gross : 0 ) - expMonthly [ i ] ; } ) , annual : grossAnnual - expAnnual }
] ;
var deptTbody = '' ;
for ( var dri = 0 ; dri < 3 ; dri ++ ) {
var dr = deptRows [ dri ] ;
var rowCls = dri === 2 ? 'font-semibold' : '' ;
var valCls = dri === 2 ? 'text-green-600' : 'text-slate-700' ;
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + dr . label + '</td>' ;
rowHtml += '<td class="py-2 text-right ' + rowCls + ' ' + valCls + ' whitespace-nowrap">' + moneyIntL ( dr . annual ) + '</td>' ;
for ( var dmi = 0 ; dmi < 12 ; dmi ++ ) {
rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + valCls + '">' + moneyIntL ( dr . vals [ dmi ] ) + '</td>' ;
}
deptTbody += rowHtml + '</tr>' ;
}
var deptCard = card ( '<h3 class="text-sm font-bold text-slate-700">业务线经营情况</h3><p class="text-xs text-slate-400 mb-3">毛利 - 平台费用 = 利润</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>' , 'p-4' ) ;
// 业务线现金流卡片
var cfMonthly = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ;
for ( var cfi = 0 ; cfi < 12 ; cfi ++ ) { cfMonthly [ cfi ] = ( fm [ cfi ] ? ( fm [ cfi ] . payment - fm [ cfi ] . paid ) : 0 ) ; }
var cfAnnual = 0 ; for ( var cfi = 0 ; cfi < 12 ; cfi ++ ) cfAnnual += cfMonthly [ cfi ] ;
var expPaidMonthly = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ;
for ( var epi = 0 ; epi < finExp . length ; epi ++ ) {
if ( ( finExp [ epi ] . status || '' ) === '暂不计入' ) continue ;
var epm = ( finExp [ epi ] . expense _month || '' ) . substring ( 0 , 7 ) ;
var epmi = parseInt ( epm . substring ( 5 ) ) - 1 ;
if ( epmi >= 0 && epmi < 12 ) expPaidMonthly [ epmi ] += parseFloat ( finExp [ epi ] . incurred _amount || 0 ) ;
}
var expPaidAnnual = 0 ; for ( var epi = 0 ; epi < 12 ; epi ++ ) expPaidAnnual += expPaidMonthly [ epi ] ;
var blCfMonthly = cfMonthly . map ( function ( v , i ) { return v - expPaidMonthly [ i ] ; } ) ;
var blCfAnnual = cfAnnual - expPaidAnnual ;
var cfRows = [
{ label : '项目现金流' , vals : cfMonthly , annual : cfAnnual } ,
{ label : '平台费用(支出)' , vals : expPaidMonthly , annual : expPaidAnnual } ,
{ label : '业务线现金流' , vals : blCfMonthly , annual : blCfAnnual }
] ;
var cfTbody = '' ;
for ( var cfri = 0 ; cfri < 3 ; cfri ++ ) {
var cfdr = cfRows [ cfri ] ;
var cfRowCls = cfri === 2 ? 'font-semibold' : '' ;
var cfValCls = cfri === 2 ? ( cfdr . annual >= 0 ? 'text-green-600' : 'text-red-600' ) : 'text-slate-700' ;
var cfRowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap">' + cfdr . label + '</td>' ;
cfRowHtml += '<td class="py-2 text-right ' + cfRowCls + ' ' + cfValCls + ' whitespace-nowrap">' + moneyIntL ( cfdr . annual ) + '</td>' ;
for ( var cfdmi = 0 ; cfdmi < 12 ; cfdmi ++ ) {
var cellCls = cfValCls ;
if ( cfri === 2 ) cellCls = blCfMonthly [ cfdmi ] >= 0 ? 'text-green-600' : 'text-red-600' ;
cfRowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL ( cfdr . vals [ cfdmi ] ) + '</td>' ;
}
cfTbody += cfRowHtml + '</tr>' ;
}
var cfCard = card ( '<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(支出)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>' , 'p-4' ) ;
document . querySelector ( "#financeOverview" ) . innerHTML = '<div class="grid gap-5">' +
document . querySelector ( "#financeOverview" ) . innerHTML = '<div class="grid gap-5">' +
card ( '<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流 </p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>' , 'p-4' ) +
card ( '<h3 class="text-sm font-bold text-slate-700">业务(项目)财务月度 概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出 </p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>' , 'p-4' ) +
'<div class="grid grid-cols-4 gap-5">' +
deptCard +
cfCard +
'<div class="grid grid-cols-3 gap-5">' +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartSign"></canvas></div>' , 'p-4' ) +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartSign"></canvas></div>' , 'p-4' ) +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartRev"></canvas></div>' , 'p-4' ) +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartRev"></canvas></div>' , 'p-4' ) +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度回款与已付 </h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartCash"></canvas></div>' , 'p-4' ) +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度回款与支出 </h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartCash"></canvas></div>' , 'p-4' ) +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度项目利润</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartProfit"></canvas></div>' , 'p-4' ) +
'</div></div>' ;
'</div></div>' ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
// 渲染图表(复用 home.js 的 moneyTick / chartOptions / monthLabels)
if ( financeMonthly && window . Chart ) {
if ( financeMonthly && window . Chart ) {
renderOverviewCharts ( financeMonthly ) ;
renderOverviewCharts ( financeMonthly ) ;
}
}
}
}
// 季度月报渲染
function renderFinanceQuarterlyOverview ( ) {
const { summary , financeMonthly } = state . data ;
if ( ! summary ) return ;
const m = summary . metrics ;
const moneyIntL = ( v ) => Math . round ( Number ( v || 0 ) ) . toLocaleString ( "zh-CN" ) ;
const card = ( body , cls ) => '<div class="card ' + ( cls || 'p-4' ) + '">' + body + '</div>' ;
const fm = financeMonthly || [ ] ;
const LABELS = [ '合同金额' , '确收金额' , '确收毛利' , '成本' , '支出' ] ;
const qLbl = [ 'Q1' , 'Q2' , 'Q3' , 'Q4' ] ;
const qRanges = [ [ 0 , 1 , 2 ] , [ 3 , 4 , 5 ] , [ 6 , 7 , 8 ] , [ 9 , 10 , 11 ] ] ;
const annualVals = [ m . signed _annual || m . signed _amount || 0 , m . revenue _annual || 0 , m . gross _annual || 0 , m . cost _annual || 0 , m . paid _annual || 0 ] ;
const fmFields = [ 'sign' , 'revenue' , 'gross' , 'cost' , 'paid' ] ;
var qData = [ [ 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 ] ] ;
for ( var qi = 0 ; qi < 4 ; qi ++ ) {
for ( var ri = 0 ; ri < 5 ; ri ++ ) {
var sum = 0 ;
for ( var mi = 0 ; mi < 3 ; mi ++ ) {
var idx = qRanges [ qi ] [ mi ] ;
if ( fm [ idx ] ) {
var fk = fmFields [ ri ] ; if ( fk ) sum += fm [ idx ] [ fk ] || 0 ;
}
}
qData [ ri ] [ qi ] = sum ;
}
}
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>' ;
thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">Q1</th>' ;
thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">Q2</th><th class="py-2 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>' ;
thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">Q3</th><th class="py-2 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>' ;
thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">Q4</th><th class="py-2 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>' ;
thead += '</tr>' ;
var qoq = function ( cur , prev ) { if ( ! prev ) return '<span class="text-slate-300">—</span>' ; var pct = Math . round ( ( cur - prev ) / prev * 100 ) ; var cls = pct >= 0 ? 'text-green-600' : 'text-red-600' ; var sign = pct >= 0 ? '+' : '' ; return '<span class="' + cls + '">' + sign + pct + '%</span>' ; } ;
var tbody = '' ;
for ( var ri = 0 ; ri < 5 ; ri ++ ) {
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">' + LABELS [ ri ] + '</td>' ;
rowHtml += '<td class="py-2 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL ( annualVals [ ri ] ) + '</td>' ;
for ( var qi = 0 ; qi < 4 ; qi ++ ) {
var val = qData [ ri ] [ qi ] ;
var cls = 'text-slate-700' ;
rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + cls + '">' + moneyIntL ( val ) + '</td>' ;
if ( qi > 0 ) { rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap">' + qoq ( val , qData [ ri ] [ qi - 1 ] ) + '</td>' ; }
}
tbody += rowHtml + '</tr>' ;
}
document . querySelector ( "#financeQuarterlyOverview" ) . innerHTML = '<div class="grid gap-5">' +
card ( '<h3 class="text-sm font-bold text-slate-700">业务(项目)财务季度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 支出 → 现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>' , 'p-4' ) +
'</div>' ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
}
function renderOverviewCharts ( data ) {
function renderOverviewCharts ( data ) {
if ( typeof monthLabels !== 'function' ) return ;
if ( typeof monthLabels !== 'function' ) return ;
const labels = monthLabels ( data ) ;
const labels = monthLabels ( data ) ;
@@ -1019,7 +1113,7 @@ function renderOverviewCharts(data) {
var c3 = document . querySelector ( "#finChartCash" ) ;
var c3 = document . querySelector ( "#finChartCash" ) ;
if ( c3 && window . Chart ) {
if ( c3 && window . Chart ) {
if ( state . finChart3 ) state . finChart3 . destroy ( ) ;
if ( state . finChart3 ) state . finChart3 . destroy ( ) ;
state . finChart3 = 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 } ] } } ) ) ;
state . finChart3 = 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 ( "#finChartProfit" ) ;
var c4 = document . querySelector ( "#finChartProfit" ) ;
if ( c4 && window . Chart ) {
if ( c4 && window . Chart ) {