// utils.js — 全局工具函数与共享状态
const state = {
active: "home",
data: null,
tenant: "科普·无界",
opFilter: "all",
finFilter: "已签约",
selectedProject: null,
taskQuery: "",
taskView: localStorage.getItem("opc-task-view") || "detail",
finView: "overview",
finSort: null, // { key: 'col', asc: true }
planFilter: "待签约",
planView: "overview",
planSort: null,
proposalTab: "standard",
chart: null,
chart2: null,
chart3: null,
chart4: null,
productPlatform: "all",
uploadTasks: [],
expenseFilter: "全部",
expenseView: "total",
expenseMonth: "",
expenseQuarter: "",
};
const money = (value) => `${Number(value || 0).toLocaleString("zh-CN")} 元`;
const text = (value) => value === undefined || value === null || value === "" ? "—" : esc(value);
function escapeHtml(str) { return String(str || "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); }
const html = escapeHtml;
const esc = escapeHtml;
// Toast 通知
function toast(message, type = "info", duration = 3000) {
let container = document.querySelector(".toast-container");
if (!container) {
container = document.createElement("div");
container.className = "toast-container";
document.body.appendChild(container);
}
const el = document.createElement("div");
el.className = `toast toast-${type}`;
const icon = type === "success" ? "check-circle" : type === "error" ? "alert-circle" : "info";
el.innerHTML = `${esc(message)}`;
container.appendChild(el);
if (window.lucide) window.lucide.createIcons();
setTimeout(() => {
el.classList.add("fade-out");
setTimeout(() => el.remove(), 250);
}, duration);
}
window.toast = toast;
function monthOptions(selected = '') {
const now = new Date();
const startYear = now.getFullYear() - 1;
const endYear = now.getFullYear() + 1;
let options = selected ? '' : '';
for (let y = startYear; y <= endYear; y++) {
for (const m of ["01","02","03","04","05","06","07","08","09","10","11","12"]) {
const val = y + "-" + m;
const sel = val === selected ? " selected" : "";
options += ``;
}
}
return options;
}
async function api(path, options = {}) {
const response = await fetch(path, {
headers: options.body instanceof FormData ? undefined : { "Content-Type": "application/json" },
...options,
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "请求失败");
return data;
}
async function logActivity(targetType, targetId, content) {
try {
await api(`/api/followups/${targetType}/${targetId}`, {
method: "POST",
body: JSON.stringify({ data: { content, tenant: state.tenant } }),
});
} catch (e) { /* non-critical */ }
}
function badge(value) {
const val = String(value || "—");
let cls = "badge-slate";
if (["P0", "有风险", "已丢单", "已延期"].includes(val)) cls = "badge-red";
if (["P1", "方案中", "方案已提交", "商务谈判", "待客户确认"].includes(val)) cls = "badge-amber";
if (["已签约", "已上线", "已完成", "已归档"].includes(val)) cls = "badge-green";
if (["execution", "已签约执行项目"].includes(val)) cls = "badge-blue";
return `${val === "execution" ? "已签约执行项目" : val === "opportunity" ? "业务机会项目" : val}`;
}
function card(content, cls = "") {
return ``;
}
function renderTable(headers, rows, rowClicks) {
const trAttrs = (rowClicks || []).map((rc) => rc ? `onclick="openDrawer('${rc.resource}', ${rc.id})" class="clickable-row"` : "");
return card(`
${headers.map((h) => `| ${h} | `).join("")}
${rows.map((row, i) => `${row.map((c) => `| ${c} | `).join("")}
`).join("")}
`);
}
async function load() {
state.data = await api(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`);
// 首次加载时确保标准资料库 7 项已初始化
if (typeof ensureStandardProposals === "function") {
const existing = (state.data.proposals || []).filter(p => p.proposal_type === "标准资料");
const missingCount = 7 - existing.length;
if (missingCount > 0) {
await ensureStandardProposals();
return; // ensureStandardProposals 内部会再次 render
}
}
render();
}
function switchTab(tab) {
state.active = tab;
localStorage.setItem("opc-active-tab", tab);
document.querySelectorAll(".sidebar-tab").forEach((btn) => btn.classList.toggle("active", btn.dataset.tab === tab));
document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === tab));
// 更新顶部标题
const titles = { home: 'OPC 工作台', finance: '实际管理', plan: '计划管理', projects: '重点工作台账', proposals: '业务方案', products: '版本与运营', performance: '季度绩效考核' };
const titleEl = document.querySelector('#workspaceTitle');
if (titleEl) titleEl.textContent = titles[tab] || state.tenant + ' OPC 工作台';
render();
}
function updateSidebarTabs() {
const isOverview = state.tenant === "总工作台";
document.querySelectorAll(".sidebar-tab").forEach((btn) => {
const tab = btn.dataset.tab;
if (isOverview && tab !== "home") {
btn.style.display = "none";
} else {
btn.style.display = "";
}
});
}
function render() {
if (!state.data) return;
renderHome();
renderProjects();
renderProposals();
renderProducts();
renderPerformance();
renderFinance();
renderPlan();
if (window.lucide) window.lucide.createIcons();
}
function renderActive() {
if (!state.data) return;
const tab = state.active;
if (tab === "home") renderHome();
else if (tab === "projects") renderProjects();
else if (tab === "proposals") renderProposals();
else if (tab === "products") renderProducts();
else if (tab === "performance") renderPerformance();
else if (tab === "finance") renderFinance();
else if (tab === "plan") renderPlan();
if (window.lucide) window.lucide.createIcons();
}
window.setTaskView = (view) => {
state.taskView = view;
localStorage.setItem("opc-task-view", view);
// 更新按钮选中状态
const toggle = document.querySelector("#taskViewToggle");
if (toggle) {
toggle.querySelectorAll("button").forEach((btn, i) => {
const isCompact = i === 0;
btn.className = `btn btn-sm ${(isCompact ? view === 'compact' : view !== 'compact') ? 'btn-primary' : 'btn-ghost'} p-1.5`;
});
}
if (state.selectedProject) {
const body = document.querySelector(".task-feed-body");
if (body) body.innerHTML = renderTaskListHTML(state.selectedProject);
if (window.lucide) window.lucide.createIcons();
}
};
window.switchTab = switchTab;
window.setFinView = (view) => {
state.finView = view;
renderFinance();
};
window.switchFinFilter = (filter) => {
state.finFilter = filter;
state.finSort = null;
renderFinance();
};
window.setFinSort = (key) => {
if (!state.finSort) state.finSort = { key: null, asc: true };
if (state.finSort.key === key) { state.finSort.asc = !state.finSort.asc; }
else { state.finSort.key = key; state.finSort.asc = true; }
renderFinance();
};
window.setPlanView = (view) => {
state.planView = view;
renderPlan();
};
window.switchPlanFilter = (filter) => {
state.planFilter = filter;
state.planSort = null;
renderPlan();
};
window.setPlanSort = (key) => {
if (!state.planSort) state.planSort = { key: null, asc: true };
if (state.planSort.key === key) { state.planSort.asc = !state.planSort.asc; }
else { state.planSort.key = key; state.planSort.asc = true; }
renderPlan();
};
window.switchTenant = (tenant) => {
state.tenant = tenant;
state.selectedProject = null;
localStorage.setItem("opc-active-tenant", tenant);
document.querySelector("#workspaceTitle").textContent = tenant.replace("·无界", "") + " OPC 工作台";
const label = document.querySelector("#currentTenantLabel");
if (label) { label.textContent = tenant.replace("·无界", "") || "工作台"; label.title = tenant; }
updateSidebarTabs();
if (tenant === "总工作台") switchTab("home");
load();
};
window.doLogout = async () => {
await api("/api/auth/logout", { method: "POST" });
location.href = "/login";
};