216 lines
8.1 KiB
JavaScript
216 lines
8.1 KiB
JavaScript
/**
|
||
* generate-process-flowchart.js
|
||
* ------------------------------------------------------------
|
||
* configs/site-{SiteId}_{サイト名}/processes.json と
|
||
* siteSettingJsons/site-{SiteId}_latest.json(Status列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.Status(JSON文字列化された配列)を文字列配列として取り出す。無ければ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,
|
||
};
|