feat: 计划模块 + 待签约迁移 + 表格排序 + 已签约→项目
- 新增计划模块(plan_finances/plan_expense_records, 侧边栏计划tab) - 待签约项目自动迁移到计划模块(data_fixes 幂等) - 已签约tab改名为项目(业务+计划两个模块) - 业务模块表格点击列头排序(月度/季度/总视图) - 计划与业务侧边栏位置对调 - plan模块CSS修复(使用finance-tab类) - 版本号 v1.0.0.17
This commit is contained in:
@@ -116,7 +116,7 @@ function renderFinance() {
|
||||
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 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('已签约')" 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>`;
|
||||
|
||||
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
||||
${finFilterTabs}
|
||||
@@ -261,38 +261,49 @@ function renderFinance() {
|
||||
{ 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);
|
||||
|
||||
// 列排序
|
||||
if (!state.finSort) state.finSort = { key: null, asc: true };
|
||||
const sortData = (data, key) => {
|
||||
if (!key || !state.finSort.key) return data;
|
||||
var asc = state.finSort.asc ? 1 : -1;
|
||||
return data.slice().sort(function(a, b) {
|
||||
var va = a[key], vb = b[key];
|
||||
if (typeof va === 'string') va = va || '', vb = vb || '';
|
||||
else { va = parseFloat(va) || 0; vb = parseFloat(vb) || 0; }
|
||||
if (va < vb) return -1 * asc;
|
||||
if (va > vb) return 1 * asc;
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
const buildThead = (cols, sortKey) => {
|
||||
var h = '<thead><tr class="bg-slate-50 border-b border-slate-200">';
|
||||
cols.forEach(function(c) {
|
||||
var arrow = sortKey ? (sortKey === c.key ? (state.finSort.asc ? ' ↑' : ' ↓') : '') : '';
|
||||
h += '<th class="p-2 text-center font-semibold align-middle cursor-pointer select-none" onclick="setFinSort(\'' + sortKey + '|' + c.key + '\')">' + c.label + arrow + '</th>';
|
||||
});
|
||||
h += '</tr></thead>';
|
||||
return h;
|
||||
};
|
||||
const SIGNED_COLS = [
|
||||
{ key: 'customer_name', label: '项目名称' }, { key: 'status', label: '状态' }, { key: 'sign_amount', label: '签约金额' },
|
||||
{ key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'cost', label: '成本' },
|
||||
{ key: 'profit', label: '项目利润' }, { key: 'payment', label: '已回款' }, { key: 'paid', label: '已付' },
|
||||
{ key: 'cashflow', label: '现金流' }, { key: 'exe_rate', label: '执行率' }, { key: 'gross_rate', label: '毛利率' },
|
||||
{ key: 'pay_rate', label: '回款率' }, { key: 'paid_rate', label: '付款率' },
|
||||
];
|
||||
const UNSIGNED_COLS = [
|
||||
{ key: 'customer_name', label: '项目名称' }, { key: 'status', label: '状态' }, { key: 'sign_amount', 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 ctx = { sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt };
|
||||
const cards = METRIC_CARDS.map(c => {
|
||||
const v = c.value(ctx);
|
||||
return typeof v === 'object' ? [c.label, v.val, v.cls] : [c.label, v, c.color];
|
||||
});
|
||||
const thead = isUnsigned
|
||||
? '<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 text-rose-600">成本</th>' +
|
||||
'<th class="p-2 text-center font-semibold align-middle">项目利润</th>' +
|
||||
'</tr></thead>'
|
||||
: '<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 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 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 thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig');
|
||||
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>`;
|
||||
|
||||
@@ -336,8 +347,7 @@ function renderFinance() {
|
||||
const defaultMonth = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
|
||||
if (!state.finMonth) state.finMonth = defaultMonth;
|
||||
const selMonth = state.finMonth;
|
||||
const rows = [];
|
||||
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
|
||||
var dataItems = [];
|
||||
allPfs.forEach(pf => {
|
||||
let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
|
||||
let ed = []; try { ed = JSON.parse(pf.expense_data || "[]"); } catch (e) {}
|
||||
@@ -349,8 +359,24 @@ function renderFinance() {
|
||||
const cost = Math.round(parseFloat(e.cost || 0) || 0);
|
||||
const paid = Math.round(parseFloat(e.paid || 0) || 0);
|
||||
if (!rev && !payment && !cost && !paid && !gross) return;
|
||||
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
||||
rows.push(isUnsigned ? tdRowUnsigned(pf, cost, gross) : tdRow(pf, rev, payment, cost, paid, gross));
|
||||
dataItems.push({
|
||||
pf: pf, customer_name: pf.customer_name || '', status: pf.status || '',
|
||||
sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
||||
rev: rev, gross: gross, cost: cost, profit: gross, payment: payment, paid: paid,
|
||||
cashflow: payment - paid,
|
||||
exe_rate: rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) : 0,
|
||||
gross_rate: rev && gross ? Math.round(gross / rev * 100) : 0,
|
||||
pay_rate: rev && payment ? Math.round(payment / rev * 100) : 0,
|
||||
paid_rate: cost && paid ? Math.round(paid / cost * 100) : 0,
|
||||
});
|
||||
});
|
||||
var sortK = state.finSort && state.finSort.key ? state.finSort.key.split('|')[1] : null;
|
||||
dataItems = sortData(dataItems, sortK);
|
||||
const rows = [];
|
||||
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
|
||||
dataItems.forEach(d => {
|
||||
sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += 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 signCnt = allPfs.filter(x => x.status === "已签约" && (x.sign_month || "").substring(0, 7) === selMonth).length;
|
||||
@@ -377,8 +403,7 @@ function renderFinance() {
|
||||
try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(e[field] || 0); }); } catch (e) {}
|
||||
return total;
|
||||
};
|
||||
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
|
||||
const rows = [];
|
||||
var dataItems = [];
|
||||
allPfs.forEach(pf => {
|
||||
const rev = sumBudget(pf, "rev");
|
||||
const payment = sumBudget(pf, "payment");
|
||||
@@ -386,8 +411,24 @@ function renderFinance() {
|
||||
const cost = sumExpense(pf, "cost");
|
||||
const paid = sumExpense(pf, "paid");
|
||||
if (!rev && !payment && !cost && !paid && !gross) return;
|
||||
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
||||
rows.push(isUnsigned ? tdRowUnsigned(pf, cost, gross) : tdRow(pf, rev, payment, cost, paid, gross));
|
||||
dataItems.push({
|
||||
pf: pf, customer_name: pf.customer_name || '', status: pf.status || '',
|
||||
sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
||||
rev: rev, gross: gross, cost: cost, profit: gross, payment: payment, paid: paid,
|
||||
cashflow: payment - paid,
|
||||
exe_rate: rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) : 0,
|
||||
gross_rate: rev && gross ? Math.round(gross / rev * 100) : 0,
|
||||
pay_rate: rev && payment ? Math.round(payment / rev * 100) : 0,
|
||||
paid_rate: cost && paid ? Math.round(paid / cost * 100) : 0,
|
||||
});
|
||||
});
|
||||
var sortK = state.finSort && state.finSort.key ? state.finSort.key.split('|')[1] : null;
|
||||
dataItems = sortData(dataItems, sortK);
|
||||
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
|
||||
const rows = [];
|
||||
dataItems.forEach(d => {
|
||||
sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += 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 signCnt = allPfs.filter(x => x.status === "已签约" && qRange.includes(parseInt((x.sign_month || "0").substring(5)) || 0)).length;
|
||||
@@ -405,12 +446,26 @@ function renderFinance() {
|
||||
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);
|
||||
var dataItems = allPfs.map(pf => {
|
||||
var t = calcTotals(pf);
|
||||
return {
|
||||
pf: pf, customer_name: pf.customer_name || '', status: pf.status || '',
|
||||
sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
||||
rev: t.rev, gross: t.gross, cost: t.cost, profit: t.gross,
|
||||
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,
|
||||
gross_rate: t.rev && t.gross ? Math.round(t.gross / t.rev * 100) : 0,
|
||||
pay_rate: t.rev && t.payment ? Math.round(t.payment / t.rev * 100) : 0,
|
||||
paid_rate: t.cost && t.paid ? Math.round(t.paid / t.cost * 100) : 0,
|
||||
};
|
||||
});
|
||||
var sortK = state.finSort && state.finSort.key ? state.finSort.key.split('|')[1] : null;
|
||||
dataItems = sortData(dataItems, sortK);
|
||||
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
|
||||
const rows = [];
|
||||
allPfs.forEach(pf => {
|
||||
const t = calcTotals(pf);
|
||||
sumRev += t.rev; sumPay += t.payment; sumCost += t.cost; sumPaid += t.paid; sumGross += t.gross;
|
||||
rows.push(isUnsigned ? tdRowUnsigned(pf, t.cost, t.gross) : tdRow(pf, t.rev, t.payment, t.cost, t.paid, t.gross));
|
||||
dataItems.forEach(d => {
|
||||
sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += 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 signCnt = allPfs.filter(x => x.status === "已签约").length;
|
||||
|
||||
Reference in New Issue
Block a user