ken_nogi/ClaudePleasanter/営業積算システム/generate-process-flowchart.js
Kenichiro NOGI ed33892f08 chore: 作業中の変更を整理しコミット(複数プロジェクト分)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 11:09:50 +09:00

216 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* generate-process-flowchart.js
* ------------------------------------------------------------
* configs/site-{SiteId}_{サイト名}/processes.json と
* siteSettingJsons/site-{SiteId}_latest.jsonStatus列ChoicesTextから
* Mermaid横向きフローチャートStatus遷移図を生成し、標準出力へ表示します。
*
* このスクリプトはファイル書き込み・API送信を一切行いません標準出力のみ
* 出力された```mermaid```ブロックは、仕様書「8. プロセス設定」節へ手動で組み込んでください。
*
* 使い方:
* node generate-process-flowchart.js site-335411
* (フォルダ名は "site-{SiteId}_{サイト名}" 形式だが、"site-{SiteId}" だけの
* 前方一致指定でも configs/ 配下から自動的に該当フォルダを探して解決する)
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
function readJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// フォルダ名は "site-{SiteId}_{サイト名}" 形式。"site-{SiteId}" のみの指定でも
// configs/ 配下から前方一致で該当フォルダを探して解決する。
// (build-desired-config.js の resolveSiteDir と同一ロジック)
function resolveSiteDir(baseDir, arg) {
const configsDir = path.join(baseDir, "configs");
const direct = path.isAbsolute(arg) ? arg : path.join(configsDir, path.basename(arg));
const baseName = path.basename(arg);
const candidates = fs.existsSync(configsDir)
? fs.readdirSync(configsDir).filter((name) => {
const full = path.join(configsDir, name);
return (
fs.statSync(full).isDirectory() && (name === baseName || name.startsWith(`${baseName}_`))
);
})
: [];
const suffixed = candidates.filter((name) => name !== baseName);
if (suffixed.length === 1) return path.join(configsDir, suffixed[0]);
if (suffixed.length > 1) {
console.error(`[エラー] "${baseName}" に一致するフォルダが複数見つかりました: ${suffixed.join(", ")}`);
console.error("フォルダ名をフルで指定してください。");
process.exit(1);
}
if (candidates.includes(baseName)) return path.join(configsDir, baseName);
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) return direct;
console.error(`[エラー] フォルダが見つかりません: ${direct}`);
console.error("先に node extract-site-config.jsまたはnode get-site-config.jsを実行してください。");
process.exit(1);
}
// "値,ラベル,短縮ラベル,色" 形式改行区切りのChoicesTextを 値→ラベル のMapへ変換する
function parseChoicesText(choicesText) {
const map = new Map();
if (!choicesText) return map;
const lines = choicesText.split("\n").filter((line) => line.trim().length > 0);
for (const line of lines) {
const parts = line.split(",");
const value = (parts[0] || "").trim();
if (!value) continue;
const label = (parts[1] || value).trim();
map.set(value, label);
}
return map;
}
// エッジのラベル文字列を組み立てる(プロセス名 + Dept注記 + 📧マーク)
function formatEdgeLabel(process) {
let label = process.DisplayName || process.Name || `Process${process.Id}`;
if (Array.isArray(process.Depts) && process.Depts.length > 0) {
label += ` (Dept${process.Depts.join(",")})`;
}
if (Array.isArray(process.Notifications) && process.Notifications.length > 0) {
label = `📧${label}`;
}
return label;
}
// View.ColumnFilterHash.StatusJSON文字列化された配列を文字列配列として取り出す。無ければnull。
function getFilterStatusList(process) {
const raw = process?.View?.ColumnFilterHash?.Status;
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed.map(String) : null;
} catch (e) {
return null;
}
}
// processes配列から、Mermaidのエッジ文字列配列と、登場したStatusード集合、
// "any"仮想ノードが必要かどうかを組み立てる。
function buildEdges(processes) {
const edgeLines = [];
const statusValues = new Set();
let needsAnyNode = false;
for (const process of processes) {
const current = String(process.CurrentStatus);
const changed = String(process.ChangedStatus);
const label = formatEdgeLabel(process);
const filterList = getFilterStatusList(process);
if (current !== "-1" && changed !== "-1") {
statusValues.add(current);
statusValues.add(changed);
edgeLines.push(` ${current} -->|"${label}"| ${changed}`);
} else if (current === "-1" && changed !== "-1") {
statusValues.add(changed);
if (filterList && filterList.length > 0) {
for (const from of filterList) {
statusValues.add(from);
edgeLines.push(` ${from} -->|"${label}"| ${changed}`);
}
} else {
needsAnyNode = true;
edgeLines.push(` any -->|"${label}"| ${changed}`);
}
} else if (current !== "-1" && changed === "-1") {
statusValues.add(current);
edgeLines.push(` ${current} -->|"${label}"| ${current}`);
} else {
if (filterList && filterList.length > 0) {
for (const target of filterList) {
statusValues.add(target);
edgeLines.push(` ${target} -->|"${label}"| ${target}`);
}
} else {
needsAnyNode = true;
edgeLines.push(` any -->|"${label}"| any`);
}
}
}
return { edgeLines, statusValues, needsAnyNode };
}
// siteSettingJsons/site-{SiteId}_latest.json からStatus列のChoicesTextを取り出し、
// 値→ラベル のMapを返す。見つからない場合は空Mapを返し警告を出す。
function loadStatusLabels(baseDir, siteId) {
const latestPath = path.join(baseDir, "siteSettingJsons", `site-${siteId}_latest.json`);
if (!fs.existsSync(latestPath)) {
console.error(`[警告] ${latestPath} が見つかりません。ノードは値のみで表示します。`);
return new Map();
}
const json = readJson(latestPath);
const columns = json?.Response?.Data?.SiteSettings?.Columns || [];
const statusColumn = columns.find((col) => col.ColumnName === "Status");
if (!statusColumn || !statusColumn.ChoicesText) {
console.error("[警告] Status列のChoicesTextが見つかりません。ードは値のみで表示します。");
return new Map();
}
return parseChoicesText(statusColumn.ChoicesText);
}
// Mermaid本文```mermaid〜```込み)を組み立てる
function buildMermaid(processes, statusLabels) {
const { edgeLines, statusValues, needsAnyNode } = buildEdges(processes);
const nodeLines = Array.from(statusValues).map((value) => {
const label = statusLabels.get(value) || value;
return ` ${value}((${label}<br/>${value}))`;
});
if (needsAnyNode) {
nodeLines.push(" any((任意))");
}
const lines = ["```mermaid", "flowchart LR", ...nodeLines, "", ...edgeLines, "```"];
return lines.join("\n");
}
function main() {
const baseDir = __dirname;
const arg = process.argv[2];
if (!arg) {
console.error("[エラー] 対象フォルダを指定してください(例: node generate-process-flowchart.js site-335411");
process.exit(1);
}
const siteDir = resolveSiteDir(baseDir, arg);
const processesPath = path.join(siteDir, "processes.json");
if (!fs.existsSync(processesPath)) {
console.error(`[エラー] processes.jsonが見つかりません: ${processesPath}`);
console.error("先に node get-site-config.js を実行してください。");
process.exit(1);
}
const processes = readJson(processesPath);
const siteIdMatch = path.basename(siteDir).match(/^site-(\d+)/);
const siteId = siteIdMatch ? siteIdMatch[1] : null;
const statusLabels = siteId ? loadStatusLabels(baseDir, siteId) : new Map();
const mermaid = buildMermaid(processes, statusLabels);
console.log(mermaid);
}
if (require.main === module) {
main();
}
module.exports = {
resolveSiteDir,
parseChoicesText,
formatEdgeLabel,
getFilterStatusList,
buildEdges,
loadStatusLabels,
buildMermaid,
};