+
+
| 月份 | 任务类型 | 任务数量 | 已执行 | 差额 | 单价 | 执行金额 | 未执行金额 | |
-
+
-
+
-
+
${(() => {
if (state.planFilter === 'overview') {
setTimeout(() => renderPlanOverview(), 10);
- return ``;
+ return ``;
}
if (state.planFilter === 'expense') {
setTimeout(() => renderPlanExpense(), 10);
return ``;
}
- const isUnsigned = state.planFilter === '待签约';
+ const isUnsigned = false;
const METRIC_CARDS = [
{ label: '签约金额', value: ctx => moneyWan(ctx.signTotal), color: 'text-slate-700' },
{ label: '已确收', hideUnsigned: true, value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' },
@@ -257,7 +257,7 @@ function renderPlan() {
{ label: '毛利率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
{ label: '回款率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
{ label: '付款率', hideUnsigned: true, value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' },
- ].filter(c => state.planFilter !== '待签约' || !c.hideUnsigned);
+ ].filter(c => true);
// 列排序
if (!state.planSort) state.planSort = { key: null, asc: true };
@@ -342,7 +342,7 @@ function renderPlan() {
const profit = gross;
const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600';
const pfVal = profit ? money(profit) : '—';
- return `| ${esc(pf.customer_name)} | ${st} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} | ${fmtNoUnit(payment)} | ${fmtNoUnit(paid)} | ${cf} | ${exe} | ${grossR} | ${payR} | ${paidR} |
`;
+ return `| ${esc(pf.customer_name)} | ${st} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} | ${fmtNoUnit(payment)} | ${fmtNoUnit(paid)} | ${cf} | ${exe} | ${grossR} | ${payR} | ${paidR} |
`;
};
// 待签约简版行
const tdRowUnsigned = (pf, cost, gross) => {
@@ -350,7 +350,7 @@ function renderPlan() {
const profit = gross;
const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600';
const pfVal = profit ? money(profit) : '—';
- return `| ${esc(pf.customer_name)} | ${st} | ${money(pf.sign_amount)} | ${pf.sign_month || '—'} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} |
`;
+ return `| ${esc(pf.customer_name)} | ${st} | ${money(pf.sign_amount)} | ${pf.sign_month || '—'} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} |
`;
};
if (state.planView === 'monthly') {
@@ -508,46 +508,46 @@ window.openPlanModal = () => {
if (dept) dept.value = state.tenant;
const pfIdInput = form.querySelector('[name="pf_id"]');
if (!pfIdInput || !pfIdInput.value) {
- initRevpayTable(null);
- initCostTable(null);
- initTaskTable(null);
+ planInitRevpayTable(null);
+ planInitCostTable(null);
+ planInitTaskTable(null);
document.querySelector("#planDeleteBtn").classList.add("hidden");
}
modal.classList.remove("hidden");
};
-window.addRevpayRow = (month = '', rev = '', gross = '', payment = '', rev_note = '') => {
- const tbody = document.querySelector("#revpayTbody");
+window.planAddRevpayRow = (month = '', rev = '', gross = '', payment = '', rev_note = '') => {
+ const tbody = document.querySelector("#plan_revpayTbody");
if (!tbody) return;
const row = document.createElement("tr");
- row.innerHTML = ` |
- |
- |
- |
+ row.innerHTML = ` |
+ |
+ |
+ |
|
- | `;
+ | `;
tbody.appendChild(row);
if (window.lucide) window.lucide.createIcons();
};
-window.addCostRow = (month = '', expense_type = '', cost = '', paid = '', exp_note = '') => {
- const tbody = document.querySelector("#costTbody");
+window.planAddCostRow = (month = '', expense_type = '', cost = '', paid = '', exp_note = '') => {
+ const tbody = document.querySelector("#plan_costTbody");
if (!tbody) return;
const row = document.createElement("tr");
- row.innerHTML = ` |
+ row.innerHTML = ` |
|
- |
- |
+ |
+ |
|
- | `;
+ | `;
tbody.appendChild(row);
if (window.lucide) window.lucide.createIcons();
};
-window.updateRevpaySummary = () => {
- const revEl = document.querySelector("#revpayTotalRev");
- const grossEl = document.querySelector("#revpayTotalGross");
- const paymentEl = document.querySelector("#revpayTotalPayment");
+window.planUpdateRevpaySummary = () => {
+ const revEl = document.querySelector("#plan_revpayTotalRev");
+ const grossEl = document.querySelector("#plan_revpayTotalGross");
+ const paymentEl = document.querySelector("#plan_revpayTotalPayment");
if (!revEl || !paymentEl) return;
const revInputs = document.querySelectorAll('[name="budget_rev[]"]');
const grossInputs = document.querySelectorAll('[name="budget_gross[]"]');
@@ -561,9 +561,9 @@ window.updateRevpaySummary = () => {
paymentEl.textContent = money(totalPayment);
};
-window.updateCostSummary = () => {
- const costEl = document.querySelector("#costTotalCost");
- const paidEl = document.querySelector("#costTotalPaid");
+window.planUpdateCostSummary = () => {
+ const costEl = document.querySelector("#plan_costTotalCost");
+ const paidEl = document.querySelector("#plan_costTotalPaid");
if (!costEl || !paidEl) return;
const costInputs = document.querySelectorAll('[name="expense_cost[]"]');
const paidInputs = document.querySelectorAll('[name="expense_paid[]"]');
@@ -574,27 +574,27 @@ window.updateCostSummary = () => {
paidEl.textContent = money(totalPaid);
};
-window.initRevpayTable = (budgetData) => {
- const tbody = document.querySelector("#revpayTbody");
+window.planInitRevpayTable = (budgetData) => {
+ const tbody = document.querySelector("#plan_revpayTbody");
if (!tbody) return;
tbody.innerHTML = "";
const rows = budgetData || [];
- rows.forEach(r => addRevpayRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.rev_note || ''));
- setTimeout(() => updateRevpaySummary(), 50);
+ rows.forEach(r => planAddRevpayRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.rev_note || ''));
+ setTimeout(() => planUpdateRevpaySummary(), 50);
};
-window.initCostTable = (expenseData) => {
- const tbody = document.querySelector("#costTbody");
+window.planInitCostTable = (expenseData) => {
+ const tbody = document.querySelector("#plan_costTbody");
if (!tbody) return;
tbody.innerHTML = "";
const rows = expenseData || [];
- rows.forEach(r => addCostRow(r.month || '', r.expense_type || '', r.cost || '', r.paid || '', r.exp_note || ''));
- setTimeout(() => updateCostSummary(), 50);
+ rows.forEach(r => planAddCostRow(r.month || '', r.expense_type || '', r.cost || '', r.paid || '', r.exp_note || ''));
+ setTimeout(() => planUpdateCostSummary(), 50);
};
// ---------- 任务管理 tab ----------
-function taskMonthOptions(selected) {
+function planTaskMonthOptions(selected) {
const now = new Date();
const opts = [];
for (let i = -2; i <= 12; i++) {
@@ -605,36 +605,36 @@ function taskMonthOptions(selected) {
return opts.join("");
}
-window.addTaskRow = (taskMonth = '', taskType = '', taskCount = '', executedCount = '', unitPrice = '') => {
- const tbody = document.querySelector("#taskTbody");
+window.planAddTaskRow = (taskMonth = '', taskType = '', taskCount = '', executedCount = '', unitPrice = '') => {
+ const tbody = document.querySelector("#plan_taskTbody");
if (!tbody) return;
const row = document.createElement("tr");
const defaultMonth = (() => { const n = new Date(); return n.getFullYear() + "-" + String(n.getMonth()+1).padStart(2,"0"); })();
const isPreset = TASK_TYPES.includes(taskType);
const typeCell = isPreset || !taskType
- ? ``
- : ``;
- row.innerHTML = ` |
+ ? ``
+ : ``;
+ row.innerHTML = ` |
${typeCell} |
- |
- |
+ |
+ |
|
- |
+ |
|
|
| `;
tbody.appendChild(row);
if (window.lucide) window.lucide.createIcons();
- updateRowCalc(row);
+ planUpdateRowCalc(row);
};
-window.updateTaskDiff = (el) => {
+window.planUpdateTaskDiff = (el) => {
const row = el.closest('tr');
if (!row) return;
- updateRowCalc(row);
+ planUpdateRowCalc(row);
};
-function updateRowCalc(row) {
+function planUpdateRowCalc(row) {
const countInput = row.querySelector('[name="task_count[]"]');
const execInput = row.querySelector('[name="task_executed[]"]');
const priceInput = row.querySelector('[name="task_unit_price[]"]');
@@ -655,7 +655,7 @@ function updateRowCalc(row) {
unexecAmtInput.value = unexecAmt ? unexecAmt.toFixed(2) : '';
}
-window.toggleBtChip = (chip) => {
+window.planToggleBtChip = (chip) => {
const cb = chip.querySelector('input');
cb.checked = !cb.checked;
chip.classList.toggle('bg-blue-50', cb.checked);
@@ -663,35 +663,36 @@ window.toggleBtChip = (chip) => {
chip.classList.toggle('text-blue-600', cb.checked);
};
-window.initTaskTable = (taskData) => {
- const tbody = document.querySelector("#taskTbody");
+window.planInitTaskTable = (taskData) => {
+ const tbody = document.querySelector("#plan_taskTbody");
if (!tbody) return;
tbody.innerHTML = "";
const rows = taskData || [];
- rows.forEach(r => addTaskRow(r.task_month || '', r.task_type || '', r.task_count || '', r.task_executed || '', r.unit_price || ''));
+ rows.forEach(r => planAddTaskRow(r.task_month || '', r.task_type || '', r.task_count || '', r.task_executed || '', r.unit_price || ''));
};
-window.onTaskTypeChange = (sel) => {
+window.planOnTaskTypeChange = (sel) => {
if (sel.value !== '__custom__') return;
const td = sel.parentElement;
const oldVal = sel.value;
- td.innerHTML = ``;
+ td.innerHTML = ``;
if (window.lucide) window.lucide.createIcons();
td.querySelector('input').focus();
};
-window.revertTaskType = (btn) => {
+window.planRevertTaskType = (btn) => {
const td = btn.parentElement;
- td.innerHTML = ``;
+ td.innerHTML = ``;
if (window.lucide) window.lucide.createIcons();
};
window.closePlanModal = () => {
const modal = document.querySelector("#planModal");
- modal.classList.add("hidden");
+ if (modal) modal.classList.add("hidden");
+ render();
};
-window.editPfSignMonth = (event, pfId) => {
+window.planEditSignMonth = (event, pfId) => {
event.stopPropagation();
const pf = (state.data.planFinances || []).find(x => x.id === pfId);
if (!pf) return;
@@ -707,11 +708,11 @@ window.editPfSignMonth = (event, pfId) => {
try {
await api(`/api/planFinances/${pfId}`, { method: "PUT", body: JSON.stringify({ data: { sign_month: newValue } }) });
pf.sign_month = newValue;
- td.innerHTML = `${newValue || '—'}`;
+ td.innerHTML = `${newValue || '—'}`;
} catch (e) { toast("修改失败:" + e.message, "error"); }
});
select.addEventListener("blur", () => {
- td.innerHTML = `${currentValue || '—'}`;
+ td.innerHTML = `${currentValue || '—'}`;
});
td.innerHTML = "";
td.appendChild(select);
@@ -720,25 +721,25 @@ window.editPfSignMonth = (event, pfId) => {
window.switchPlanTab = (tab) => {
document.querySelectorAll(".plan-tab").forEach(b => b.classList.toggle("active", b.dataset.tab === tab));
- document.querySelector("#financeTabInfo").classList.toggle("hidden", tab !== "info");
- document.querySelector("#financeTabRevpay").classList.toggle("hidden", tab !== "revpay");
- document.querySelector("#financeTabCost").classList.toggle("hidden", tab !== "cost");
- document.querySelector("#financeTabExec").classList.toggle("hidden", tab !== "exec");
- document.querySelector("#financeTabTasks").classList.toggle("hidden", tab !== "tasks");
- document.querySelector("#financeTabActivity").classList.toggle("hidden", tab !== "activity");
+ document.querySelector("#plan_financeTabInfo").classList.toggle("hidden", tab !== "info");
+ document.querySelector("#plan_financeTabRevpay").classList.toggle("hidden", tab !== "revpay");
+ document.querySelector("#plan_financeTabCost").classList.toggle("hidden", tab !== "cost");
+ document.querySelector("#plan_financeTabExec").classList.toggle("hidden", tab !== "exec");
+ document.querySelector("#plan_financeTabTasks").classList.toggle("hidden", tab !== "tasks");
+ document.querySelector("#plan_financeTabActivity").classList.toggle("hidden", tab !== "activity");
document.querySelector(".plan-form-actions").classList.toggle("hidden", tab === "activity");
- if (tab === "activity") initFinSquire();
+ if (tab === "activity") planInitSquire();
};
// ---------- 活动与跟进 ----------
-async function loadFinFollowups(pfId) {
- const list = document.querySelector("#finActivityList");
+async function planLoadFollowups(pfId) {
+ const list = document.querySelector("#plan_finActivityList");
if (!list || !pfId) return;
try {
const fups = await api(`/api/followups/project_finance/${pfId}`);
list.innerHTML = fups.length
- ? fups.map(f => `
${esc(f.follower)} · ${esc(f.follow_up_method)}${esc(f.followed_at)}
${f.next_action ? `
下一步:${text(f.next_action)}
` : ""}
`).join("")
+ ? fups.map(f => `
${esc(f.follower)} · ${esc(f.follow_up_method)}${esc(f.followed_at)}
${f.next_action ? `
下一步:${text(f.next_action)}
` : ""}
`).join("")
: '暂无跟进记录
';
if (window.lucide) window.lucide.createIcons();
list.querySelectorAll(".rich-content").forEach(el => {
@@ -748,8 +749,8 @@ async function loadFinFollowups(pfId) {
} catch (e) { /* ignore */ }
}
-function initFinSquire() {
- const ed = document.querySelector("#squire_finance");
+function planInitSquire() {
+ const ed = document.querySelector("#plan_squire_finance");
if (!ed || !window.Squire) return;
if (window.squireInstances["squire_finance"]) { window.squireInstances["squire_finance"].destroy(); }
const sq = new Squire(ed, { blockTag: "P" });
@@ -758,8 +759,8 @@ function initFinSquire() {
ed.addEventListener("blur", () => { if (!ed.textContent.trim()) ed.classList.remove("focused"); });
}
-window.submitFinComment = async () => {
- const pfId = document.querySelector("#pf-id-input").value;
+window.planSubmitComment = async () => {
+ const pfId = document.querySelector("#plan_pf-id-input").value;
if (!pfId) return;
const sq = window.squireInstances["squire_finance"];
const content = sq ? sq.getHTML().trim() : "";
@@ -771,22 +772,22 @@ window.submitFinComment = async () => {
sq.setHTML("");
btn.disabled = false;
btn.textContent = "评论";
- await loadFinFollowups(pfId);
+ await planLoadFollowups(pfId);
};
-window.deleteFinFollowup = async (event, followupId) => {
+window.planDeleteFollowup = async (event, followupId) => {
event.stopPropagation();
if (!confirm("确认删除这条评论?")) return;
await api(`/api/followups/${followupId}`, { method: "DELETE" });
- const pfId = document.querySelector("#pf-id-input").value;
- if (pfId) await loadFinFollowups(pfId);
+ const pfId = document.querySelector("#plan_pf-id-input").value;
+ if (pfId) await planLoadFollowups(pfId);
};
-window.openPfEditModal = (pfId) => {
+window.planOpenPfEditModal = (pfId) => {
const pf = (state.data.planFinances || []).find(x => x.id === pfId);
if (!pf) return;
- document.querySelector("#pf-id-input").value = pf.id;
- document.querySelector("#financeModalTitle").textContent = "编辑项目财务";
+ document.querySelector("#plan_pf-id-input").value = pf.id;
+ document.querySelector("#planModalTitle").textContent = "编辑项目财务";
document.querySelector("#planDeleteBtn").classList.remove("hidden");
const form = document.querySelector("#financeModal form");
form.querySelector('[name="project_id"]').value = pf.project_id || "";
@@ -827,15 +828,15 @@ window.openPfEditModal = (pfId) => {
setVal("other_info", pf.other_info);
let budgetData = [];
try { budgetData = JSON.parse(pf.budget_data || "[]"); } catch (e) { budgetData = []; }
- initRevpayTable(budgetData.length ? budgetData : null);
+ planInitRevpayTable(budgetData.length ? budgetData : null);
let expenseData = [];
try { expenseData = JSON.parse(pf.expense_data || "[]"); } catch (e) { expenseData = []; }
- initCostTable(expenseData.length ? expenseData : null);
+ planInitCostTable(expenseData.length ? expenseData : null);
let taskData = [];
try { taskData = JSON.parse(pf.task_data || "[]"); } catch (e) { taskData = []; }
- initTaskTable(taskData.length ? taskData : null);
+ planInitTaskTable(taskData.length ? taskData : null);
setTimeout(() => updateBudgetSummary(), 100);
- loadFinFollowups(pf.id);
+ planLoadFollowups(pf.id);
openPlanModal();
};
@@ -936,8 +937,8 @@ window.createPlanFinance = async (event) => {
if (result.id && data.customer_name) logActivity("finance", result.id, "创建了「" + data.customer_name + "」的财务项目");
}
form.reset();
- document.querySelector("#pf-id-input").value = "";
- document.querySelector("#financeModalTitle").textContent = "新增项目财务";
+ document.querySelector("#plan_pf-id-input").value = "";
+ document.querySelector("#planModalTitle").textContent = "新增项目财务";
closePlanModal();
await load();
} catch (error) {
@@ -946,7 +947,7 @@ window.createPlanFinance = async (event) => {
};
window.deletePlanItem = async () => {
- const pfId = document.querySelector("#pf-id-input").value;
+ const pfId = document.querySelector("#plan_pf-id-input").value;
if (!pfId) return;
const pf = (state.data.planFinances || []).find(x => x.id === parseInt(pfId));
const name = pf ? (pf.customer_name || "此项目") : "此项目";
diff --git a/static/styles.css b/static/styles.css
index 4f65346..2e44b98 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -96,26 +96,26 @@ body {
}
/* 详情弹窗固定高度,tab 切换不抖动 */
-#financeModal > div {
+#financeModal > div, #planModal > div {
height: 650px;
display: flex;
flex-direction: column;
}
-#financeModal .finance-tabs { flex-shrink: 0; }
-#financeModal form {
+#financeModal .finance-tabs, #planModal .finance-tabs { flex-shrink: 0; }
+#financeModal form, #planModal form {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
-#financeModal .finance-tab-body {
+#financeModal .finance-tab-body, #planModal .finance-tab-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 32px;
}
-#financeModal .finance-form-actions {
+#financeModal .finance-form-actions, #planModal .finance-form-actions {
flex-shrink: 0;
background: #fff;
padding: 12px 32px 8px;
diff --git a/templates/index.html b/templates/index.html
index 9259e15..901bc9a 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -80,7 +80,7 @@
-
OPC Manager v1.0.0.19
+
OPC Manager v1.0.0.20