From 07e1b0ae7b9f204a9b869cf8248aa806285b4daa Mon Sep 17 00:00:00 2001 From: chezhiqi Date: Sun, 20 Sep 2026 22:08:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9A=E5=BC=80=E9=A1=B9=E7=9B=AE=EF=BC=9A?= =?UTF-8?q?=E6=94=B9=E5=90=8D=EF=BC=88=E5=8D=8E=E6=99=BA=E4=B8=8B=E5=8D=95?= =?UTF-8?q?/=E8=BF=9B=E8=A1=8C=E4=B8=AD=EF=BC=89+=20=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E4=B8=8E=E7=AD=9B=E9=80=89=20+=20=E5=88=97?= =?UTF-8?q?=E5=BA=8F/=E6=8C=89=E9=92=AE=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 项目归属「新签订单」→「华智下单」(含数据迁移 83 条,导入脚本自动映射) 项目状态「交付中」→「进行中」(含数据迁移 10 条,导入脚本自动映射);项目归属去掉「其他」选项 本周进展卡片:本周新增项目→新增项目、交付项目→进行中项目 清空早于 2026-09-18 的「更新时间」(111 条),后续编辑按提交时间重新写入 表格:备注与更新时间列对调;下单时间/订单金额/更新时间 表头支持升/降序 去掉「删除本周更新」按钮(后端接口保留) 新增「推动验收计划」筛选,导出同步(export_custom 新增 accept_plan) --- .workbuddy/memory/2026-09-20.md | 34 ++++++++ app/models.py | 4 +- app/routers/export.py | 3 + data/board.db | Bin 385024 -> 385024 bytes scripts/import_custom_ledger.py | 9 ++- web/app.js | 134 ++++++++++++++------------------ 6 files changed, 106 insertions(+), 78 deletions(-) diff --git a/.workbuddy/memory/2026-09-20.md b/.workbuddy/memory/2026-09-20.md index 3a96d67..a3a50e4 100644 --- a/.workbuddy/memory/2026-09-20.md +++ b/.workbuddy/memory/2026-09-20.md @@ -60,3 +60,37 @@ - 定开第一个 tab **「项目明细」→「项目总览」**(chip 与卡片标题「定开项目总览」都改)。 - `customTableHtml` 列序:把**「项目归属」移到「下单时间」之后**(新序:项目名称 / 项目ID / 下单时间 / 项目归属 / 办事处 / …)。项目总览与本周进展两个 tab 共用,故同时生效。 - 后端导出标题仍是「定开项目明细」(未改,用户只说 tab 名)。纯前端改动,无需重启。 + +## 推送到 develop-chezhiqi +- 仓库此前**未配置 git 身份**(唯一提交 e9854eb 作者是 mula.liu )。按用户选择,设**本仓库** `user.name=chezhiqi` / `user.email=chezhiqi@unissense.tech`(邮箱取远端域名 unissense.tech,如不对可 amend)。 +- 提交 **a77cd7b**「定开项目:台账化改造 + 统计/导出/搜索调整,新增本周进展与年度任务 tab」,包含 app/**、web/**、scripts/**(新增 `import_custom_ledger.py`)、README.md、run.sh、`data/board.db`、`.workbuddy/memory/**`(含被 git 跟踪的 `__pycache__/*.pyc`)。 +- 已 push 到 `origin/develop-chezhiqi`(`e9854eb..a77cd7b`),本地与远端一致(远端还提示可发 PR main...develop-chezhiqi)。 +- **有意未提交**:`data/board.db.bak-*` 4 个本地备份(保持 untracked)。 + +## 「项目归属」新签订单 → 华智下单 +- 前端 `web/app.js`:`CUSTOM_ORIGINS` / `CUSTOM_PLACED` 里 `新签订单` → **`华智下单`**(连同新建对话框默认值、统计说明文案,全文替换 6 处,现 app.js 内已无「新签订单」)。 +- **数据迁移**:`UPDATE custom_projects SET origin='华智下单' WHERE origin='新签订单'`(83 行);备份 `data/board.db.bak-before-origin-rename-20260920_213304`。现 origin 分布:华智下单 83 / 存量项目 27 / 汇智直签 10 / 提前交付 2。 +- `scripts/import_custom_ledger.py`:读入行时把 `新签订单` 归一化为 `华智下单`(防再导入回退),并更新文档注释;`app/models.py` 注释同步。 +- 纯前端 + 数据 + 脚本改动,**无需重启**(models.py 仅注释)。**本次未 push**(用户本轮只要求改名)。 + +## 三项改名/调整(项目状态 / 本周卡片 / 归属选项) +1. **项目状态「交付中」→「进行中」**(全量改名): + - `web/app.js` 全文 `交付中`→`进行中`(共 12 处:状态色映射、STATUS_OPTIONS、进度列判定、本周「交付项目」= 进行中+交付完成、新建/编辑 statusOpts、placeholder、注释)。 + - **数据迁移** `UPDATE custom_projects SET project_status='进行中' WHERE project_status='交付中'`(10 行);备份 `data/board.db.bak-before-status-rename-20260920_213707`。现状态:已验收 64 / 待开发 29 / 交付完成 17 / **进行中 10** / 退单 2。 + - `import_custom_ledger.py` 读入时 `交付中`→`进行中` 归一化;`models.py` 注释同步。 +2. **本周进展卡片「本周新增项目」→「新增项目」**(`web/app.js` 卡片标题 + 说明文案)。 +3. **项目归属去掉「其他」选项**:`CUSTOM_ORIGINS = [华智下单, 存量项目, 汇智直签, 提前交付]`(新建/筛选下拉都不再有「其他」);`models.py` / 导入脚本文档注释同步。数据中本来就没有 origin=其他 的行。 +- 纯前端 + 数据 + 脚本改动,**无需重启**;`node --check` / `ast.parse` 通过。**本轮未 push**。 + +## 本周卡片改名 + 清空历史「更新时间」 +1. **本周进展卡片「交付项目」→「进行中项目」**(`web/app.js` 卡片标题/说明/标题栏摘要;口径未变,仍是 本周更新 ∩ 进行中+交付完成)。 +2. **清空早于 2026-09-18 的「更新时间」**:`UPDATE custom_projects SET update_date=NULL WHERE update_date IS NOT NULL AND update_date!='' AND update_date < '2026-09-18'` → 清空 **111** 行;保留 11 行(均为 2026-09-18 及以后编辑的,含 "YYYY-MM-DD HH:MM" 提交时间)。备份 `data/board.db.bak-before-clearing-updatedate-20260920_214254`。 + - 影响:`update_date` 仅剩 11 行有值;「删除本周更新」现在只会命中这 11 行(都在本周)。后续「修改/新建」会按提交时间重新写入。 + +## 表格排序 + 计划筛选 + 列序/按钮调整 +1. **列序**:`customTableHtml` 里把「备注」与「更新时间」**对调**(… 风险值 / **更新时间** / **备注** / 操作)。 +2. **去掉「删除本周更新」按钮**:删除 `renderCustomList` 卡片头里的按钮、`bindCustomList` 里的绑定与整个 `purgeWeek()` 函数(后端 `/custom/weekly-preview`、`/custom/purge-week` 接口保留未删)。 +3. **表头排序**:`下单时间`(sign_date) / `订单金额`(amount, 数值) / `更新时间`(update_date) 三个表头可点击排序,`state.customSort = {key, dir}`;点不同列默认「降序」,再点同列切换升/降;表头显示 ▲/▼,卡片副标题显示当前排序。 +4. **新增「推动验收计划」筛选**:工具栏加 `#cAcceptPlan`(`state.customAcceptPlan`),选项取数据中实际出现过的值(Q3/Q4/27年/-- 等);导出也带 `accept_plan` 参数(后端 `export_custom` 新增 `accept_plan`)。 +- `state` 新增 `customAcceptPlan` / `customSort`,均加入 `UI_FIELDS` 持久化。 +- 后端 `export.py` 改动 → 重启 uvicorn(PID 23436);`node --check` / `ast.parse` 通过。实测导出 `accept_plan=Q3` 生效。**本轮未 push**。 diff --git a/app/models.py b/app/models.py index cb59a27..ac02c35 100644 --- a/app/models.py +++ b/app/models.py @@ -414,7 +414,7 @@ class CustomProject(Base, TimestampMixin): source: Mapped[str] = mapped_column(String(16), default="import", nullable=False) # ---- 市场/销售侧台账字段(来源《定开项目.xlsx》,2026-09 起)---- - origin: Mapped[str | None] = mapped_column(String(32), nullable=True) # 项目归属:新签订单/存量项目/汇智直签 + origin: Mapped[str | None] = mapped_column(String(32), nullable=True) # 项目归属:华智下单/存量项目/汇智直签/提前交付 project_code: Mapped[str | None] = mapped_column( String(64), nullable=True, index=True ) # 项目ID(市场编号,导入时用于去重) @@ -426,7 +426,7 @@ class CustomProject(Base, TimestampMixin): ) # 市场责任人(销售侧;多人用 / 分隔,与花名册无关) project_status: Mapped[str | None] = mapped_column( String(32), nullable=True - ) # 项目状态:已验收/待开发/交付完成/交付中/退单 + ) # 项目状态:已验收/待开发/交付完成/进行中/退单 accept_plan: Mapped[str | None] = mapped_column( String(32), nullable=True ) # 推动验收计划(Q1..Q4 / 27年) diff --git a/app/routers/export.py b/app/routers/export.py index 6912f58..19c7c62 100644 --- a/app/routers/export.py +++ b/app/routers/export.py @@ -48,6 +48,7 @@ def export_custom( region: str | None = None, origin: str | None = None, status: str | None = None, + accept_plan: str | None = None, keyword: str | None = None, archived: str = Query("false", pattern="^(true|false|all)$"), db: Session = Depends(get_db), @@ -63,6 +64,8 @@ def export_custom( q = q.filter(CustomProject.origin == origin) if status: q = q.filter(CustomProject.project_status == status) + if accept_plan: + q = q.filter(CustomProject.accept_plan == accept_plan) if keyword: q = q.filter(CustomProject.name.contains(keyword)) projects = _sort_by_update(q.all()) diff --git a/data/board.db b/data/board.db index 04b27610467ceefc4ada5294ac12e59924743ebc..c99e0ec4aab6c5fb2ab62880eb78ce09f3400fa0 100644 GIT binary patch delta 5669 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 QwO>^B9;Xx6_0o|42R+O&1poj5 delta 5950 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 diff --git a/scripts/import_custom_ledger.py b/scripts/import_custom_ledger.py index 17c6817..25506e2 100644 --- a/scripts/import_custom_ledger.py +++ b/scripts/import_custom_ledger.py @@ -4,7 +4,7 @@ | 表格列 | 数据库列 | 说明 | |--------------|-----------------|------| -| 项目归属 | origin | 新签订单 / 存量项目 / 汇智直签 | +| 项目归属 | origin | 华智下单 / 存量项目 / 汇智直签 / 提前交付(「新签订单」自动映射为「华智下单」) | | 项目名称 | name | 匹配键之一 | | 项目ID | project_code | 匹配键(优先) | | 下单时间 | sign_date | 日期取 YYYY-MM-DD,像「2024年3月前」这种文本原样保留 | @@ -13,7 +13,7 @@ | 市场下单人天 | man_days | | | 订单金额 | amount | | | 市场责任人 | market_owner | 销售侧人员,与花名册无关,可「张宁/叶镇」这种多人写法 | -| 项目状态 | project_status | 已验收 / 待开发 / 交付完成 / 交付中 / 退单 | +| 项目状态 | project_status | 已验收 / 待开发 / 交付完成 / 进行中 / 退单(「交付中」自动映射为「进行中」) | | 验收时间 | accept_date | 存的是季度文本(Q1..Q4),不是日期 | | 推动验收计划 | accept_plan | Q1..Q4 / 27年 | | 风险值 | risk_level | 1.高风险 / 2.低风险 | @@ -137,6 +137,11 @@ def read_rows(xlsx: str) -> list[dict]: continue row["region"] = normalize_region(row.get("office")) + # 历史取值改名(2026-09-20):项目归属 新签订单 → 华智下单;项目状态 交付中 → 进行中 + if row.get("origin") == "新签订单": + row["origin"] = "华智下单" + if row.get("project_status") == "交付中": + row["project_status"] = "进行中" # 验收日期(真实日期,区别于 accept_date 季度文本):仅对「已验收」且验收时间为季度文本的项目, # 补一个当年季度末占位,使「本年/本月验收」统计有数可算。用户后续在界面上改了真日期不会被覆盖。 row["accept_date_real"] = None diff --git a/web/app.js b/web/app.js index bb63398..61483d5 100644 --- a/web/app.js +++ b/web/app.js @@ -31,6 +31,8 @@ customStage: "", customStatus: "", customOrigin: "", + customAcceptPlan: "", + customSort: null, // { key: amount | sign_date | update_date, dir: asc | desc } customArchived: "false", customRows: [], intakeType: "general", @@ -1472,12 +1474,12 @@ const CUSTOM_STATUS_COLOR = { 已验收: "#16a34a", 交付完成: "#0891b2", - 交付中: "#2563eb", + 进行中: "#2563eb", 待开发: "#b45309", 退单: "#7a8394", }; - /** 项目状态:已验收/交付完成/交付中/待开发/退单 */ + /** 项目状态:已验收/交付完成/进行中/待开发/退单 */ function statusPill(code) { if (!code) return `—`; const color = CUSTOM_STATUS_COLOR[code] || "#7a8394"; @@ -1496,8 +1498,8 @@ let keywordFocus = false; /** 定开「项目归属」取值:前三项视为「已下单」,提前交付为下单前状态 */ - const CUSTOM_ORIGINS = ["新签订单", "存量项目", "汇智直签", "提前交付", "其他"]; - const CUSTOM_PLACED = ["新签订单", "存量项目", "汇智直签"]; + const CUSTOM_ORIGINS = ["华智下单", "存量项目", "汇智直签", "提前交付"]; + const CUSTOM_PLACED = ["华智下单", "存量项目", "汇智直签"]; const isPlaced = (p) => CUSTOM_PLACED.indexOf(p.origin) >= 0; /** 年度计收目标(万元),来源《年度任务.xlsx》(Q1 35 / Q2 225 / Q3 160 / Q4 260,合计 680) */ const CUSTOM_ANNUAL_TARGETS = { 1: 35, 2: 225, 3: 160, 4: 260 }; @@ -1532,11 +1534,15 @@ ``; } - const STATUS_OPTIONS = ["已验收", "待开发", "交付完成", "交付中", "退单"]; + const STATUS_OPTIONS = ["已验收", "待开发", "交付完成", "进行中", "退单"]; const statusFilterHtml = `` + STATUS_OPTIONS.map((s) => ``).join(""); const originFilterHtml = `` + CUSTOM_ORIGINS.map((o) => ``).join(""); + // 推动验收计划:取数据里实际出现过的值 + const planVals = Array.from(new Set(allRows.map((p) => String(p.accept_plan || "").trim()).filter(Boolean))).sort(); + const planFilterHtml = `` + + planVals.map((v) => ``).join(""); const isList = state.customTab !== "weekly" && state.customTab !== "annual"; const toolbar = ` @@ -1550,6 +1556,7 @@ + 全部 ${allRows.length} 个项目` @@ -1572,6 +1579,8 @@ if (rs) rs.addEventListener("change", () => { state.customRegion = rs.value; renderCustom(); }); const st = $("#cStatus"); if (st) st.addEventListener("change", () => { state.customStatus = st.value; renderCustom(); }); + const ap = $("#cAcceptPlan"); + if (ap) ap.addEventListener("change", () => { state.customAcceptPlan = ap.value; renderCustom(); }); if (isList) bindKeyword("cKeyword", { enter: true }); const bn = $("#btnNewCustom"); if (bn) bn.addEventListener("click", () => openNewCustomDialog(() => afterWrite())); @@ -1582,6 +1591,7 @@ if (state.customOrigin) q.push("origin=" + encodeURIComponent(state.customOrigin)); if (state.customRegion) q.push("region=" + encodeURIComponent(state.customRegion)); if (state.customStatus) q.push("status=" + encodeURIComponent(state.customStatus)); + if (state.customAcceptPlan) q.push("accept_plan=" + encodeURIComponent(state.customAcceptPlan)); if (state.keyword) q.push("keyword=" + encodeURIComponent(state.keyword)); download("/api/export/custom?" + q.join("&")); }); @@ -1589,19 +1599,24 @@ } function renderCustomList(allRows) { - // 列表:按 关键字 + 归属 + 区域 + 状态 在本地筛选;并按「更新时间」倒序 + // 列表:按 关键字 + 归属 + 区域 + 状态 + 推动验收计划 在本地筛选;再按表头排序 const kw = (state.keyword || "").trim(); let list = allRows; if (kw) list = list.filter((p) => String(p.name || "").includes(kw)); if (state.customOrigin) list = list.filter((p) => (p.origin || "") === state.customOrigin); if (state.customRegion) list = list.filter((p) => (p.region || "") === state.customRegion); if (state.customStatus) list = list.filter((p) => (p.project_status || "") === state.customStatus); - const view = list.slice().sort((a, b) => - String(b.update_date || "").localeCompare(String(a.update_date || "")) - ); + if (state.customAcceptPlan) list = list.filter((p) => String(p.accept_plan || "").trim() === state.customAcceptPlan); + const s = state.customSort || { key: "update_date", dir: "desc" }; + const sv = (p, k) => (k === "amount" ? Number(p.amount || 0) : String(p[k] || "")); + const view = list.slice().sort((a, b) => { + const va = sv(a, s.key), vb = sv(b, s.key); + const c = va < vb ? -1 : va > vb ? 1 : 0; + return s.dir === "asc" ? c : -c; + }); // ---- 统计条:基于「全部已下单项目」,不随搜索 / 筛选变化 ---- - const placed = allRows.filter(isPlaced); // 已下单 = 新签订单 / 存量项目 / 汇智直签 + const placed = allRows.filter(isPlaced); // 已下单 = 华智下单 / 存量项目 / 汇智直签 const today = new Date(); const Y = today.getFullYear(); const M = today.getMonth() + 1; @@ -1634,47 +1649,48 @@ ${stat(`本季度验收(${Y}年Q${Q})`, accQuarter.length, sum(accQuarter, "amount"), "var(--amber)")} ${stat(`本年验收项目(${Y}年)`, accYear.length, sum(accYear, "amount"), "var(--amber)")} -
「已下单项目 / 合同额」与「本季度·本年下单」只统计 新签订单 / 存量项目 / 汇智直签(提前交付不计入下单);统计基于全部项目,不受搜索 / 筛选影响。
`; +
「已下单项目 / 合同额」与「本季度·本年下单」只统计 华智下单 / 存量项目 / 汇智直签(提前交付不计入下单);统计基于全部项目,不受搜索 / 筛选影响。
`; const manDays = view.reduce((a, p) => a + Number(p.man_days || 0), 0); const highRisk = view.filter((p) => String(p.risk_level || "").includes("高")).length; const accepted = view.filter((p) => p.project_status === "已验收").length; + const sortLabel = { sign_date: "下单时间", amount: "订单金额", update_date: "更新时间" }[s.key] || "更新时间"; return `
-

定开项目总览按「更新时间」倒序 · 共 ${view.length} 个${state.customStatus ? "(" + esc(state.customStatus) + ")" : ""} · 列表只读,改数据请点「修改」

+

定开项目总览按「${sortLabel}」${s.dir === "asc" ? "升序" : "降序"} · 共 ${view.length} 个${state.customStatus ? "(" + esc(state.customStatus) + ")" : ""} · 点表头(下单时间 / 订单金额 / 更新时间)可排序;列表只读,改数据请点「修改」

人天 ${manDays.toLocaleString()} · 高风险 ${highRisk} 个 · 已验收 ${accepted} 个 -
- -
${statBar} ${view.length ? customTableHtml(view) : `
${state.keyword ? "没有匹配「" + esc(state.keyword) + "」的定开项目" : "当前筛选条件下没有定开项目"}
`}
`; } - /** 定开项目表格(「项目明细」与「本周进展」两个 tab 共用) */ + /** 定开项目表格(「项目总览」与「本周进展」两个 tab 共用);下单时间 / 订单金额 / 更新时间 表头可排序 */ function customTableHtml(view, withOps) { const ops = withOps !== false; + const s = state.customSort || { key: "update_date", dir: "desc" }; + const sth = (label, k, cls) => + `${label}${s.key === k ? (s.dir === "asc" ? " ▲" : " ▼") : ""}`; return `
- + ${sth("下单时间", "sign_date")} - + ${sth("订单金额", "amount", "num")} + ${sth("更新时间", "update_date")} - ${ops ? `` : ""} @@ -1696,12 +1712,12 @@ - + - + ${ops ? `` : ""} `).join("")} @@ -1723,25 +1739,25 @@ const sum = (arr, f) => arr.reduce((a, p) => a + Number(p[f] || 0), 0); const acc = week.filter((p) => p.project_status === "已验收"); - // 本周新增项目:按「下单时间」在本周的新下单 / 提前交付项目(不是「本周更新」) + // 新增项目:按「下单时间」在本周的新下单 / 提前进行中项目(不是「本周更新」) const newDeal = allRows.filter((p) => inWeek(p.sign_date) && (isPlaced(p) || p.origin === "提前交付")); - // 交付项目:本周更新的「交付中 + 交付完成」 - const doing = week.filter((p) => p.project_status === "交付中" || p.project_status === "交付完成"); + // 进行中项目:本周更新的「进行中 + 交付完成」 + const doing = week.filter((p) => p.project_status === "进行中" || p.project_status === "交付完成"); const stat = (label, rows, color) => `
${rows.length}个 · ${fen(sum(rows, "amount"))}万
${label}
`; const statBar = `
${stat("验收情况", acc, "var(--amber)")} - ${stat("本周新增项目", newDeal, "var(--violet)")} - ${stat("交付项目", doing, "var(--primary)")} + ${stat("新增项目", newDeal, "var(--violet)")} + ${stat("进行中项目", doing, "var(--primary)")}
-
本周 = ${start} ~ ${end};本周新增项目 = 按「下单时间」在本周的新下单 / 提前交付项目(已下单 + 提前交付);验收情况 / 交付项目 为「更新时间」在本周的项目(交付项目 = 交付中 + 交付完成)。
`; +
本周 = ${start} ~ ${end};新增项目 = 按「下单时间」在本周的新下单 / 提前进行中项目(已下单 + 提前交付);验收情况 / 进行中项目 为「更新时间」在本周的项目(进行中项目 = 进行中 + 交付完成)。
`; return `

本周进展更新时间在本周(${start} ~ ${end})· 共 ${week.length} 个项目 · 列表只读,改数据请点项目名称

- 验收 ${acc.length} · 新增 ${newDeal.length} · 交付 ${doing.length} + 验收 ${acc.length} · 新增 ${newDeal.length} · 进行中 ${doing.length}
${statBar} ${week.length @@ -1808,11 +1824,11 @@
项目名称 项目ID下单时间项目归属 办事处 行业 市场下单人天订单金额市场责任人 项目状态 进度 验收时间 推动验收计划 风险值备注更新时间操作
${money(p.amount)} ${esc(p.market_owner || "—")} ${statusPill(p.project_status)}${(p.project_status === "交付中" || Number(p.progress || 0) > 0) ? Math.round(p.progress || 0) + "%" : "—"}${(p.project_status === "进行中" || Number(p.progress || 0) > 0) ? Math.round(p.progress || 0) + "%" : "—"} ${p.accept_date ? esc(p.accept_date) : "—"} ${esc(p.accept_plan || "—")} ${riskPill(p.risk_level)}${esc((p.remark || "").slice(0, 40)) || "—"} ${esc(p.update_date || "—")}${esc((p.remark || "").slice(0, 40)) || "—"}
-
计收项目 = 按「验收时间」归属季度的项目;新增签单 = 按「下单时间」归属本年度季度、且项目归属为已下单(新签订单 / 存量项目 / 汇智直签)的项目;金额由「元」换算为「万元」;计收完成率 = 计收项目金额 ÷ 计收目标。
+
计收项目 = 按「验收时间」归属季度的项目;新增签单 = 按「下单时间」归属本年度季度、且项目归属为已下单(华智下单 / 存量项目 / 汇智直签)的项目;金额由「元」换算为「万元」;计收完成率 = 计收项目金额 ÷ 计收目标。
`; } - /** 项目明细表:列表只读,行内只有「修改」;名称可点开详情 */ + /** 项目总览表:列表只读,行内只有「修改」;名称可点开详情;表头(下单时间 / 订单金额 / 更新时间)可排序 */ function bindCustomList(host) { host.querySelectorAll("[data-copen]").forEach((a) => a.addEventListener("click", () => openCustomDrawer(Number(a.dataset.copen), "view")) @@ -1820,44 +1836,14 @@ host.querySelectorAll("[data-cedit]").forEach((b) => b.addEventListener("click", () => openCustomDrawer(Number(b.dataset.cedit), "edit")) ); - const bp = $("#btnPurgeWeek"); - if (bp) bp.addEventListener("click", purgeWeek); - } - - /** 删除本周(周一~周日)更新的定开项目:先预览数量,二次确认后执行。 */ - async function purgeWeek() { - let pv; - try { - pv = await api("/custom/weekly-preview"); - } catch (e) { - return toast("读取本周更新失败:" + e.message); - } - if (!pv.count) { - return toast("本周(" + pv.start + " ~ " + pv.end + ")没有更新的定开项目"); - } - const ok = await confirmDialog({ - title: "删除本周更新的定开项目", - okText: "确认删除 " + pv.count + " 个", - danger: true, - requireText: "删除", - requirePlaceholder: "输入「删除」以确认", - body: - `

将删除更新时间在本周(${esc(pv.start)} ~ ${esc(pv.end)})内的定开项目:` + - `${pv.count} 个,合同额合计 ${fen(pv.amount)} 万。

` + - `
此操作会连同项目的更新记录一起删除,且不可撤销。` + - `若只是想标记,请在详情里改状态。
` + - (pv.names && pv.names.length - ? `
${pv.names.map((n) => esc(n)).join("、")}${pv.count > pv.names.length ? " …" : ""}
` - : ""), - }); - if (!ok) return; - try { - const r = await post("/custom/purge-week", {}); - toast("已删除本周更新的 " + r.deleted + " 个定开项目"); - afterWrite(); - } catch (e) { - toast("删除失败:" + e.message); - } + host.querySelectorAll("[data-sort]").forEach((th) => + th.addEventListener("click", () => { + const k = th.dataset.sort; + const cur = state.customSort || { key: "update_date", dir: "desc" }; + state.customSort = { key: k, dir: cur.key === k && cur.dir === "desc" ? "asc" : "desc" }; + renderCustom(); + }) + ); } /** @@ -1873,9 +1859,9 @@ // 选项直接对齐《定开项目.xlsx》台账字段取值 const opts = (list, sel) => list.map((v) => ``).join(""); - const originOpts = opts(CUSTOM_ORIGINS, "新签订单"); + const originOpts = opts(CUSTOM_ORIGINS, "华智下单"); const industryOpts = opts(["安平", "政府", "企业", "教育", "交通", "医疗", "高速", "行业", "其他"], ""); - const statusOpts = opts(["待开发", "交付中", "交付完成", "已验收", "退单"], "待开发"); + const statusOpts = opts(["待开发", "进行中", "交付完成", "已验收", "退单"], "待开发"); const riskOpts = opts(["1.高风险", "2.低风险"], "2.低风险"); const numVal = (sel) => ($(sel).value === "" ? null : Number($(sel).value)); const txtVal = (sel) => $(sel).value.trim() || null; @@ -1895,7 +1881,7 @@
-
+
@@ -2030,7 +2016,7 @@ const optList = (list, sel) => list.map((v) => ``).join(""); const originOpts = optList(CUSTOM_ORIGINS, p.origin); const industryOpts = optList(["安平", "政府", "企业", "教育", "交通", "医疗", "高速", "行业", "其他"], p.industry); - const statusOpts = optList(["待开发", "交付中", "交付完成", "已验收", "退单"], p.project_status); + const statusOpts = optList(["待开发", "进行中", "交付完成", "已验收", "退单"], p.project_status); const riskOpts = optList(["1.高风险", "2.低风险"], p.risk_level); // 市场台账(来源《定开项目.xlsx》):详情只读,修改模式给输入框 @@ -2065,7 +2051,7 @@ ${ro("市场下单人天", numOrDash(p.man_days))} ${ro("市场责任人", esc(p.market_owner || "—"))} ${ro("项目状态", statusPill(p.project_status))} - ${ro("进度", (p.project_status === "交付中" || Number(p.progress || 0) > 0) ? Math.round(p.progress || 0) + "%" : "—")} + ${ro("进度", (p.project_status === "进行中" || Number(p.progress || 0) > 0) ? Math.round(p.progress || 0) + "%" : "—")} ${ro("验收时间", esc(p.accept_date || "—"))} ${ro("推动验收计划", esc(p.accept_plan || "—"))} ${ro("风险值", riskPill(p.risk_level))} @@ -3342,7 +3328,7 @@ const UI_KEY = "unis.ui.v1"; const UI_FIELDS = ["view", "gran", "weekId", "monthIdx", "categoryId", "keyOnly", "keyword", "projOwner", "projIdleOnly", "showArchived", "focusFilter", "focusArchived", - "customTab", "customArchived", "customRegion", "customOrigin", "staffArchived", "staffGroup"]; + "customTab", "customArchived", "customRegion", "customOrigin", "customAcceptPlan", "customSort", "staffArchived", "staffGroup"]; function saveUI() { const o = {};