|
|
|
@@ -98,7 +98,7 @@ function renderFinance() {
|
|
|
|
})();
|
|
|
|
})();
|
|
|
|
const sm = pf.sign_month || "";
|
|
|
|
const sm = pf.sign_month || "";
|
|
|
|
const signMonthCell = `<td class="px-2 py-px text-center align-middle text-sm"><span class="pf-sm-text cursor-pointer hover:text-brand-600" id="pf-sm-${pf.id}" onclick="event.stopPropagation(); editPfSignMonth(event, ${pf.id})">${sm || '—'}</span></td>`;
|
|
|
|
const signMonthCell = `<td class="px-2 py-px text-center align-middle text-sm"><span class="pf-sm-text cursor-pointer hover:text-brand-600" id="pf-sm-${pf.id}" onclick="event.stopPropagation(); editPfSignMonth(event, ${pf.id})">${sm || '—'}</span></td>`;
|
|
|
|
return `<tr class="border-b ${state._fmMarks.red[pf.id] ? 'bg-red-50' : 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-sm font-medium text-center align-middle">${esc(pf.customer_name)}</td>${signMonthCell}<td class="px-2 py-px text-center align-middle text-sm">${money(pf.sign_amount)}</td>${mCols}${totalCol}</tr>`;
|
|
|
|
return `<tr class="border-b ${fmMarkBg(pf.id) || 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-sm font-medium text-center align-middle">${esc(pf.customer_name)}</td>${signMonthCell}<td class="px-2 py-px text-center align-middle text-sm">${money(pf.sign_amount)}</td>${mCols}${totalCol}</tr>`;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const now2 = new Date();
|
|
|
|
const now2 = new Date();
|
|
|
|
@@ -119,7 +119,7 @@ function renderFinance() {
|
|
|
|
const activeQ = state.finQuarter !== undefined ? state.finQuarter : Math.floor(nowD.getMonth() / 3);
|
|
|
|
const activeQ = state.finQuarter !== undefined ? state.finQuarter : Math.floor(nowD.getMonth() / 3);
|
|
|
|
const activeMonths = qMonths[activeQ];
|
|
|
|
const activeMonths = qMonths[activeQ];
|
|
|
|
const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027];
|
|
|
|
const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027];
|
|
|
|
const finHeaderBase = `<div class="flex items-center gap-2"><span class="text-sm text-slate-500">年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white">${`<option value="0" ${state.finYear===0?'selected':''}>所有年份</option>`}${yearOpts.map(y => `<option value="${y}" ${state.finYear===y?'selected':''}>${y}年</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setFinView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='annual'?'bg-brand-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-0.5 rounded text-sm font-medium ${state.finView==='quarterly'&&activeQ===i?'bg-brand-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 = `${state.finYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setFinMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='monthly'&&state.finMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
|
|
|
const finHeaderBase = `<div class="flex items-center gap-2"><span class="text-sm text-slate-500">年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white">${`<option value="0" ${state.finYear===0?'selected':''}>所有年份</option>`}${yearOpts.map(y => `<option value="${y}" ${state.finYear===y?'selected':''}>${y}年</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setFinView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='annual'?'bg-brand-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-0.5 rounded text-sm font-medium ${state.finView==='quarterly'&&activeQ===i?'bg-brand-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 = `${state.finYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setFinMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='monthly'&&state.finMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
|
|
|
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 isOverviewMode = false;
|
|
|
|
const isOverviewMode = false;
|
|
|
|
@@ -349,13 +349,13 @@ 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 payable = cost - paid;
|
|
|
|
const payable = cost - paid;
|
|
|
|
const payableCls = payable > 0 ? 'text-rose-600' : payable < 0 ? 'text-green-600' : 'text-slate-700';
|
|
|
|
const payableCls = payable > 0 ? 'text-green-600' : payable < 0 ? 'text-red-600' : 'text-slate-700';
|
|
|
|
const payableVal = payable ? (payable > 0 ? '+' : '') + money(payable) : '<span class="text-slate-300">—</span>';
|
|
|
|
const payableVal = payable ? (payable > 0 ? '+' : '') + money(payable) : '<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 cf = cashflow ? money(cashflow) : '<span class="text-slate-300">—</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 pending = rev - payment;
|
|
|
|
const pending = rev - payment;
|
|
|
|
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-700';
|
|
|
|
const pendingCls = pending > 0 ? 'text-green-600' : pending < 0 ? 'text-red-600' : 'text-slate-700';
|
|
|
|
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
|
|
|
|
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
|
|
|
|
var budgetArr = [];
|
|
|
|
var budgetArr = [];
|
|
|
|
var expenseArr = [];
|
|
|
|
var expenseArr = [];
|
|
|
|
@@ -379,11 +379,11 @@ function renderFinance() {
|
|
|
|
var bCf = bPay - ePaid;
|
|
|
|
var bCf = bPay - ePaid;
|
|
|
|
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-600">' + m + '</td><td class="px-2 py-px text-center text-xs text-brand-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
|
|
|
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-600">' + m + '</td><td class="px-2 py-px text-center text-xs text-brand-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
|
|
|
}).join('');
|
|
|
|
}).join('');
|
|
|
|
return `<tr class="border-b ${state._fmMarks.red[pf.id] ? 'bg-red-50' : 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px 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="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-brand-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${payableCls}">${payableVal}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="14"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">月份</th><th class="px-2 py-1 text-center text-xs text-slate-600">确收</th><th class="px-2 py-1 text-center text-xs text-slate-600">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">成本</th><th class="px-2 py-1 text-center text-xs text-slate-600">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-600">应收</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
|
|
|
return `<tr class="border-b ${fmMarkBg(pf.id) || 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px 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="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-brand-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${payableCls}">${payableVal}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="14"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">月份</th><th class="px-2 py-1 text-center text-xs text-slate-600">确收</th><th class="px-2 py-1 text-center text-xs text-slate-600">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">成本</th><th class="px-2 py-1 text-center text-xs text-slate-600">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-600">应收</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
// 待签约简版行
|
|
|
|
// 待签约简版行
|
|
|
|
const tdRowUnsigned = (pf, cost, gross) => {
|
|
|
|
const tdRowUnsigned = (pf, cost, gross) => {
|
|
|
|
return `<tr class="border-b ${state._fmMarks.red[pf.id] ? 'bg-red-50' : 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px 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="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center align-middle text-sm">${pf.sign_month || '—'}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td></tr>`;
|
|
|
|
return `<tr class="border-b ${fmMarkBg(pf.id) || 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px 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="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center align-middle text-sm">${pf.sign_month || '—'}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td></tr>`;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (state.finView === 'monthly') {
|
|
|
|
if (state.finView === 'monthly') {
|
|
|
|
@@ -1104,11 +1104,12 @@ function renderFinanceOverview() {
|
|
|
|
for (var dri = 0; dri < 3; dri++) {
|
|
|
|
for (var dri = 0; dri < 3; dri++) {
|
|
|
|
var dr = deptRows[dri];
|
|
|
|
var dr = deptRows[dri];
|
|
|
|
var rowCls = dri === 2 ? 'font-semibold' : '';
|
|
|
|
var rowCls = dri === 2 ? 'font-semibold' : '';
|
|
|
|
var valCls = dri === 2 ? 'text-green-600' : 'text-slate-700';
|
|
|
|
var valCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
|
|
|
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + dr.label + '</td>';
|
|
|
|
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + dr.label + '</td>';
|
|
|
|
rowHtml += '<td class="py-1.5 text-right ' + rowCls + ' ' + valCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
|
|
|
|
rowHtml += '<td class="py-1.5 text-right ' + rowCls + ' ' + valCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
|
|
|
|
for (var dmi = 0; dmi < 12; dmi++) {
|
|
|
|
for (var dmi = 0; dmi < 12; dmi++) {
|
|
|
|
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + valCls + '">' + moneyIntL(dr.vals[dmi]) + '</td>';
|
|
|
|
var cellCls = dri === 2 ? (dr.vals[dmi] >= 0 ? 'text-green-600' : 'text-red-600') : valCls;
|
|
|
|
|
|
|
|
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL(dr.vals[dmi]) + '</td>';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
deptTbody += rowHtml + '</tr>';
|
|
|
|
deptTbody += rowHtml + '</tr>';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1151,7 +1152,7 @@ function renderFinanceOverview() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var cfCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
|
|
|
|
var cfCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
|
|
|
|
|
|
|
|
|
|
|
|
const yearSelectOv = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
|
|
|
|
const yearSelectOv = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
|
|
|
|
document.querySelector("#financeOverview").innerHTML = yearSelectOv + '<div class="grid gap-2.5">' +
|
|
|
|
document.querySelector("#financeOverview").innerHTML = yearSelectOv + '<div class="grid gap-2.5">' +
|
|
|
|
deptCard +
|
|
|
|
deptCard +
|
|
|
|
cfCard +
|
|
|
|
cfCard +
|
|
|
|
@@ -1292,7 +1293,7 @@ function renderFinanceQuarterlyOverview() {
|
|
|
|
cfTbody += rowHtml + '</tr>';
|
|
|
|
cfTbody += rowHtml + '</tr>';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const yearSelectQ = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
|
|
|
|
const yearSelectQ = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
|
|
|
|
document.querySelector("#financeQuarterlyOverview").innerHTML = yearSelectQ + '<div class="grid gap-2.5">' +
|
|
|
|
document.querySelector("#financeQuarterlyOverview").innerHTML = yearSelectQ + '<div class="grid gap-2.5">' +
|
|
|
|
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门损益情况 <span class="text-sm text-slate-500 font-normal ml-3">部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4') +
|
|
|
|
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门损益情况 <span class="text-sm text-slate-500 font-normal ml-3">部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4') +
|
|
|
|
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4') +
|
|
|
|
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4') +
|
|
|
|
@@ -1343,7 +1344,21 @@ window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; loc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 项目标记上下文菜单 ----
|
|
|
|
// ---- 项目标记上下文菜单 ----
|
|
|
|
if (!state._fmMarks) state._fmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_fm_marks')) || { red: {} }; } catch(e) { return { red: {} }; } })();
|
|
|
|
if (!state._fmMarks) state._fmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_fm_marks')) || { color: {} }; } catch(e) { return { color: {} }; } })();
|
|
|
|
|
|
|
|
if (!state._fmMarks.color) state._fmMarks.color = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const FM_COLORS = [
|
|
|
|
|
|
|
|
{ key: 'red', label: '标红', hex: '#ef4444', bg: 'bg-red-50' },
|
|
|
|
|
|
|
|
{ key: 'yellow', label: '标黄', hex: '#eab308', bg: 'bg-yellow-50' },
|
|
|
|
|
|
|
|
{ key: 'blue', label: '标蓝', hex: '#3b82f6', bg: 'bg-blue-50' },
|
|
|
|
|
|
|
|
{ key: 'green', label: '标绿', hex: '#22c55e', bg: 'bg-green-50' },
|
|
|
|
|
|
|
|
{ key: 'purple', label: '标紫', hex: '#a855f7', bg: 'bg-purple-50' },
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
const fmMarkBg = function(id) {
|
|
|
|
|
|
|
|
var c = (state._fmMarks.color||{})[id];
|
|
|
|
|
|
|
|
return c ? { red:'bg-red-50',yellow:'bg-yellow-50',blue:'bg-blue-50',green:'bg-green-50',purple:'bg-purple-50'}[c] || '' : '';
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.finShowCtx = function(e, pfId) {
|
|
|
|
window.finShowCtx = function(e, pfId) {
|
|
|
|
e.preventDefault();
|
|
|
|
e.preventDefault();
|
|
|
|
var menu = document.getElementById('finCtxMenu');
|
|
|
|
var menu = document.getElementById('finCtxMenu');
|
|
|
|
@@ -1352,12 +1367,27 @@ window.finShowCtx = function(e, pfId) {
|
|
|
|
menu.id = 'finCtxMenu';
|
|
|
|
menu.id = 'finCtxMenu';
|
|
|
|
menu.style.cssText = 'display:none;position:fixed;z-index:9999;background:#fff;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,.15);border:1px solid #e2e8f0;padding:4px 0;min-width:130px';
|
|
|
|
menu.style.cssText = 'display:none;position:fixed;z-index:9999;background:#fff;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,.15);border:1px solid #e2e8f0;padding:4px 0;min-width:130px';
|
|
|
|
menu.innerHTML = '<div style="padding:4px 12px;font-size:11px;font-weight:600;color:#94a3b8">项目标记</div>' +
|
|
|
|
menu.innerHTML = '<div style="padding:4px 12px;font-size:11px;font-weight:600;color:#94a3b8">项目标记</div>' +
|
|
|
|
'<div data-act="red" style="padding:6px 12px;font-size:13px;cursor:pointer;color:#334155"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#ef4444;margin-right:8px"></span>标红</div>';
|
|
|
|
FM_COLORS.map(function(c) {
|
|
|
|
|
|
|
|
return '<div data-act="' + c.key + '" style="padding:6px 12px;font-size:13px;cursor:pointer;color:#334155;display:flex;align-items:center"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + c.hex + ';margin-right:8px;flex-shrink:0"></span>' + c.label + '</div>';
|
|
|
|
|
|
|
|
}).join('') +
|
|
|
|
|
|
|
|
'<div style="border-top:1px solid #e2e8f0;margin:2px 0"></div>' +
|
|
|
|
|
|
|
|
'<div data-act="clear" style="padding:6px 12px;font-size:13px;cursor:pointer;color:#94a3b8">取消标记</div>';
|
|
|
|
menu.addEventListener('click', function(ev) {
|
|
|
|
menu.addEventListener('click', function(ev) {
|
|
|
|
|
|
|
|
var target = ev.target.closest('[data-act]');
|
|
|
|
|
|
|
|
if (!target) return;
|
|
|
|
ev.stopPropagation();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
var act = target.getAttribute('data-act');
|
|
|
|
var id = menu.getAttribute('data-pfid');
|
|
|
|
var id = menu.getAttribute('data-pfid');
|
|
|
|
menu.style.display = 'none';
|
|
|
|
menu.style.display = 'none';
|
|
|
|
if (state._fmMarks.red[id]) { delete state._fmMarks.red[id]; } else { state._fmMarks.red[id] = true; }
|
|
|
|
if (act === 'clear') {
|
|
|
|
|
|
|
|
delete state._fmMarks.color[id];
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if (state._fmMarks.color[id] === act) {
|
|
|
|
|
|
|
|
delete state._fmMarks.color[id];
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
state._fmMarks.color[id] = act;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
|
|
|
|
localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
|
|
|
|
renderFinance();
|
|
|
|
renderFinance();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@@ -1366,15 +1396,6 @@ window.finShowCtx = function(e, pfId) {
|
|
|
|
menu.setAttribute('data-pfid', pfId);
|
|
|
|
menu.setAttribute('data-pfid', pfId);
|
|
|
|
menu.style.display = 'block';
|
|
|
|
menu.style.display = 'block';
|
|
|
|
menu.style.left = e.pageX + 'px';
|
|
|
|
menu.style.left = e.pageX + 'px';
|
|
|
|
menu.style.top = Math.min(e.pageY, window.innerHeight - 200) + 'px';
|
|
|
|
menu.style.top = Math.min(e.pageY, window.innerHeight - 280) + 'px';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
document.addEventListener('click', function() { var m = document.getElementById('finCtxMenu'); if (m) m.style.display = 'none'; });
|
|
|
|
document.addEventListener('click', function() { var m = document.getElementById('finCtxMenu'); if (m) m.style.display = 'none'; });
|
|
|
|
|
|
|
|
|
|
|
|
window.finCtxMenu_act = function(e, type) {
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
var pfId = document.getElementById("finCtxMenu").getAttribute("data-pfid");
|
|
|
|
|
|
|
|
if (type === "clear") { delete state._fmMarks.red[pfId]; delete state._fmMarks.pin[pfId]; }
|
|
|
|
|
|
|
|
else { if (state._fmMarks[type][pfId]) { delete state._fmMarks[type][pfId]; } else { state._fmMarks[type][pfId] = true; } }
|
|
|
|
|
|
|
|
localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
|
|
|
|
|
|
|
|
renderFinance();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|