feat: 项目模块状态Tab + 待签约简化视图
- 顶部新增已签约/待签约 Tab(复用finance-tab下划线样式) - 待签约视图:表格仅显示项目名称/状态/签约金额/签约月份/毛利/成本/项目利润 - 待签约视图:卡片仅显示签约金额/毛利/成本/项目利润 - 替换原状态筛选下拉框
This commit is contained in:
@@ -113,11 +113,14 @@ function renderFinance() {
|
|||||||
const toolQuarterSelect = qLabels2.map((l,i) => '<option value="'+i+'" '+(i===state.finQuarter?'selected':'')+'>'+l+'</option>').join("");
|
const toolQuarterSelect = qLabels2.map((l,i) => '<option value="'+i+'" '+(i===state.finQuarter?'selected':'')+'>'+l+'</option>').join("");
|
||||||
const toolDateSelect = state.finView==='monthly'?'<span class="text-xs text-slate-500 ml-2">月份:</span><select onchange="state.finMonth=this.value;renderFinance()" class="text-xs 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-xs text-slate-500 ml-2">季度:</span><select onchange="state.finQuarter=parseInt(this.value);localStorage.setItem(\'opc-fin-quarter\',this.value);renderFinance()" class="text-xs 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 toolDateSelect = state.finView==='monthly'?'<span class="text-xs text-slate-500 ml-2">月份:</span><select onchange="state.finMonth=this.value;renderFinance()" class="text-xs 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-xs text-slate-500 ml-2">季度:</span><select onchange="state.finQuarter=parseInt(this.value);localStorage.setItem(\'opc-fin-quarter\',this.value);renderFinance()" class="text-xs 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-xs text-slate-500">视图:</span><select onchange="setFinView(this.value)" class="text-xs 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><span class="text-xs text-slate-500 ml-3">状态:</span><select onchange="state.finFilter=this.value;renderFinance()" class="text-xs 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="已签约" ${state.finFilter==='已签约'?'selected':''}>已签约 (${pfs.filter(x=>x.status==='已签约').length})</option><option value="待签约" ${state.finFilter==='待签约'?'selected':''}>待签约 (${pfs.filter(x=>x.status==='待签约').length})</option></select>${toolDateSelect}`;
|
const finHeaderBase = `<span class="text-xs text-slate-500">视图:</span><select onchange="setFinView(this.value)" class="text-xs 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('已签约')" 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></div>`;
|
||||||
|
|
||||||
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
||||||
${card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "p-3")}
|
${finFilterTabs}
|
||||||
|
${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>
|
||||||
@@ -234,20 +237,21 @@ function renderFinance() {
|
|||||||
</div>
|
</div>
|
||||||
</div><div class="flex justify-end gap-3 pt-2 finance-form-actions"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closeFinanceModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>
|
</div><div class="flex justify-end gap-3 pt-2 finance-form-actions"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closeFinanceModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>
|
||||||
${(() => {
|
${(() => {
|
||||||
|
const isUnsigned = state.finFilter === '待签约';
|
||||||
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: '已确收', 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: '项目利润', value: ctx => { const v = ctx.sumGross; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
|
||||||
{ label: '已回款', value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' },
|
{ label: '已回款', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' },
|
||||||
{ label: '已付', value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' },
|
{ label: '已付', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' },
|
||||||
{ label: '现金流', 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: '执行率', 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.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' },
|
||||||
{ label: '毛利率', 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: '回款率', 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: '付款率', 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);
|
||||||
|
|
||||||
const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, emptyText) => {
|
const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, emptyText) => {
|
||||||
const ctx = { sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt };
|
const ctx = { sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt };
|
||||||
@@ -255,26 +259,41 @@ function renderFinance() {
|
|||||||
const v = c.value(ctx);
|
const v = c.value(ctx);
|
||||||
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 = '<thead><tr class="bg-slate-50 border-b border-slate-200">' +
|
const thead = isUnsigned
|
||||||
'<th class="p-2 text-center font-semibold align-middle">项目名称</th>' +
|
? '<thead><tr class="bg-slate-50 border-b border-slate-200">' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle">状态</th>' +
|
'<th class="p-2 text-center font-semibold align-middle">项目名称</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle">签约金额</th>' +
|
'<th class="p-2 text-center font-semibold align-middle">状态</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle text-blue-600">已确收</th>' +
|
'<th class="p-2 text-center font-semibold align-middle">签约金额</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle">毛利</th>' +
|
'<th class="p-2 text-center font-semibold align-middle">签约月份</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle text-rose-600">成本</th>' +
|
'<th class="p-2 text-center font-semibold align-middle">毛利</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle">项目利润</th>' +
|
'<th class="p-2 text-center font-semibold align-middle text-rose-600">成本</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle text-amber-600">已回款</th>' +
|
'<th class="p-2 text-center font-semibold align-middle">项目利润</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th>' +
|
'</tr></thead>'
|
||||||
'<th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th>' +
|
: '<thead><tr class="bg-slate-50 border-b border-slate-200">' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle">执行率</th>' +
|
'<th class="p-2 text-center font-semibold align-middle">项目名称</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle">毛利率</th>' +
|
'<th class="p-2 text-center font-semibold align-middle">状态</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle">回款率</th>' +
|
'<th class="p-2 text-center font-semibold align-middle">签约金额</th>' +
|
||||||
'<th class="p-2 text-center font-semibold align-middle">付款率</th>' +
|
'<th class="p-2 text-center font-semibold align-middle text-blue-600">已确收</th>' +
|
||||||
'</tr></thead>';
|
'<th class="p-2 text-center font-semibold align-middle">毛利</th>' +
|
||||||
const tbody = rows.length ? `<tbody>${rows.join("")}</tbody>` : `<tr><td colspan="15" class="p-6 text-center text-slate-400">${emptyText}</td></tr>`;
|
'<th class="p-2 text-center font-semibold align-middle text-rose-600">成本</th>' +
|
||||||
|
'<th class="p-2 text-center font-semibold align-middle">项目利润</th>' +
|
||||||
|
'<th class="p-2 text-center font-semibold align-middle text-amber-600">已回款</th>' +
|
||||||
|
'<th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th>' +
|
||||||
|
'<th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th>' +
|
||||||
|
'<th class="p-2 text-center font-semibold align-middle">执行率</th>' +
|
||||||
|
'<th class="p-2 text-center font-semibold align-middle">毛利率</th>' +
|
||||||
|
'<th class="p-2 text-center font-semibold align-middle">回款率</th>' +
|
||||||
|
'<th class="p-2 text-center font-semibold align-middle">付款率</th>' +
|
||||||
|
'</tr></thead>';
|
||||||
|
const theadCols = isUnsigned ? 7 : 14;
|
||||||
|
const tbody = rows.length ? `<tbody>${rows.join("")}</tbody>` : `<tr><td colspan="${theadCols}" class="p-6 text-center text-slate-400">${emptyText}</td></tr>`;
|
||||||
|
|
||||||
return card(`<div class="grid grid-cols-6 gap-3 mb-3">${cards.map(([l, v, c]) => '<div class="metric-card p-3"><span class="flex items-center gap-2 text-xs text-slate-500">' + l + '</span><strong class="mt-2 block text-xl ' + c + '">' + v + '</strong></div>').join("")}</div>`, "p-4") +
|
const cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-6';
|
||||||
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}${tbody}</table></div>`, "p-4");
|
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-xs 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");
|
||||||
};
|
};
|
||||||
|
|
||||||
const fmtNoUnit = (v) => v ? `<span class="font-medium">${Number(v||0).toLocaleString('zh-CN')}</span>` : '<span class="text-slate-300">—</span>';
|
const fmtNoUnit = (v) => v ? `<span class="font-medium">${Number(v||0).toLocaleString('zh-CN')}</span>` : '<span class="text-slate-300">—</span>';
|
||||||
@@ -292,6 +311,14 @@ function renderFinance() {
|
|||||||
const pfVal = profit ? money(profit) : '<span class="text-slate-300">—</span>';
|
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>`;
|
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 st = pf.status === '待签约' ? '<span class="text-amber-600">待签约</span>' : '<span class="text-green-600">已签约</span>';
|
||||||
|
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.filter(x => x.status === state.finFilter);
|
||||||
@@ -313,7 +340,7 @@ 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;
|
||||||
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
||||||
rows.push(tdRow(pf, rev, payment, cost, paid, gross));
|
rows.push(isUnsigned ? tdRowUnsigned(pf, cost, gross) : tdRow(pf, rev, payment, cost, paid, 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.status === "已签约" && (x.sign_month || "").substring(0, 7) === selMonth).length;
|
||||||
@@ -349,7 +376,7 @@ function renderFinance() {
|
|||||||
const cost = sumExpense(pf, "cost");
|
const cost = sumExpense(pf, "cost");
|
||||||
const paid = sumExpense(pf, "paid");
|
const paid = sumExpense(pf, "paid");
|
||||||
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
||||||
rows.push(tdRow(pf, rev, payment, cost, paid, gross));
|
rows.push(isUnsigned ? tdRowUnsigned(pf, cost, gross) : tdRow(pf, rev, payment, cost, paid, 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 => x.status === "已签约" && qRange.includes(parseInt((x.sign_month || "0").substring(5)) || 0)).length;
|
||||||
@@ -372,7 +399,7 @@ function renderFinance() {
|
|||||||
allPfs.forEach(pf => {
|
allPfs.forEach(pf => {
|
||||||
const t = calcTotals(pf);
|
const t = calcTotals(pf);
|
||||||
sumRev += t.rev; sumPay += t.payment; sumCost += t.cost; sumPaid += t.paid; sumGross += t.gross;
|
sumRev += t.rev; sumPay += t.payment; sumCost += t.cost; sumPaid += t.paid; sumGross += t.gross;
|
||||||
rows.push(tdRow(pf, t.rev, t.payment, t.cost, t.paid, t.gross));
|
rows.push(isUnsigned ? tdRowUnsigned(pf, t.cost, t.gross) : tdRow(pf, t.rev, t.payment, t.cost, t.paid, t.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.filter(x => x.status === "已签约").length;
|
||||||
|
|||||||
@@ -192,6 +192,10 @@ window.setFinView = (view) => {
|
|||||||
localStorage.setItem("opc-fin-view", view);
|
localStorage.setItem("opc-fin-view", view);
|
||||||
renderFinance();
|
renderFinance();
|
||||||
};
|
};
|
||||||
|
window.switchFinFilter = (filter) => {
|
||||||
|
state.finFilter = filter;
|
||||||
|
renderFinance();
|
||||||
|
};
|
||||||
window.switchTenant = (tenant) => {
|
window.switchTenant = (tenant) => {
|
||||||
state.tenant = tenant;
|
state.tenant = tenant;
|
||||||
state.selectedProject = null;
|
state.selectedProject = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user