// proposals.js — 业务方案 + 文件管理
// 标准资料库固定 7 项
const STANDARD_PROPOSALS = [
"业务方案-医生版",
"业务方案-药企版",
"服务清单与报价单",
"患者服务清单",
"医生项目清单与劳务报价",
"项目执行 SOP",
"财务结算流程",
];
// 确保标准资料库已初始化(首次进入时创建)
async function ensureStandardProposals() {
const existing = (state.data.proposals || []).filter(p => p.proposal_type === "标准资料");
const missing = STANDARD_PROPOSALS.filter(name => !existing.find(p => p.customer_or_project_name === name));
if (missing.length === 0) return;
for (const name of missing) {
try {
await api("/api/proposals", {
method: "POST",
body: JSON.stringify({ data: {
customer_or_project_name: name,
proposal_type: "标准资料",
notes: "",
version: "v1.0",
status: "已归档",
created_date: new Date().toISOString().slice(0, 10),
tenant: state.tenant,
}}),
});
} catch (e) { /* ignore */ }
}
await load();
}
window.switchProposalTab = (tab) => {
state.proposalTab = tab;
renderProposals();
};
function renderProposals() {
const items = state.data.proposals || [];
const standardItems = items.filter(p => p.proposal_type === "标准资料");
const otherItems = items.filter(p => p.proposal_type !== "标准资料");
const isStandard = state.proposalTab === "standard";
document.querySelector("#proposals").innerHTML = `
${!isStandard ? `
` : ''}
${isStandard ? `
这是每一条 OPC 线,必须要梳理清楚的 7 份资料,项目不可以删除,只可以更新附件,请大家将最新的材料上传
` : `
在这里新建,并且上传您希望与团队其他成员共享的资料
`}
${isStandard ? renderStandardTable(standardItems) : renderOtherTable(otherItems)}
`;
if (window.lucide) window.lucide.createIcons();
}
// 标准资料库:按固定顺序排序,点击行打开附件抽屉(不含删除按钮)
function renderStandardTable(items) {
const sorted = STANDARD_PROPOSALS.map(name => items.find(p => p.customer_or_project_name === name)).filter(Boolean);
const rows = sorted.map((p) => {
const fileCount = (p.files || []).length;
return `
| ${esc(p.customer_or_project_name)} |
${fileCount} 个文件 |
${(p.created_at || "").slice(0,10) || "—"} |
`;
}).join("");
return ``;
}
// 其他资料:原有表格 + 行点击打开抽屉
function renderOtherTable(items) {
const rows = items.map((p) => [
`${esc(p.customer_or_project_name)}`,
p.proposal_type || "业务方案",
text(p.notes || ""),
(p.created_at || "").slice(0, 10) || "\u2014",
]);
return renderTable(["方案名称", "方案类型", "方案说明", "日期"], rows, items.map((p) => ({ resource: "proposals", id: p.id })));
}
// 标准资料专用抽屉(附件管理 + 评论,不能编辑字段、不能删除项目)
window.openStandardProposalDrawer = (id) => {
const item = (state.data.proposals || []).find(p => p.id === id);
if (!item) return;
const drawer = document.querySelector("#drawer");
const title = esc(item.customer_or_project_name);
const followupTarget = "proposal";
drawer.innerHTML = `
附件管理
${fileGroup("proposal", item.id, "", "附件", item.files || [])}
${followupTarget ? `
活动 / 跟进
${(item.followups || []).map((f) => `
${esc(f.follower)} · ${esc(f.follow_up_method)}${esc(f.followed_at)}
${f.next_action ? `
下一步:${text(f.next_action)}
` : ""}
`).join("")}
` : ""}
`;
drawer.classList.add("open");
if (window.lucide) window.lucide.createIcons();
renderUploadTasks();
// 渲染富文本评论内容
drawer.querySelectorAll(".rich-content").forEach((el) => {
const html = el.dataset.html;
if (html) el.innerHTML = decodeURIComponent(html);
});
// 初始化 Squire 编辑器
const squireDiv = drawer.querySelector(".squire-editor");
if (squireDiv && window.Squire) {
const sid = squireDiv.id;
if (window.squireInstances[sid]) window.squireInstances[sid].destroy();
const sq = new Squire(squireDiv, { blockTag: "P" });
window.squireInstances[sid] = sq;
squireDiv.addEventListener("focus", () => squireDiv.classList.add("focused"));
squireDiv.addEventListener("blur", () => {
if (!squireDiv.textContent.trim()) squireDiv.classList.remove("focused");
});
}
};
// 标准资料评论提交(提交后重新打开标准资料抽屉)
window.submitStandardComment = async (event, targetId) => {
event.preventDefault();
const form = event.currentTarget;
const editorDiv = form.querySelector(".squire-editor");
const sq = window.squireInstances[editorDiv.id];
const content = sq ? sq.getHTML().trim() : "";
if (!content || content === "
" || content === "
") return;
const button = form.querySelector(".comment-submit");
button.disabled = true;
button.textContent = "发送中…";
await api(`/api/followups/proposal/${targetId}`, { method: "POST", body: JSON.stringify({ data: { content } }) });
await load();
openStandardProposalDrawer(targetId);
};
window.openProposalModal = () => {
document.querySelector("#proposalModal").classList.remove("hidden");
};
window.closeProposalModal = () => {
document.querySelector("#proposalModal").classList.add("hidden");
};
window.submitProposal = async (event) => {
event.preventDefault();
const form = event.currentTarget;
const data = Object.fromEntries(new FormData(form).entries());
data.tenant = state.tenant;
if (!data.version) data.version = "v1.0";
if (!data.description) data.description = "";
if (!data.status) data.status = "草稿";
if (!data.created_date) data.created_date = new Date().toISOString().slice(0, 10);
try {
const result = await api("/api/proposals", { method: "POST", body: JSON.stringify({ data }) });
if (result.id && data.customer_or_project_name) logActivity("proposal", result.id, "创建了方案「" + data.customer_or_project_name + "」");
form.reset();
closeProposalModal();
await load();
} catch (error) {
toast("保存失败:" + error.message, "error");
}
};
// 文件管理
function fileGroup(module, ownerId, version, category, files) {
return `
${files.length ? files.map(fileItem).join("") : `
暂无文件
`}
`;
}
function fileItem(file) {
return ``;
}
window.deleteFile = async (fileId) => {
if (!confirm("确认删除此文件?")) return;
await api(`/api/files/${fileId}`, { method: "DELETE" });
// 优先在当前打开的抽屉中查找并刷新
const drawer = document.querySelector("#drawer.open");
if (drawer) {
const uploadList = drawer.querySelector("#uploadTaskList");
// 通过 file.id 反查所属 item
for (const listKey of ["proposals", "operations", "sales", "products"]) {
if (!state.data[listKey]) continue;
for (const item of state.data[listKey]) {
if (!item.files) continue;
const idx = item.files.findIndex(f => f.id === fileId);
if (idx !== -1) {
item.files.splice(idx, 1);
// 判断是标准资料还是普通抽屉
if (item.proposal_type === "标准资料") {
openStandardProposalDrawer(item.id);
} else {
openDrawer(listKey, item.id);
}
return;
}
}
}
}
};
window.uploadFile = (event, module, ownerId, version, category) => {
const file = event.target.files[0];
if (!file) return;
const taskId = Date.now();
const task = { id: taskId, name: file.name, progress: 0, xhr: null };
state.uploadTasks.push(task);
renderUploadTasks();
const form = new FormData();
form.append("module", module);
form.append("owner_id", ownerId);
form.append("owner_version", version);
form.append("file_category", category);
form.append("file", file);
const xhr = new XMLHttpRequest();
task.xhr = xhr;
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
task.progress = Math.round((e.loaded / e.total) * 100);
renderUploadTasks();
}
});
xhr.addEventListener("load", () => {
if (xhr.status === 200) {
task.progress = 100;
renderUploadTasks();
const result = JSON.parse(xhr.responseText);
const resourceMap = { proposal: "proposals", operation: "operations", sales: "sales", product: "products" };
const listKey = resourceMap[module];
if (listKey && state.data[listKey]) {
const item = state.data[listKey].find(x => x.id === ownerId);
if (item) {
if (!item.files) item.files = [];
item.files.push({ id: result.id, file_name: file.name, file_category: category });
// 刷新当前抽屉
if (item.proposal_type === "标准资料") {
openStandardProposalDrawer(item.id);
} else if (document.querySelector("#drawer.open")) {
openDrawer(listKey, item.id);
}
}
}
setTimeout(() => {
state.uploadTasks = state.uploadTasks.filter(t => t.id !== taskId);
renderUploadTasks();
}, 1500);
}
});
xhr.addEventListener("error", () => {
toast("上传失败:" + file.name, "error");
state.uploadTasks = state.uploadTasks.filter(t => t.id !== taskId);
renderUploadTasks();
});
xhr.open("POST", "/api/files/upload");
xhr.send(form);
};
window.cancelUpload = (taskId) => {
const task = state.uploadTasks.find(t => t.id === taskId);
if (task && task.xhr) task.xhr.abort();
state.uploadTasks = state.uploadTasks.filter(t => t.id !== taskId);
renderUploadTasks();
};
window.renderUploadTasks = () => {
const el = document.querySelector("#uploadTaskList");
if (!el) return;
el.innerHTML = state.uploadTasks.map(t => `
${esc(t.name)}
${t.progress}%
`).join("");
if (window.lucide) window.lucide.createIcons();
};