// 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 `
${rows}
资料名称 附件 创建日期
`; } // 其他资料:原有表格 + 行点击打开抽屉 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 = `

标准资料

${title}

附件管理

${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 `

${category}

${files.length ? files.map(fileItem).join("") : `

暂无文件

`}
`; } function fileItem(file) { return `

${esc(file.file_name)}

`; } 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(); };