/* views/pool.js —— 机会卡片池(组织资产)。
*
* 这一页回答:"研发中心现在手上有哪些机会、值不值得做、谁在什么时候用什么方式送上来的。"
* 卡片形态(不是列表行);可筛(优先级/阶段/品类/状态/上传人)、可排(优先级/评分/最新)、
* 可点评分(多人平均)、可改状态(候选/立项/否决,落库后刷新保持)。
* 筛选/排序/打开的卡都写进 URL;评分/状态落库,不做仅本地的假操作。
*/
(function () {
"use strict";
const T = window.KF_TAXONOMY;
const U = window.KF_UTIL;
const UI = window.KF_UI;
const OPP = window.KF_OPP;
const esc = U.esc;
const STATUS_FILTER = ["candidate", "approved", "rejected"];
/* 数组.contains 有坑:CSS 里 .o-cat/.o-stage 是类名,这里绝不能把数组当字符串用。
* 统一用 Set,筛选是精确命中,不做子串匹配。 */
const has = (arr, v) => new Set(arr).has(v);
function matches(o, p) {
if (p.priority && p.priority.length && !has(p.priority, o.priority)) return false;
if (p.stage && p.stage.length && !(o.stageObj && has(p.stage, o.stageObj.code))) return false;
if (p.category && p.category.length && !has(p.category, o.category)) return false;
if (p.status && p.status.length && !has(p.status, o.status)) return false;
if (p.member && p.member.length && !has(p.member, o.upload.by)) return false;
return true;
}
const list = (v) => (v ? String(v).split(",").filter(Boolean) : []);
/* 概览条:池子总量 + 三个状态 + 平均分(给判断,不只给数字) */
function summary(all) {
const n = (k) => all.filter((o) => o.status === k).length;
const scored = all.filter((o) => o.stats.n);
const avg = scored.length ? Math.round(scored.reduce((s, o) => s + o.stats.avg, 0) / scored.length * 10) / 10 : null;
const top = OPP.sortCards(all, "score")[0];
return `
池均综合分
${avg == null ? "—" : avg}
最高分机会
${top ? esc(top.title) : "—"}
进池的人
${new Set(all.map((o) => o.upload.by).filter(Boolean)).size}人
`;
}
function facet(label, dim, values, active, counts, multi) {
if (!values.length) return "";
const chips = values.map((v) => {
const on = active.includes(v.value);
const zero = !counts.get(v.value) && !on;
return ``;
}).join("");
return `${esc(label)}${chips}
`;
}
function filterBar(all, c, n) {
const rest = (dim) => all.filter((o) => matches(o, { ...c, [dim]: [] }));
const count = (rows, fn) => U.countBy(rows, fn);
const pc = count(rest("priority"), (o) => o.priority);
const sc = count(rest("stage"), (o) => (o.stageObj ? o.stageObj.code : ""));
const cc = count(rest("category"), (o) => o.category);
const stc = count(rest("status"), (o) => o.status);
const mc = count(rest("member"), (o) => o.upload.by);
const cats = [...new Set(all.map((o) => o.category).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh"));
const uploaders = [...mc.entries()].sort((a, b) => b[1] - a[1]).map(([v, k]) => ({ value: v, label: v }));
const any = ["priority", "stage", "category", "status", "member"].some((k) => c[k].length);
return `
${facet("优先级", "priority", T.PRIORITIES.map((v) => ({ value: v, label: v })), c.priority, pc)}
${facet("阶段", "stage", T.STAGES.map((s) => ({ value: s.code, label: s.code })), c.stage, sc)}
${facet("品类", "category", cats.map((v) => ({ value: v, label: v })), c.category, cc)}
${facet("状态", "status", T.OPP_STATUS.map((s) => ({ value: s.key, label: s.label })), c.status, stc)}
${facet("上传人", "member", uploaders, c.member, mc, true)}
符合当前条件 ${n} 张卡
${any ? `
` : ""}
`;
}
function render(db, params) {
const all = db.opportunities;
const c = {
priority: list(params.priority), stage: list(params.stage), category: list(params.category),
status: list(params.status).filter((s) => STATUS_FILTER.includes(s)), member: list(params.member)
};
const sortKey = OPP.SORTS[params.sort] ? params.sort : "prio";
const rows = OPP.sortCards(all.filter((o) => matches(o, c)), sortKey);
const head = `
机会卡片池
全员机会卡汇总入库:每张卡带评分、优先级与决策记录,构成研发中心的机会储备。卡片上的每一步都能追到「谁 · 哪个 Agent · 什么方式 · 什么时候」上传。
${OPP.sortSeg(sortKey, params.sort)}
`;
const body = !all.length
? UI.empty({
title: "池子里还没有机会卡",
body: "学员在 S3 机会识别阶段用主分身上传机会卡后,会自动进池。这里会按卡片形态排开,带评分、状态和决策记录。",
actions: [{ label: "看阶段板块", href: "#/stages" }]
})
: !rows.length
? UI.empty({ title: "当前条件下没有卡", body: `池中 ${all.length} 张卡,没有同时满足全部条件的。单独去掉某一项后会有卡出现。`, actions: [{ label: "清除全部筛选", act: "opp-clear" }] })
: `${rows.map((o) => OPP.card(o)).join("")}
`;
return `${head}
${summary(all)}
${filterBar(all, c, rows.length)}
${body}
`;
}
window.KF_VIEW_POOL = { render, matches };
})();