健康診断管理システムのn8nワークフロー設計ドキュメントとPleasanter操作スクリプトを追加
n8nに構築済みのHC-SUB/HC-WP/HC-WAワークフロー設計・実装計画・エクスポートJSONを Pleasanter/健康診断管理/docs/n8n/に集約管理。508971/513156のColumns/Styles/Summaries 反映用ワンショットスクリプトも合わせて追加。worktree-healthcheck-survey-botブランチを mainへfast-forward mergeし、.claude/worktrees/はgit管理対象外に追加。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
bdec81838e
commit
bd6a605539
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,3 +6,6 @@ logs/
|
||||
# 巨大バイナリ・アーカイブ(サイズが大きすぎるためgit管理対象外。ローカル/別バックアップで管理)
|
||||
NodeSrv/notepm/export_nextgroup_20260807020526491/
|
||||
NodeSrv/Keys/Pleasanter_1.5.7.1.zip
|
||||
|
||||
# git worktree実体(別ブランチの作業ツリー。中身は各ブランチ側でコミット管理)
|
||||
.claude/worktrees/
|
||||
|
||||
@ -0,0 +1,133 @@
|
||||
// 健康診断管理 SiteId 508971(社員別健康診断管理): Summaries(513156への集計設定)に、
|
||||
// 既存のNum031/Num032と同じ条件(SiteId:513156, DestinationReferenceType:Results,
|
||||
// LinkColumn:ClassZ, Type:Total, SourceColumn=DestinationColumn)でNum033~038/Num061~071/081~091を
|
||||
// 追加するワンショットスクリプト。updatesite(Mode:full)を使う。
|
||||
// --execute指定時のみ実際に送信、指定なければプレビューのみ出力。
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { findSiteDir, newModifyDir } = require("../../site-paths.js");
|
||||
|
||||
const PROJECT_NAME = "健康診断管理";
|
||||
const ENV = "production";
|
||||
const SITE_ID = 508971;
|
||||
const SITE_NAME = "社員別健康診断管理";
|
||||
const DEST_SITE_ID = 513156;
|
||||
const REQUEST_LABEL = "add-summaries-num033-038-and-061-091";
|
||||
|
||||
const TARGET_COLUMNS = [
|
||||
"Num033", "Num034", "Num035", "Num036", "Num037", "Num038",
|
||||
"Num061", "Num062", "Num063", "Num064", "Num065", "Num066", "Num067", "Num068", "Num069", "Num070", "Num071",
|
||||
"Num081", "Num082", "Num083", "Num084", "Num085", "Num086", "Num087", "Num088", "Num089", "Num090", "Num091",
|
||||
];
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..", "..");
|
||||
const PROJECT_ROOT = path.join(REPO_ROOT, PROJECT_NAME);
|
||||
const CONFIGS_DIR = path.join(PROJECT_ROOT, "configs", ENV);
|
||||
|
||||
function loadServerConfig() {
|
||||
const configPath = path.join(REPO_ROOT, `config_${ENV}.json`);
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
if (!config.BaseUrl || !config.ApiKey) {
|
||||
console.error(`[エラー] ${configPath} に BaseUrl / ApiKey を設定してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadLatest() {
|
||||
const siteDir = findSiteDir(CONFIGS_DIR, SITE_ID);
|
||||
if (!siteDir) {
|
||||
console.error(`[エラー] configs/${ENV}/site-${SITE_ID}_* が見つかりません。先にget-site-config.jsを実行してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const p = path.join(siteDir, "sitesettings", `site-${SITE_ID}_latest.json`);
|
||||
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
||||
return { current: raw.Response.Data, siteDir, rawText: fs.readFileSync(p, "utf-8") };
|
||||
}
|
||||
|
||||
function buildDesiredSiteSettings(current) {
|
||||
const settings = JSON.parse(JSON.stringify(current.SiteSettings));
|
||||
const summaries = settings.Summaries || [];
|
||||
|
||||
const existingIds = summaries.map((s) => s.Id);
|
||||
let nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
|
||||
|
||||
const added = [];
|
||||
for (const col of TARGET_COLUMNS) {
|
||||
if (summaries.some((s) => s.SourceColumn === col && s.SiteId === DEST_SITE_ID)) {
|
||||
console.error(`[エラー] Summaries内に${col}(SiteId${DEST_SITE_ID}向け)が既に存在します(重複防止のため中断)。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const entry = {
|
||||
SiteId: DEST_SITE_ID,
|
||||
DestinationReferenceType: "Results",
|
||||
DestinationColumn: col,
|
||||
LinkColumn: "ClassZ",
|
||||
Type: "Total",
|
||||
SourceColumn: col,
|
||||
Id: nextId,
|
||||
};
|
||||
summaries.push(entry);
|
||||
added.push(entry);
|
||||
nextId++;
|
||||
}
|
||||
settings.Summaries = summaries;
|
||||
|
||||
// Links省略厳禁(全置換のため既存値をそのまま含める)
|
||||
settings.Links = current.SiteSettings.Links || [];
|
||||
|
||||
return { settings, added };
|
||||
}
|
||||
|
||||
function buildUpdateBody(apiKey, current, desiredSettings) {
|
||||
return {
|
||||
ApiVersion: "1.1",
|
||||
ApiKey: apiKey,
|
||||
SiteId: SITE_ID,
|
||||
Title: current.Title,
|
||||
ReferenceType: current.ReferenceType,
|
||||
ParentId: current.ParentId,
|
||||
InheritPermission: current.InheritPermission,
|
||||
SiteSettings: desiredSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const execute = process.argv.includes("--execute");
|
||||
const server = loadServerConfig();
|
||||
|
||||
(async () => {
|
||||
const { current, siteDir, rawText } = loadLatest();
|
||||
const { settings: desiredSettings, added } = buildDesiredSiteSettings(current);
|
||||
const body = buildUpdateBody(server.ApiKey, current, desiredSettings);
|
||||
|
||||
const modifyDir = newModifyDir(siteDir, REQUEST_LABEL);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
fs.writeFileSync(path.join(modifyDir, `before_site-${SITE_ID}_latest.json`), rawText, "utf-8");
|
||||
const updatedPath = path.join(modifyDir, `site-${SITE_ID}_updated_${timestamp}.json`);
|
||||
fs.writeFileSync(updatedPath, JSON.stringify(body, null, 2), "utf-8");
|
||||
|
||||
console.log(`\n==== SiteId ${SITE_ID} (${SITE_NAME}) ====`);
|
||||
console.log(`追加するSummaries(SiteId${DEST_SITE_ID}向け):`);
|
||||
for (const s of added) console.log(` - Id${s.Id}: ${s.SourceColumn} → ${s.DestinationColumn} (Type:${s.Type}, LinkColumn:${s.LinkColumn})`);
|
||||
console.log(`\nSummaries件数: 変更前${(current.SiteSettings.Summaries || []).length} → 変更後${desiredSettings.Summaries.length}`);
|
||||
console.log(`プレビュー保存先: ${updatedPath}`);
|
||||
|
||||
if (!execute) {
|
||||
console.log("\n(--execute 未指定のためプレビューのみ。送信していません)");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${server.BaseUrl}api/items/${SITE_ID}/updatesite`;
|
||||
console.log(`送信先: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log(`[結果] HTTP ${res.status}:`, JSON.stringify(json));
|
||||
const resultPath = path.join(modifyDir, `site-${SITE_ID}_result_${timestamp}.json`);
|
||||
fs.writeFileSync(resultPath, JSON.stringify(json, null, 2), "utf-8");
|
||||
})();
|
||||
@ -0,0 +1,140 @@
|
||||
// 健康診断管理 SiteId 513156(実施年度-集計): site-508971_社員別健康診断管理のNum061~071/081~091の
|
||||
// Column設定(LabelText/ControlType/NoWrap/Min/Max/Description)をそのまま複製して新規追加し、
|
||||
// 508971のStyles(Id1:基本スタイル調整→Id2:数値エリア調整の順)も複製して適用するワンショットスクリプト。
|
||||
// EditorColumnHash.General(Num061~091)は画面上で追加済みのため変更しない。updatesite(Mode:full)を使う。
|
||||
// --execute指定時のみ実際に送信、指定なければプレビュー(送信Body・diff)のみ出力。
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { findSiteDir, newModifyDir } = require("../../site-paths.js");
|
||||
|
||||
const PROJECT_NAME = "健康診断管理";
|
||||
const ENV = "production";
|
||||
const SRC_SITE_ID = 508971;
|
||||
const DST_SITE_ID = 513156;
|
||||
const DST_SITE_NAME = "実施年度-集計";
|
||||
const REQUEST_LABEL = "apply-508971-num-columns-and-styles";
|
||||
|
||||
const NUM_COLUMN_NAMES = [
|
||||
"Num061", "Num062", "Num063", "Num064", "Num065", "Num066", "Num067", "Num068", "Num069", "Num070", "Num071",
|
||||
"Num081", "Num082", "Num083", "Num084", "Num085", "Num086", "Num087", "Num088", "Num089", "Num090", "Num091",
|
||||
];
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..", "..");
|
||||
const PROJECT_ROOT = path.join(REPO_ROOT, PROJECT_NAME);
|
||||
const CONFIGS_DIR = path.join(PROJECT_ROOT, "configs", ENV);
|
||||
|
||||
function loadServerConfig() {
|
||||
const configPath = path.join(REPO_ROOT, `config_${ENV}.json`);
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
if (!config.BaseUrl || !config.ApiKey) {
|
||||
console.error(`[エラー] ${configPath} に BaseUrl / ApiKey を設定してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadLatest(siteId) {
|
||||
const siteDir = findSiteDir(CONFIGS_DIR, siteId);
|
||||
if (!siteDir) {
|
||||
console.error(`[エラー] configs/${ENV}/site-${siteId}_* が見つかりません。先にget-site-config.jsを実行してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const p = path.join(siteDir, "sitesettings", `site-${siteId}_latest.json`);
|
||||
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
||||
return { current: raw.Response.Data, siteDir, rawText: fs.readFileSync(p, "utf-8") };
|
||||
}
|
||||
|
||||
function buildDesiredSiteSettings(srcCurrent, dstCurrent) {
|
||||
const settings = JSON.parse(JSON.stringify(dstCurrent.SiteSettings));
|
||||
|
||||
const addedColumns = [];
|
||||
for (const name of NUM_COLUMN_NAMES) {
|
||||
if (settings.Columns.some((c) => c.ColumnName === name)) {
|
||||
console.error(`[エラー] 移行先Columns内に${name}が既に存在します(重複追加防止のため中断)。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const src = srcCurrent.SiteSettings.Columns.find((c) => c.ColumnName === name);
|
||||
if (!src) {
|
||||
console.error(`[エラー] 移行元(508971)Columns内に${name}が見つかりません。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const col = JSON.parse(JSON.stringify(src));
|
||||
settings.Columns.push(col);
|
||||
addedColumns.push(col);
|
||||
}
|
||||
|
||||
// Styles: 508971のId1(基本スタイル調整)→Id2(数値エリア調整)の順で複製
|
||||
const srcStyles = srcCurrent.SiteSettings.Styles || [];
|
||||
const style1 = srcStyles.find((s) => s.Id === 1);
|
||||
const style2 = srcStyles.find((s) => s.Id === 2);
|
||||
if (!style1 || !style2) {
|
||||
console.error("[エラー] 移行元(508971)にStyle Id1/Id2が見つかりません。");
|
||||
process.exit(1);
|
||||
}
|
||||
settings.Styles = [
|
||||
{ ...style1 },
|
||||
{ ...style2 },
|
||||
];
|
||||
|
||||
// Links省略厳禁(全置換のため既存値をそのまま含める。移行先に元々設定が無ければ空配列)
|
||||
settings.Links = dstCurrent.SiteSettings.Links || [];
|
||||
|
||||
return { settings, addedColumns };
|
||||
}
|
||||
|
||||
function buildUpdateBody(apiKey, current, desiredSettings) {
|
||||
return {
|
||||
ApiVersion: "1.1",
|
||||
ApiKey: apiKey,
|
||||
SiteId: DST_SITE_ID,
|
||||
Title: current.Title,
|
||||
ReferenceType: current.ReferenceType,
|
||||
ParentId: current.ParentId,
|
||||
InheritPermission: current.InheritPermission,
|
||||
SiteSettings: desiredSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const execute = process.argv.includes("--execute");
|
||||
const server = loadServerConfig();
|
||||
|
||||
(async () => {
|
||||
const { current: srcCurrent } = loadLatest(SRC_SITE_ID);
|
||||
const { current: dstCurrent, siteDir: dstSiteDir, rawText: dstRawText } = loadLatest(DST_SITE_ID);
|
||||
|
||||
const { settings: desiredSettings, addedColumns } = buildDesiredSiteSettings(srcCurrent, dstCurrent);
|
||||
const body = buildUpdateBody(server.ApiKey, dstCurrent, desiredSettings);
|
||||
|
||||
const modifyDir = newModifyDir(dstSiteDir, REQUEST_LABEL);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
fs.writeFileSync(path.join(modifyDir, `before_site-${DST_SITE_ID}_latest.json`), dstRawText, "utf-8");
|
||||
const updatedPath = path.join(modifyDir, `site-${DST_SITE_ID}_updated_${timestamp}.json`);
|
||||
fs.writeFileSync(updatedPath, JSON.stringify(body, null, 2), "utf-8");
|
||||
|
||||
console.log(`\n==== SiteId ${DST_SITE_ID} (${DST_SITE_NAME}) ====`);
|
||||
console.log("追加するColumns(508971から複製):");
|
||||
for (const c of addedColumns) console.log(` - ${c.ColumnName}: ${c.LabelText}`);
|
||||
console.log("適用するStyles(508971から複製、順序: Id1→Id2):");
|
||||
for (const s of desiredSettings.Styles) console.log(` - Id${s.Id}: ${s.Title}`);
|
||||
console.log(`\nColumns件数: 変更前${dstCurrent.SiteSettings.Columns.length} → 変更後${desiredSettings.Columns.length}`);
|
||||
console.log(`プレビュー保存先: ${updatedPath}`);
|
||||
|
||||
if (!execute) {
|
||||
console.log("\n(--execute 未指定のためプレビューのみ。送信していません)");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${server.BaseUrl}api/items/${DST_SITE_ID}/updatesite`;
|
||||
console.log(`送信先: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log(`[結果] HTTP ${res.status}:`, JSON.stringify(json));
|
||||
const resultPath = path.join(modifyDir, `site-${DST_SITE_ID}_result_${timestamp}.json`);
|
||||
fs.writeFileSync(resultPath, JSON.stringify(json, null, 2), "utf-8");
|
||||
})();
|
||||
@ -0,0 +1,128 @@
|
||||
// 健康診断管理 SiteId 508971(社員別健康診断管理): 513156(実施年度-集計)側で
|
||||
// NumA~H→Num031~038へ置き換えた後の設定(LabelText/Unit/TextAlign/NoWrap等)をそのまま複製し、
|
||||
// 508971のColumnsへ新規追加するワンショットスクリプト。EditorColumnHash._Tab-2(カウンタータブ)には
|
||||
// 画面上で既にNum031~038が配置済み(Columns定義のみ未設定)のため、EditorColumnHashは変更しない。
|
||||
// 508971の既存Num項目(NumA=年齢, Num001~003, Num061~091)には一切触れない。
|
||||
// updatesite(Mode:full)を使う。--execute指定時のみ実際に送信、指定なければプレビューのみ出力。
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { findSiteDir, newModifyDir } = require("../../site-paths.js");
|
||||
|
||||
const PROJECT_NAME = "健康診断管理";
|
||||
const ENV = "production";
|
||||
const SRC_SITE_ID = 513156;
|
||||
const DST_SITE_ID = 508971;
|
||||
const DST_SITE_NAME = "社員別健康診断管理";
|
||||
const REQUEST_LABEL = "apply-num031-038-columns";
|
||||
|
||||
const NUM_COLUMN_NAMES = ["Num031", "Num032", "Num033", "Num034", "Num035", "Num036", "Num037", "Num038"];
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..", "..");
|
||||
const PROJECT_ROOT = path.join(REPO_ROOT, PROJECT_NAME);
|
||||
const CONFIGS_DIR = path.join(PROJECT_ROOT, "configs", ENV);
|
||||
|
||||
function loadServerConfig() {
|
||||
const configPath = path.join(REPO_ROOT, `config_${ENV}.json`);
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
if (!config.BaseUrl || !config.ApiKey) {
|
||||
console.error(`[エラー] ${configPath} に BaseUrl / ApiKey を設定してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadLatest(siteId) {
|
||||
const siteDir = findSiteDir(CONFIGS_DIR, siteId);
|
||||
if (!siteDir) {
|
||||
console.error(`[エラー] configs/${ENV}/site-${siteId}_* が見つかりません。先にget-site-config.jsを実行してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const p = path.join(siteDir, "sitesettings", `site-${siteId}_latest.json`);
|
||||
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
||||
return { current: raw.Response.Data, siteDir, rawText: fs.readFileSync(p, "utf-8") };
|
||||
}
|
||||
|
||||
function buildDesiredSiteSettings(srcCurrent, dstCurrent) {
|
||||
const settings = JSON.parse(JSON.stringify(dstCurrent.SiteSettings));
|
||||
|
||||
const tab2 = settings.EditorColumnHash["_Tab-2"] || [];
|
||||
const addedColumns = [];
|
||||
for (const name of NUM_COLUMN_NAMES) {
|
||||
if (settings.Columns.some((c) => c.ColumnName === name)) {
|
||||
console.error(`[エラー] 移行先(508971)Columns内に${name}が既に存在します(重複防止のため中断)。`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!tab2.includes(name)) {
|
||||
console.error(`[エラー] EditorColumnHash._Tab-2内に${name}が見つかりません。画面上の配置状況を確認してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const src = srcCurrent.SiteSettings.Columns.find((c) => c.ColumnName === name);
|
||||
if (!src) {
|
||||
console.error(`[エラー] 移行元(513156)Columns内に${name}が見つかりません。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const col = JSON.parse(JSON.stringify(src));
|
||||
settings.Columns.push(col);
|
||||
addedColumns.push(col);
|
||||
}
|
||||
|
||||
// Links省略厳禁(全置換のため既存値をそのまま含める)
|
||||
settings.Links = dstCurrent.SiteSettings.Links || [];
|
||||
|
||||
return { settings, addedColumns };
|
||||
}
|
||||
|
||||
function buildUpdateBody(apiKey, current, desiredSettings) {
|
||||
return {
|
||||
ApiVersion: "1.1",
|
||||
ApiKey: apiKey,
|
||||
SiteId: DST_SITE_ID,
|
||||
Title: current.Title,
|
||||
ReferenceType: current.ReferenceType,
|
||||
ParentId: current.ParentId,
|
||||
InheritPermission: current.InheritPermission,
|
||||
SiteSettings: desiredSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const execute = process.argv.includes("--execute");
|
||||
const server = loadServerConfig();
|
||||
|
||||
(async () => {
|
||||
const { current: srcCurrent } = loadLatest(SRC_SITE_ID);
|
||||
const { current: dstCurrent, siteDir: dstSiteDir, rawText: dstRawText } = loadLatest(DST_SITE_ID);
|
||||
|
||||
const { settings: desiredSettings, addedColumns } = buildDesiredSiteSettings(srcCurrent, dstCurrent);
|
||||
const body = buildUpdateBody(server.ApiKey, dstCurrent, desiredSettings);
|
||||
|
||||
const modifyDir = newModifyDir(dstSiteDir, REQUEST_LABEL);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
fs.writeFileSync(path.join(modifyDir, `before_site-${DST_SITE_ID}_latest.json`), dstRawText, "utf-8");
|
||||
const updatedPath = path.join(modifyDir, `site-${DST_SITE_ID}_updated_${timestamp}.json`);
|
||||
fs.writeFileSync(updatedPath, JSON.stringify(body, null, 2), "utf-8");
|
||||
|
||||
console.log(`\n==== SiteId ${DST_SITE_ID} (${DST_SITE_NAME}) ====`);
|
||||
console.log("追加するColumns(513156から複製):");
|
||||
for (const c of addedColumns) console.log(` - ${JSON.stringify(c)}`);
|
||||
console.log(`\nColumns件数: 変更前${dstCurrent.SiteSettings.Columns.length} → 変更後${desiredSettings.Columns.length}`);
|
||||
console.log(`プレビュー保存先: ${updatedPath}`);
|
||||
|
||||
if (!execute) {
|
||||
console.log("\n(--execute 未指定のためプレビューのみ。送信していません)");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${server.BaseUrl}api/items/${DST_SITE_ID}/updatesite`;
|
||||
console.log(`送信先: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log(`[結果] HTTP ${res.status}:`, JSON.stringify(json));
|
||||
const resultPath = path.join(modifyDir, `site-${DST_SITE_ID}_result_${timestamp}.json`);
|
||||
fs.writeFileSync(resultPath, JSON.stringify(json, null, 2), "utf-8");
|
||||
})();
|
||||
@ -0,0 +1,113 @@
|
||||
// 健康診断管理 SiteId 508971: 有所見者数側(Num081~091)のNoWrapをfalseに修正するワンショットスクリプト。
|
||||
// restructure-site-508971-tab2-counts.jsで追加した際、実施人数側(Num061~071)のNoWrap:trueを
|
||||
// そのまま複製していたため、有所見者数側のみfalseへ訂正する。updatesite(Mode:full)を使う。
|
||||
// --execute指定時のみ実際に送信、指定なければプレビュー(送信Body・diff)のみ出力。
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { findSiteDir, newModifyDir } = require("../../site-paths.js");
|
||||
|
||||
const PROJECT_NAME = "健康診断管理";
|
||||
const ENV = "production";
|
||||
const SITE_ID = 508971;
|
||||
const SITE_NAME = "健康診断管理";
|
||||
const REQUEST_LABEL = "fix-arishoken-nowrap";
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..", "..");
|
||||
const PROJECT_ROOT = path.join(REPO_ROOT, PROJECT_NAME);
|
||||
const CONFIGS_DIR = path.join(PROJECT_ROOT, "configs", ENV);
|
||||
|
||||
const TARGET_COLUMNS = [
|
||||
"Num081", "Num082", "Num083", "Num084", "Num085",
|
||||
"Num086", "Num087", "Num088", "Num089", "Num090", "Num091",
|
||||
];
|
||||
|
||||
function loadServerConfig() {
|
||||
const configPath = path.join(REPO_ROOT, `config_${ENV}.json`);
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
if (!config.BaseUrl || !config.ApiKey) {
|
||||
console.error(`[エラー] ${configPath} に BaseUrl / ApiKey を設定してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadLatest() {
|
||||
const siteDir = findSiteDir(CONFIGS_DIR, SITE_ID);
|
||||
if (!siteDir) {
|
||||
console.error(`[エラー] configs/${ENV}/site-${SITE_ID}_* が見つかりません。先にget-site-config.jsを実行してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const p = path.join(siteDir, "sitesettings", `site-${SITE_ID}_latest.json`);
|
||||
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
||||
return { current: raw.Response.Data, siteDir, rawText: fs.readFileSync(p, "utf-8") };
|
||||
}
|
||||
|
||||
function buildDesiredSiteSettings(current) {
|
||||
const settings = JSON.parse(JSON.stringify(current.SiteSettings));
|
||||
const changed = [];
|
||||
for (const name of TARGET_COLUMNS) {
|
||||
const col = settings.Columns.find((c) => c.ColumnName === name);
|
||||
if (!col) {
|
||||
console.error(`[エラー] Columns内に${name}が見つかりません。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const before = col.NoWrap;
|
||||
col.NoWrap = false;
|
||||
changed.push({ name, before, after: col.NoWrap, label: col.LabelText });
|
||||
}
|
||||
// Links省略厳禁(全置換のため既存値をそのまま含める)
|
||||
settings.Links = current.SiteSettings.Links || [];
|
||||
return { settings, changed };
|
||||
}
|
||||
|
||||
function buildUpdateBody(apiKey, current, desiredSettings) {
|
||||
return {
|
||||
ApiVersion: "1.1",
|
||||
ApiKey: apiKey,
|
||||
SiteId: SITE_ID,
|
||||
Title: current.Title,
|
||||
ReferenceType: current.ReferenceType,
|
||||
ParentId: current.ParentId,
|
||||
InheritPermission: current.InheritPermission,
|
||||
SiteSettings: desiredSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const execute = process.argv.includes("--execute");
|
||||
const server = loadServerConfig();
|
||||
|
||||
(async () => {
|
||||
const { current, siteDir, rawText } = loadLatest();
|
||||
const { settings: desiredSettings, changed } = buildDesiredSiteSettings(current);
|
||||
const body = buildUpdateBody(server.ApiKey, current, desiredSettings);
|
||||
|
||||
const modifyDir = newModifyDir(siteDir, REQUEST_LABEL);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
fs.writeFileSync(path.join(modifyDir, `before_site-${SITE_ID}_latest.json`), rawText, "utf-8");
|
||||
const updatedPath = path.join(modifyDir, `site-${SITE_ID}_updated_${timestamp}.json`);
|
||||
fs.writeFileSync(updatedPath, JSON.stringify(body, null, 2), "utf-8");
|
||||
|
||||
console.log(`\n==== SiteId ${SITE_ID} (${SITE_NAME}) ====`);
|
||||
console.log("NoWrap変更:");
|
||||
for (const c of changed) console.log(` - ${c.name} (${c.label}): ${c.before} → ${c.after}`);
|
||||
console.log(`プレビュー保存先: ${updatedPath}`);
|
||||
|
||||
if (!execute) {
|
||||
console.log("\n(--execute 未指定のためプレビューのみ。送信していません)");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${server.BaseUrl}api/items/${SITE_ID}/updatesite`;
|
||||
console.log(`送信先: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log(`[結果] HTTP ${res.status}:`, JSON.stringify(json));
|
||||
const resultPath = path.join(modifyDir, `site-${SITE_ID}_result_${timestamp}.json`);
|
||||
fs.writeFileSync(resultPath, JSON.stringify(json, null, 2), "utf-8");
|
||||
})();
|
||||
@ -0,0 +1,129 @@
|
||||
// 健康診断管理 SiteId 513156(実施年度-集計): EditorColumnHash.Generalの並びを
|
||||
// Num061→Num081→Num062→Num082…と交互配置に組み替えるワンショットスクリプト
|
||||
// (508971の並びに合わせる。508971にはNum061~071→Num081~091の連続配置で反映されており見た目が揃わないため)。
|
||||
// Columns自体の変更は無く配置順のみ変更。updatesite(Mode:full)を使う。
|
||||
// --execute指定時のみ実際に送信、指定なければプレビュー(送信Body・diff)のみ出力。
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { findSiteDir, newModifyDir } = require("../../site-paths.js");
|
||||
|
||||
const PROJECT_NAME = "健康診断管理";
|
||||
const ENV = "production";
|
||||
const SITE_ID = 513156;
|
||||
const SITE_NAME = "実施年度-集計";
|
||||
const REQUEST_LABEL = "fix-editorcolumnhash-order";
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..", "..");
|
||||
const PROJECT_ROOT = path.join(REPO_ROOT, PROJECT_NAME);
|
||||
const CONFIGS_DIR = path.join(PROJECT_ROOT, "configs", ENV);
|
||||
|
||||
// [実施人数側, 有所見者数側] のペア。左の直後に右を差し込む
|
||||
const PAIRS = [
|
||||
["Num061", "Num081"],
|
||||
["Num062", "Num082"],
|
||||
["Num063", "Num083"],
|
||||
["Num064", "Num084"],
|
||||
["Num065", "Num085"],
|
||||
["Num066", "Num086"],
|
||||
["Num067", "Num087"],
|
||||
["Num068", "Num088"],
|
||||
["Num069", "Num089"],
|
||||
["Num070", "Num090"],
|
||||
["Num071", "Num091"],
|
||||
];
|
||||
|
||||
function loadServerConfig() {
|
||||
const configPath = path.join(REPO_ROOT, `config_${ENV}.json`);
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
if (!config.BaseUrl || !config.ApiKey) {
|
||||
console.error(`[エラー] ${configPath} に BaseUrl / ApiKey を設定してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadLatest() {
|
||||
const siteDir = findSiteDir(CONFIGS_DIR, SITE_ID);
|
||||
if (!siteDir) {
|
||||
console.error(`[エラー] configs/${ENV}/site-${SITE_ID}_* が見つかりません。先にget-site-config.jsを実行してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const p = path.join(siteDir, "sitesettings", `site-${SITE_ID}_latest.json`);
|
||||
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
||||
return { current: raw.Response.Data, siteDir, rawText: fs.readFileSync(p, "utf-8") };
|
||||
}
|
||||
|
||||
function buildDesiredSiteSettings(current) {
|
||||
const settings = JSON.parse(JSON.stringify(current.SiteSettings));
|
||||
|
||||
const general = settings.EditorColumnHash.General;
|
||||
const pairSrcSet = new Set(PAIRS.map(([src]) => src));
|
||||
const arishokenSet = new Set(PAIRS.map(([, dst]) => dst));
|
||||
const rebuilt = [];
|
||||
for (const name of general) {
|
||||
if (arishokenSet.has(name)) continue; // 有所見者数側は元位置から除去し、対応する実施人数側の直後へ差し込む
|
||||
rebuilt.push(name);
|
||||
if (pairSrcSet.has(name)) {
|
||||
const pair = PAIRS.find(([src]) => src === name);
|
||||
rebuilt.push(pair[1]);
|
||||
}
|
||||
}
|
||||
settings.EditorColumnHash.General = rebuilt;
|
||||
|
||||
// Links省略厳禁(全置換のため既存値をそのまま含める)
|
||||
settings.Links = current.SiteSettings.Links || [];
|
||||
|
||||
return { settings, rebuilt };
|
||||
}
|
||||
|
||||
function buildUpdateBody(apiKey, current, desiredSettings) {
|
||||
return {
|
||||
ApiVersion: "1.1",
|
||||
ApiKey: apiKey,
|
||||
SiteId: SITE_ID,
|
||||
Title: current.Title,
|
||||
ReferenceType: current.ReferenceType,
|
||||
ParentId: current.ParentId,
|
||||
InheritPermission: current.InheritPermission,
|
||||
SiteSettings: desiredSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const execute = process.argv.includes("--execute");
|
||||
const server = loadServerConfig();
|
||||
|
||||
(async () => {
|
||||
const { current, siteDir, rawText } = loadLatest();
|
||||
const { settings: desiredSettings, rebuilt } = buildDesiredSiteSettings(current);
|
||||
const body = buildUpdateBody(server.ApiKey, current, desiredSettings);
|
||||
|
||||
const modifyDir = newModifyDir(siteDir, REQUEST_LABEL);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
fs.writeFileSync(path.join(modifyDir, `before_site-${SITE_ID}_latest.json`), rawText, "utf-8");
|
||||
const updatedPath = path.join(modifyDir, `site-${SITE_ID}_updated_${timestamp}.json`);
|
||||
fs.writeFileSync(updatedPath, JSON.stringify(body, null, 2), "utf-8");
|
||||
|
||||
console.log(`\n==== SiteId ${SITE_ID} (${SITE_NAME}) ====`);
|
||||
console.log("EditorColumnHash.General 変更後の並び:");
|
||||
console.log(` [${rebuilt.join(", ")}]`);
|
||||
console.log(`プレビュー保存先: ${updatedPath}`);
|
||||
|
||||
if (!execute) {
|
||||
console.log("\n(--execute 未指定のためプレビューのみ。送信していません)");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${server.BaseUrl}api/items/${SITE_ID}/updatesite`;
|
||||
console.log(`送信先: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log(`[結果] HTTP ${res.status}:`, JSON.stringify(json));
|
||||
const resultPath = path.join(modifyDir, `site-${SITE_ID}_result_${timestamp}.json`);
|
||||
fs.writeFileSync(resultPath, JSON.stringify(json, null, 2), "utf-8");
|
||||
})();
|
||||
@ -0,0 +1,120 @@
|
||||
// 健康診断管理 SiteId 513156(実施年度-集計): 診断結果集計Num項目(Num061~071/081~091)の
|
||||
// ControlType/Min/Maxを削除し、Unit:"人"/TextAlign:20を付与するワンショットスクリプト
|
||||
// (NumA~Hは既にUnit:"人"/TextAlign:20設定済みのため対象外。508971側のSpinner/Min0/Max1指定は
|
||||
// このサイトの集計項目には不要なため、508971とは別設定にする)。
|
||||
// updatesite(Mode:full)を使う。--execute指定時のみ実際に送信、指定なければプレビューのみ出力。
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { findSiteDir, newModifyDir } = require("../../site-paths.js");
|
||||
|
||||
const PROJECT_NAME = "健康診断管理";
|
||||
const ENV = "production";
|
||||
const SITE_ID = 513156;
|
||||
const SITE_NAME = "実施年度-集計";
|
||||
const REQUEST_LABEL = "fix-num-attrs-unit-textalign";
|
||||
|
||||
const TARGET_COLUMNS = [
|
||||
"Num061", "Num062", "Num063", "Num064", "Num065", "Num066", "Num067", "Num068", "Num069", "Num070", "Num071",
|
||||
"Num081", "Num082", "Num083", "Num084", "Num085", "Num086", "Num087", "Num088", "Num089", "Num090", "Num091",
|
||||
];
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..", "..");
|
||||
const PROJECT_ROOT = path.join(REPO_ROOT, PROJECT_NAME);
|
||||
const CONFIGS_DIR = path.join(PROJECT_ROOT, "configs", ENV);
|
||||
|
||||
function loadServerConfig() {
|
||||
const configPath = path.join(REPO_ROOT, `config_${ENV}.json`);
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
if (!config.BaseUrl || !config.ApiKey) {
|
||||
console.error(`[エラー] ${configPath} に BaseUrl / ApiKey を設定してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadLatest() {
|
||||
const siteDir = findSiteDir(CONFIGS_DIR, SITE_ID);
|
||||
if (!siteDir) {
|
||||
console.error(`[エラー] configs/${ENV}/site-${SITE_ID}_* が見つかりません。先にget-site-config.jsを実行してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const p = path.join(siteDir, "sitesettings", `site-${SITE_ID}_latest.json`);
|
||||
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
||||
return { current: raw.Response.Data, siteDir, rawText: fs.readFileSync(p, "utf-8") };
|
||||
}
|
||||
|
||||
function buildDesiredSiteSettings(current) {
|
||||
const settings = JSON.parse(JSON.stringify(current.SiteSettings));
|
||||
const changed = [];
|
||||
for (const name of TARGET_COLUMNS) {
|
||||
const col = settings.Columns.find((c) => c.ColumnName === name);
|
||||
if (!col) {
|
||||
console.error(`[エラー] Columns内に${name}が見つかりません。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const before = JSON.parse(JSON.stringify(col));
|
||||
delete col.ControlType;
|
||||
delete col.Min;
|
||||
delete col.Max;
|
||||
col.Unit = "人";
|
||||
col.TextAlign = 20;
|
||||
changed.push({ name, before, after: JSON.parse(JSON.stringify(col)) });
|
||||
}
|
||||
// Links省略厳禁(全置換のため既存値をそのまま含める)
|
||||
settings.Links = current.SiteSettings.Links || [];
|
||||
return { settings, changed };
|
||||
}
|
||||
|
||||
function buildUpdateBody(apiKey, current, desiredSettings) {
|
||||
return {
|
||||
ApiVersion: "1.1",
|
||||
ApiKey: apiKey,
|
||||
SiteId: SITE_ID,
|
||||
Title: current.Title,
|
||||
ReferenceType: current.ReferenceType,
|
||||
ParentId: current.ParentId,
|
||||
InheritPermission: current.InheritPermission,
|
||||
SiteSettings: desiredSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const execute = process.argv.includes("--execute");
|
||||
const server = loadServerConfig();
|
||||
|
||||
(async () => {
|
||||
const { current, siteDir, rawText } = loadLatest();
|
||||
const { settings: desiredSettings, changed } = buildDesiredSiteSettings(current);
|
||||
const body = buildUpdateBody(server.ApiKey, current, desiredSettings);
|
||||
|
||||
const modifyDir = newModifyDir(siteDir, REQUEST_LABEL);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
fs.writeFileSync(path.join(modifyDir, `before_site-${SITE_ID}_latest.json`), rawText, "utf-8");
|
||||
const updatedPath = path.join(modifyDir, `site-${SITE_ID}_updated_${timestamp}.json`);
|
||||
fs.writeFileSync(updatedPath, JSON.stringify(body, null, 2), "utf-8");
|
||||
|
||||
console.log(`\n==== SiteId ${SITE_ID} (${SITE_NAME}) ====`);
|
||||
console.log("変更内容:");
|
||||
for (const c of changed) {
|
||||
console.log(` - ${c.name}: ${JSON.stringify(c.before)} → ${JSON.stringify(c.after)}`);
|
||||
}
|
||||
console.log(`プレビュー保存先: ${updatedPath}`);
|
||||
|
||||
if (!execute) {
|
||||
console.log("\n(--execute 未指定のためプレビューのみ。送信していません)");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${server.BaseUrl}api/items/${SITE_ID}/updatesite`;
|
||||
console.log(`送信先: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log(`[結果] HTTP ${res.status}:`, JSON.stringify(json));
|
||||
const resultPath = path.join(modifyDir, `site-${SITE_ID}_result_${timestamp}.json`);
|
||||
fs.writeFileSync(resultPath, JSON.stringify(json, null, 2), "utf-8");
|
||||
})();
|
||||
@ -0,0 +1,133 @@
|
||||
// 健康診断管理 SiteId 513156(実施年度-集計): NumA~NumH(社員集計項目)をNum031~038へ置き換える
|
||||
// ワンショットスクリプト。設定(LabelText/Unit/TextAlign/NoWrap等)はそのまま維持し、ColumnNameのみ変更。
|
||||
// EditorColumnHash.General内の配置位置もNumA~H→Num031~038に置換。既存アイテム件数0件を確認済みのため
|
||||
// データ移行は行わない(ColumnNameはPleasanterの物理列名に直結するため、データがあれば別途移行が必要)。
|
||||
// updatesite(Mode:full)を使う。--execute指定時のみ実際に送信、指定なければプレビューのみ出力。
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { findSiteDir, newModifyDir } = require("../../site-paths.js");
|
||||
|
||||
const PROJECT_NAME = "健康診断管理";
|
||||
const ENV = "production";
|
||||
const SITE_ID = 513156;
|
||||
const SITE_NAME = "実施年度-集計";
|
||||
const REQUEST_LABEL = "rename-numah-to-num031-038";
|
||||
|
||||
// [旧ColumnName, 新ColumnName]
|
||||
const RENAME_PAIRS = [
|
||||
["NumA", "Num031"],
|
||||
["NumB", "Num032"],
|
||||
["NumC", "Num033"],
|
||||
["NumD", "Num034"],
|
||||
["NumE", "Num035"],
|
||||
["NumF", "Num036"],
|
||||
["NumG", "Num037"],
|
||||
["NumH", "Num038"],
|
||||
];
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..", "..");
|
||||
const PROJECT_ROOT = path.join(REPO_ROOT, PROJECT_NAME);
|
||||
const CONFIGS_DIR = path.join(PROJECT_ROOT, "configs", ENV);
|
||||
|
||||
function loadServerConfig() {
|
||||
const configPath = path.join(REPO_ROOT, `config_${ENV}.json`);
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
if (!config.BaseUrl || !config.ApiKey) {
|
||||
console.error(`[エラー] ${configPath} に BaseUrl / ApiKey を設定してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadLatest() {
|
||||
const siteDir = findSiteDir(CONFIGS_DIR, SITE_ID);
|
||||
if (!siteDir) {
|
||||
console.error(`[エラー] configs/${ENV}/site-${SITE_ID}_* が見つかりません。先にget-site-config.jsを実行してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const p = path.join(siteDir, "sitesettings", `site-${SITE_ID}_latest.json`);
|
||||
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
||||
return { current: raw.Response.Data, siteDir, rawText: fs.readFileSync(p, "utf-8") };
|
||||
}
|
||||
|
||||
function buildDesiredSiteSettings(current) {
|
||||
const settings = JSON.parse(JSON.stringify(current.SiteSettings));
|
||||
const renamedColumns = [];
|
||||
|
||||
for (const [oldName, newName] of RENAME_PAIRS) {
|
||||
const col = settings.Columns.find((c) => c.ColumnName === oldName);
|
||||
if (!col) {
|
||||
console.error(`[エラー] Columns内に${oldName}が見つかりません。`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (settings.Columns.some((c) => c.ColumnName === newName)) {
|
||||
console.error(`[エラー] Columns内に${newName}が既に存在します(重複防止のため中断)。`);
|
||||
process.exit(1);
|
||||
}
|
||||
col.ColumnName = newName;
|
||||
renamedColumns.push({ oldName, newName, label: col.LabelText });
|
||||
}
|
||||
|
||||
const general = settings.EditorColumnHash.General;
|
||||
const renameMap = new Map(RENAME_PAIRS);
|
||||
settings.EditorColumnHash.General = general.map((name) => renameMap.get(name) || name);
|
||||
|
||||
// Links省略厳禁(全置換のため既存値をそのまま含める)
|
||||
settings.Links = current.SiteSettings.Links || [];
|
||||
|
||||
return { settings, renamedColumns };
|
||||
}
|
||||
|
||||
function buildUpdateBody(apiKey, current, desiredSettings) {
|
||||
return {
|
||||
ApiVersion: "1.1",
|
||||
ApiKey: apiKey,
|
||||
SiteId: SITE_ID,
|
||||
Title: current.Title,
|
||||
ReferenceType: current.ReferenceType,
|
||||
ParentId: current.ParentId,
|
||||
InheritPermission: current.InheritPermission,
|
||||
SiteSettings: desiredSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const execute = process.argv.includes("--execute");
|
||||
const server = loadServerConfig();
|
||||
|
||||
(async () => {
|
||||
const { current, siteDir, rawText } = loadLatest();
|
||||
const { settings: desiredSettings, renamedColumns } = buildDesiredSiteSettings(current);
|
||||
const body = buildUpdateBody(server.ApiKey, current, desiredSettings);
|
||||
|
||||
const modifyDir = newModifyDir(siteDir, REQUEST_LABEL);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
fs.writeFileSync(path.join(modifyDir, `before_site-${SITE_ID}_latest.json`), rawText, "utf-8");
|
||||
const updatedPath = path.join(modifyDir, `site-${SITE_ID}_updated_${timestamp}.json`);
|
||||
fs.writeFileSync(updatedPath, JSON.stringify(body, null, 2), "utf-8");
|
||||
|
||||
console.log(`\n==== SiteId ${SITE_ID} (${SITE_NAME}) ====`);
|
||||
console.log("置き換え内容:");
|
||||
for (const c of renamedColumns) console.log(` - ${c.oldName} → ${c.newName} (${c.label})`);
|
||||
console.log("EditorColumnHash.General 変更後:");
|
||||
console.log(` [${desiredSettings.EditorColumnHash.General.join(", ")}]`);
|
||||
console.log(`プレビュー保存先: ${updatedPath}`);
|
||||
|
||||
if (!execute) {
|
||||
console.log("\n(--execute 未指定のためプレビューのみ。送信していません)");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${server.BaseUrl}api/items/${SITE_ID}/updatesite`;
|
||||
console.log(`送信先: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log(`[結果] HTTP ${res.status}:`, JSON.stringify(json));
|
||||
const resultPath = path.join(modifyDir, `site-${SITE_ID}_result_${timestamp}.json`);
|
||||
fs.writeFileSync(resultPath, JSON.stringify(json, null, 2), "utf-8");
|
||||
})();
|
||||
@ -0,0 +1,160 @@
|
||||
// 健康診断管理 SiteId 508971: タブ「診断結果」のNum061~071(検査項目)に
|
||||
// ラベル末尾" (実施人数)"を付与し、同じ設定(ControlType/NoWrap/Min/Max/Description)を
|
||||
// 複製したNum081~091(ラベル末尾" (有所見者数)")を新規追加、EditorColumnHash._Tab-1の並びを
|
||||
// Num061→Num081→Num062→Num082…のように交互配置へ組み替えるワンショットスクリプト。
|
||||
// updatesite(Mode:full)を使う。--execute指定時のみ実際に送信、指定なければプレビュー(送信Body・diff)のみ出力。
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { findSiteDir, newModifyDir } = require("../../site-paths.js");
|
||||
|
||||
const PROJECT_NAME = "健康診断管理";
|
||||
const ENV = "production";
|
||||
const SITE_ID = 508971;
|
||||
const SITE_NAME = "健康診断管理";
|
||||
const REQUEST_LABEL = "tab2-jisshi-arishoken-counts";
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..", "..");
|
||||
const PROJECT_ROOT = path.join(REPO_ROOT, PROJECT_NAME);
|
||||
const CONFIGS_DIR = path.join(PROJECT_ROOT, "configs", ENV);
|
||||
|
||||
// [実施人数側(既存), 有所見者数側(新規)] のペア。左の設定をそのまま右へ複製する
|
||||
const PAIRS = [
|
||||
["Num061", "Num081"],
|
||||
["Num062", "Num082"],
|
||||
["Num063", "Num083"],
|
||||
["Num064", "Num084"],
|
||||
["Num065", "Num085"],
|
||||
["Num066", "Num086"],
|
||||
["Num067", "Num087"],
|
||||
["Num068", "Num088"],
|
||||
["Num069", "Num089"],
|
||||
["Num070", "Num090"],
|
||||
["Num071", "Num091"],
|
||||
];
|
||||
|
||||
function loadServerConfig() {
|
||||
const configPath = path.join(REPO_ROOT, `config_${ENV}.json`);
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
if (!config.BaseUrl || !config.ApiKey) {
|
||||
console.error(`[エラー] ${configPath} に BaseUrl / ApiKey を設定してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadLatest() {
|
||||
const siteDir = findSiteDir(CONFIGS_DIR, SITE_ID);
|
||||
if (!siteDir) {
|
||||
console.error(`[エラー] configs/${ENV}/site-${SITE_ID}_* が見つかりません。先にget-site-config.jsを実行してください。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const p = path.join(siteDir, "sitesettings", `site-${SITE_ID}_latest.json`);
|
||||
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
||||
return { current: raw.Response.Data, siteDir, rawText: fs.readFileSync(p, "utf-8") };
|
||||
}
|
||||
|
||||
function buildDesiredSiteSettings(current) {
|
||||
const settings = JSON.parse(JSON.stringify(current.SiteSettings));
|
||||
const columns = settings.Columns;
|
||||
|
||||
const addedColumns = [];
|
||||
for (const [srcName, newName] of PAIRS) {
|
||||
const src = columns.find((c) => c.ColumnName === srcName);
|
||||
if (!src) {
|
||||
console.error(`[エラー] Columns内に${srcName}が見つかりません。`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (columns.some((c) => c.ColumnName === newName)) {
|
||||
console.error(`[エラー] Columns内に${newName}が既に存在します(重複追加防止のため中断)。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const originalLabel = src.LabelText;
|
||||
src.LabelText = `${originalLabel} (実施人数)`;
|
||||
|
||||
const newCol = JSON.parse(JSON.stringify(src));
|
||||
newCol.ColumnName = newName;
|
||||
newCol.LabelText = `${originalLabel} (有所見者数)`;
|
||||
columns.push(newCol);
|
||||
addedColumns.push(newCol);
|
||||
}
|
||||
|
||||
// EditorColumnHash._Tab-1 の並びをNum061→Num081→Num062→Num082…と交互に組み替える
|
||||
const tab1 = settings.EditorColumnHash["_Tab-1"];
|
||||
const pairSrcSet = new Set(PAIRS.map(([src]) => src));
|
||||
const rebuilt = [];
|
||||
for (const name of tab1) {
|
||||
rebuilt.push(name);
|
||||
if (pairSrcSet.has(name)) {
|
||||
const pair = PAIRS.find(([src]) => src === name);
|
||||
rebuilt.push(pair[1]);
|
||||
}
|
||||
}
|
||||
settings.EditorColumnHash["_Tab-1"] = rebuilt;
|
||||
|
||||
// Links省略厳禁(全置換のため既存値をそのまま含める)
|
||||
settings.Links = current.SiteSettings.Links || [];
|
||||
|
||||
return { settings, addedColumns, rebuiltTab1: rebuilt };
|
||||
}
|
||||
|
||||
function buildUpdateBody(apiKey, current, desiredSettings) {
|
||||
return {
|
||||
ApiVersion: "1.1",
|
||||
ApiKey: apiKey,
|
||||
SiteId: SITE_ID,
|
||||
Title: current.Title,
|
||||
ReferenceType: current.ReferenceType,
|
||||
ParentId: current.ParentId,
|
||||
InheritPermission: current.InheritPermission,
|
||||
SiteSettings: desiredSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const execute = process.argv.includes("--execute");
|
||||
const server = loadServerConfig();
|
||||
|
||||
(async () => {
|
||||
const { current, siteDir, rawText } = loadLatest();
|
||||
const { settings: desiredSettings, addedColumns, rebuiltTab1 } = buildDesiredSiteSettings(current);
|
||||
const body = buildUpdateBody(server.ApiKey, current, desiredSettings);
|
||||
|
||||
const modifyDir = newModifyDir(siteDir, REQUEST_LABEL);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
fs.writeFileSync(path.join(modifyDir, `before_site-${SITE_ID}_latest.json`), rawText, "utf-8");
|
||||
const updatedPath = path.join(modifyDir, `site-${SITE_ID}_updated_${timestamp}.json`);
|
||||
fs.writeFileSync(updatedPath, JSON.stringify(body, null, 2), "utf-8");
|
||||
|
||||
console.log(`\n==== SiteId ${SITE_ID} (${SITE_NAME}) ====`);
|
||||
console.log("ラベル変更(実施人数):");
|
||||
for (const [srcName] of PAIRS) {
|
||||
const c = desiredSettings.Columns.find((x) => x.ColumnName === srcName);
|
||||
console.log(` - ${srcName}: ${c.LabelText}`);
|
||||
}
|
||||
console.log("新規追加(有所見者数):");
|
||||
for (const c of addedColumns) {
|
||||
console.log(` - ${c.ColumnName}: ${c.LabelText}`);
|
||||
}
|
||||
console.log("EditorColumnHash._Tab-1 変更後の並び:");
|
||||
console.log(` [${rebuiltTab1.join(", ")}]`);
|
||||
console.log(`\nColumns件数: 変更前${current.SiteSettings.Columns.length} → 変更後${desiredSettings.Columns.length}`);
|
||||
console.log(`プレビュー保存先: ${updatedPath}`);
|
||||
|
||||
if (!execute) {
|
||||
console.log("\n(--execute 未指定のためプレビューのみ。送信していません)");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${server.BaseUrl}api/items/${SITE_ID}/updatesite`;
|
||||
console.log(`送信先: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log(`[結果] HTTP ${res.status}:`, JSON.stringify(json));
|
||||
const resultPath = path.join(modifyDir, `site-${SITE_ID}_result_${timestamp}.json`);
|
||||
fs.writeFileSync(resultPath, JSON.stringify(json, null, 2), "utf-8");
|
||||
})();
|
||||
146
Pleasanter/健康診断管理/docs/n8n/design.md
Normal file
146
Pleasanter/健康診断管理/docs/n8n/design.md
Normal file
@ -0,0 +1,146 @@
|
||||
# 健康診断管理×LINEWORKS Bot連携 n8n化 設計書
|
||||
|
||||
- 作成日: 2026-09-05
|
||||
- 対象: プリザンター「健康診断管理」プロジェクト(SiteId 508971)へLINEWORKS Bot経由の対話型ステータス管理機能を実装
|
||||
- 位置付け: 既存Express実装(`OldCode/express/modules/lineworksSurvey.js`、仕様書`Pleasanter/LINEWORKSアンケート管理/docs/lineworks-survey-scheme.md`)とは別物としてn8nで新規構築。単純な順次質問アンケートではなく、**プリザンターのProcess機能をそのままフロー定義として使うステータス駆動型対話ボット**
|
||||
|
||||
## 1. 背景・既存Express実装との違い
|
||||
|
||||
Express版(ケアセブンプロジェクト向け)は「アンケート開始→設問を順番に送信→全問回答で1レコードcreate」という一方向の順次アンケートだった。今回の健康診断管理版は要件が異なる。
|
||||
|
||||
- 508971は1レコード=1回の健診。フローは「日程通知→了承/変更→受診確認→結果受取り」のようにレコードのStatusを段階的に進める対話であり、設問の連続ではない
|
||||
- LINE WORKS Bot APIの制約上、**「トークを開いた瞬間」を検知するイベントは存在しない**(コールバックイベントは`message`/`join`/`leave`/`joined`/`left`/`postback`のみ)。そのため「初期表示切替」は、①Statusが変わった瞬間にBotから能動的にメッセージを送る(プッシュ型)、②ユーザーが何か送信したら現在Statusの案内を返す(フォールバック)、の組み合わせで実現する
|
||||
- アンケート実行管理・Bot・対象者マスタは新設せず、既存資産を流用する(後述)
|
||||
- **フロー定義(Statusごとの案内文・選択肢・遷移先)を管理する専用マスタは新設しない。508971自体の`SiteSettings.Processes`(プロセス機能)をそのままフロー定義として使う**
|
||||
- サーバー実装はExpressでなくn8n。会話の待機状態はn8nプロセスのメモリではなく、n8n Data Table(新規`bot_conversation_state`)で保持する
|
||||
|
||||
n8n環境自体の詳細(URL・API・既存ワークフロー・過去の罠)は`NodeSrv/apps/n8n/docs/n8n-guide.md`参照。以下の設計はこのガイドの制約(コンテナメモリ768MB、Data Table操作の罠、Schedule Trigger運用方針等)を踏まえている。
|
||||
|
||||
## 2. 前提とした既存資産の流用
|
||||
|
||||
| 用途 | 流用元 |
|
||||
|---|---|
|
||||
| Bot | 既存「LINEWORKSアンケート管理」プロジェクトのSiteId 484184(LINEWORKS Bot管理)をそのまま参照。健康診断管理側にBotマスタは新設しない |
|
||||
| 対象者解決 | プリザンター標準Usersのメールアドレスを、そのままLINEWORKS宛先ID(userId)として使う。LINE WORKS側からの受信時も`source.userId`=メールアドレスという前提で扱う(専用マッピングマスタは不要) |
|
||||
| 対象レコード特定 | LINEWORKSから届いたメッセージの送信者メール→508971の中で、そのユーザーが紐づくレコードのうち**未完了(Status 900/910以外)の最新レコード**を対象とする。複数該当時の自動判定は行わず、実装上は「複数ヒット」を異常系として扱う |
|
||||
|
||||
対象者解決は基本的にプリザンター標準Usersのメールアドレスだけで足りる想定。データ品質の裏付けチェック・補完に社員・組織マスタ管理テーブル(SiteId 504412)を使う件は8章参照(Bot対話フローとは独立した補助機能)。
|
||||
|
||||
## 3. 全体アーキテクチャ
|
||||
|
||||
```
|
||||
[プリザンター508971] Statusが変わる契機は2種類
|
||||
(a) 担当者がProcessボタンを押す(例:①日程通知発行、日程確定時)
|
||||
(b) Bot対話の結果としてProcessが実行される(例:②→③)
|
||||
|
||||
(a)の場合
|
||||
▼
|
||||
n8n: WP「Statusプッシュ通知」(Webhook, 508971クライアントスクリプトから起動)
|
||||
└ resultId・processIdを受け取りレコード取得
|
||||
└ 該当Processの案内文(ツールチップ欄)をレコード値で置換
|
||||
└ 対象者メール解決 → LINEWORKS Bot APIで案内+選択肢を送信
|
||||
└ n8n Data Table「bot_conversation_state」に提示内容を記録
|
||||
▼
|
||||
LINEWORKS トーク
|
||||
│ ユーザー返信(ボタン押下/テキスト/ファイル)
|
||||
▼
|
||||
n8n: WA「LINEWORKS応答受信」(Webhook、唯一の受信口、署名検証)
|
||||
└ 送信者メール解決 → 508971の対象レコード特定
|
||||
└ bot_conversation_stateの待機状態を見て回答を処理
|
||||
└ 該当Processを`ProcessId`指定でapi/items/updateへ実行 → Status遷移
|
||||
└ 遷移後の新Statusに紐づく次のProcess群を取得し、次の案内を送信(WPと共通ロジック)
|
||||
```
|
||||
|
||||
## 4. フロー定義: 508971のProcess機能をそのまま流用
|
||||
|
||||
新規マスタサイト・新規テーブルは作らない。プリザンター標準の「プロセス」設定画面(画面種別・現在の状況・変更後の状況・表示名・ツールチップ・入力検証タブ等)を、Bot対話の定義としてそのまま使う。
|
||||
|
||||
| Processの項目 | Bot連携での役割 |
|
||||
|---|---|
|
||||
| 現在の状況(CurrentStatus)/変更後の状況(ChangedStatus) | 既存のStatus遷移定義をそのまま使う |
|
||||
| 表示名(DisplayName) | Botが提示する選択肢ボタンのラベル |
|
||||
| ツールチップ | Bot案内文言。`{検査機関}` `{日程}`のようなプレースホルダーを書いておくと、n8nがレコードの実際の値に置換してから送信する |
|
||||
| 入力検証タブの「項目」 | このProcess実行に追加入力を伴うかの判定に流用。列名プレフィックスで種別を判定する(`Date*`→日付入力を1往復挟む、`Attachments*`→ファイル受信を1往復挟む、項目なし→即実行) |
|
||||
| ProcessId | n8nが`POST /api/items/{resultId}/update`に`ProcessId`パラメータとして渡して実行する。公式マニュアル記載の通り、APIからのプロセス実行では入力検証(プリザンター標準の検証機能)は適用されない点に注意 |
|
||||
|
||||
新フローを追加する際の運用手順:
|
||||
1. 508971のProcessを1つ追加(現在の状況・変更後の状況・表示名・ツールチップ文言を設定)
|
||||
2. 追加入力が必要なら、入力検証タブの「項目」に対象列(`Date*`または`Attachments*`)を登録
|
||||
3. n8nワークフロー(WP/WA)は無改修。現在Statusに紐づくProcess一覧を都度`getsite`から動的に取得する設計のため、Process追加だけで新フローを反映できる
|
||||
|
||||
## 5. n8n側詳細
|
||||
|
||||
### Data Table「bot_conversation_state」(新規)
|
||||
|
||||
| 列 | 内容 |
|
||||
|---|---|
|
||||
| resultId | 508971のResultId |
|
||||
| targetEmail | 対象者メールアドレス(LINEWORKS userId) |
|
||||
| currentStatus | 直近提示時点のStatus値 |
|
||||
| pendingProcesses | 直近提示した選択肢一覧(`[{processId, label, validateColumn}]`のJSON文字列) |
|
||||
| awaitInput | `none`(選択肢待ち)/`date`(日付入力待ち)/`file`(ファイル受信待ち) |
|
||||
| awaitProcessId | 追加入力完了後に実行すべきProcessId(`awaitInput`が`date`/`file`の間のみ使用) |
|
||||
| awaitColumn | 追加入力先の列名(例: `Date001`) |
|
||||
| updatedAt | 最終更新日時 |
|
||||
|
||||
n8n-guide.md 7-1(Clear出力の握り潰し)・7-2(複数行が後続へそのまま渡ると行数分繰り返し実行される)の罠を踏まえ、1レコード=1行の読み書きに限定し、複数行を横断する集約処理は入れない。セッションタイムアウトの概念は持たない(ユーザーの都合のいいタイミングで返信されればよい性質のフローのため、待機状態は無期限に保持する)。
|
||||
|
||||
### ワークフロー構成
|
||||
|
||||
- **WP Statusプッシュ通知**: Webhookトリガー(`X-API-Key`ヘッダー認証、508971のクライアントスクリプト専用)。担当者がProcessボタンを押した時に起動
|
||||
- **WA LINEWORKS応答受信**: Webhookトリガー(LINE WORKS本体からの直接コールバック、`x-works-signature`をHMAC-SHA256検証)。唯一の受信口
|
||||
- タイムアウト監視ワークフローは持たない(3節参照)
|
||||
|
||||
### WAの処理詳細
|
||||
|
||||
1. 署名検証 → 送信内容(テキスト/ボタン応答/ファイル)を判定
|
||||
2. 送信者メール解決 → 508971の対象レコード(未完了の最新1件)を特定
|
||||
3. `bot_conversation_state`から該当resultIdの待機状態を取得
|
||||
4. **待機状態なし、または`awaitInput=none`で回答が選択肢と不一致** → 現在Statusに紐づくProcess群を`getsite`から再取得し、案内を再送(フォールバック)
|
||||
5. **`awaitInput=none`で回答が選択肢(DisplayName)と一致**:
|
||||
- 対象列なし → 即座に`ProcessId`実行 → 新Statusの次Process群を取得 → 次の案内を送信
|
||||
- 対象列が`Date*` → `awaitInput=date`/`awaitProcessId`/`awaitColumn`を記録し、「日付を入力してください」を追加送信(Process実行は保留)
|
||||
- 対象列が`Attachments*` → 同様に`awaitInput=file`で保留し、ファイル送信を促す
|
||||
6. **`awaitInput=date`** → 受信テキストを日付として検証(Express版の和暦・月日省略対応ロジックを踏襲)→ `awaitColumn`をupdate → `awaitProcessId`を`ProcessId`実行 → 新Status提示 → 待機状態を`none`へ戻す
|
||||
7. **`awaitInput=file`** → 受信がファイルでなければ再送要求。ファイルならLINEWORKS Bot APIでダウンロード → Pleasanter添付ファイルAPIで`awaitColumn`へアップロード → `awaitProcessId`を`ProcessId`実行 → 新Status提示 → 待機状態を`none`へ戻す
|
||||
|
||||
### 認証まわり
|
||||
|
||||
- LINEWORKS Bot APIメッセージ送信: 既存Credential「LINEWORKS Bot Private Key (v4)」(`Hw0qlEaGfLPnQWp1`)が流用できるか、BotIdとの対応関係を実装時に確認する
|
||||
- 対象者メール解決: `POST api/users/get`(`View:{ApiGetMailAddresses:true}`)。org-master-sync③で実績のある実装パターンを流用
|
||||
|
||||
## 6. エラーハンドリング・異常系
|
||||
|
||||
- 対象レコードが複数ヒット(同一ユーザーの未完了レコードが2件以上)→ 自動判定せず、担当者確認が必要な異常系として扱う(Bot応答は保留し、通知等は今後の実装計画で検討)
|
||||
- 選択肢に一致しない回答 → 選択肢を再提示(4節のフォールバックと同じ経路)
|
||||
- 日付形式不正/ファイル未送信 → エラーメッセージ+再送、待機状態は維持
|
||||
- LINEWORKS送信失敗・508971 update失敗 → n8n Execution History(Postgres保存)に残す。追加のログ実装はしない
|
||||
|
||||
## 7. 導入・検証方針
|
||||
|
||||
- n8n運用ルール(n8n-guide.md 9章)に従い、ワークフローの構築・編集自体は確認不要。**Webhook実行によるプリザンター書き込み・LINEWORKS送信を伴うテストは都度事前確認**
|
||||
- 初回検証は1Process・1件のみで実施し、疎通確認後にフロー全体(①〜③)の通し検証に進める
|
||||
- 508971は本番の健診データそのものなので、検証は既存レコードを壊さない捨てレコードを用意して行う
|
||||
|
||||
## 8. 補助機能(Bot対話フローとは独立): 社員マスタ(504412)によるデータ品質チェック・補完
|
||||
|
||||
3〜7章のBot対話フローとは関係のない、508971のデータ品質を担保するための補助機能。n8nのHC-SUB/HC-WP/HC-WAワークフローには組み込まず、別途の仕組み(バッチ・Pleasanter Process/ServerScript等、実装方式は別途検討)として扱う。
|
||||
|
||||
社員・組織マスタ管理テーブル(SiteId 504412、2章参照)の列構成(`NodeSrv/apps/org-master-sync/configs/site-504412_社員・組織マスタ管理テーブル/`で取得済み):
|
||||
|
||||
| 504412の列 | 内容 |
|
||||
|---|---|
|
||||
| `Class011` | ユーザID(PleasanterUserId) |
|
||||
| `ClassB` | メールアドレス |
|
||||
| `Class036` | PLメールアドレス |
|
||||
| `Class003` / `Class004` | 姓(カナ)/名(カナ) |
|
||||
|
||||
- **メールアドレス整合性チェック**: 508971の`ClassC`(Users参照)から解決した対象者のPleasanterUserIdを軸に504412の`Class011`と突き合わせ、その社員の`Class036`(PLメールアドレス)と、`api/users/get`で解決した実際のメールアドレスが一致するかを検証する
|
||||
- **フリガナ補完**: 508971の`ClassD`(フリガナ、必須項目)が空欄の場合、504412の`Class003`(姓(カナ))+`Class004`(名(カナ))から補完する
|
||||
|
||||
## 9. 未確定事項(実装時に個別確定)
|
||||
|
||||
- 具体的なProcess定義(①日程通知〜③検査結果受取りの各Process内容、Status値の追加・修正)は508971のStatus設計がまだ未完成のため、実装着手時に個別に設計する
|
||||
- LINEWORKS Bot Private Key CredentialとBotId(484184側)の対応関係の実機確認
|
||||
- LINEWORKS Bot APIでのファイル受信(ダウンロードURL取得)とPleasanter添付ファイルAPIへのアップロードの具体的な実装方法
|
||||
- 今後フロー内容の詳細が追加判明する前提のため、4節の枠組み(Process流用)の汎用性を保ったまま個別Processを増やしていく
|
||||
1474
Pleasanter/健康診断管理/docs/n8n/plan.md
Normal file
1474
Pleasanter/健康診断管理/docs/n8n/plan.md
Normal file
File diff suppressed because it is too large
Load Diff
101
Pleasanter/健康診断管理/docs/n8n/workflows-status.md
Normal file
101
Pleasanter/健康診断管理/docs/n8n/workflows-status.md
Normal file
@ -0,0 +1,101 @@
|
||||
# healthcheck-survey-bot
|
||||
|
||||
健康診断管理(SiteId 508971)×LINEWORKS Bot連携。508971の`SiteSettings.Processes`
|
||||
をフロー定義として使い、n8n上でBot対話型のStatus管理を行う。
|
||||
|
||||
設計書: `NodeSrv/docs/superpowers/specs/2026-09-05-healthcheck-lineworks-survey-n8n-design.md`
|
||||
実装計画: `NodeSrv/docs/superpowers/plans/2026-09-05-healthcheck-lineworks-survey-n8n.md`
|
||||
|
||||
## 実機調査メモ(2026-09-05確認)
|
||||
|
||||
508971への新規テストProcess追加なしで、既存本番プロジェクト「実行予算WF申請」(SiteId 376872)の
|
||||
取得済み`processes.json`(`Pleasanter/実行予算WF申請/configs/production/site-376872_実行予算WF申請/processes.json`)
|
||||
に入力検証タブを使ったProcessの実例があり、そこからJSON構造を確認できた。
|
||||
|
||||
Processの入力検証タブは`SiteSettings.Processes[].ValidateInputs`配列として保存される。
|
||||
各要素は`{"Id": number, "ColumnName": string, "Required": boolean}`(実例: `{"Id": 1, "ColumnName": "Class021", "Required": true}`)。
|
||||
配列名は`Validations`ではなく`ValidateInputs`。値を設定していない項目(クライアント/サーバ正規表現、エラーメッセージ、最小/最大等)は
|
||||
キー自体が省略される可能性が高い(今回の実例では未設定のため確認できていない)。
|
||||
|
||||
Task 5(processFlow.js)の`getValidationColumnNames`はこの構造(`process.ValidateInputs[].ColumnName`)を前提に実装する。
|
||||
|
||||
## n8nリソースID一覧
|
||||
|
||||
(Task 8〜11完了後、ここに作成したData Table ID・ワークフローIDを記録する)
|
||||
|
||||
- Data Table `healthcheck_bot_state`: `jqMDa2YZTI4f0iQ7`
|
||||
- projectId: `LOcxF69Gm4PvnkqA`(`org-master-sync`用プロジェクトを流用)
|
||||
- 列: `resultId`, `targetEmail`, `currentStatus`, `pendingProcesses`, `awaitInput`, `awaitProcessId`, `awaitColumn`(すべてstring型)
|
||||
- 注記: 設計上の8列目`updatedAt`はn8n Data Tableのシステム予約列名(`POST /data-tables`が`400 Column name "updatedAt" is reserved as a system column name.`を返す)のため、列定義には含めていない。各行の更新日時はn8nが自動管理する`updatedAt`メタデータで代替する。
|
||||
|
||||
- ワークフロー `HC-SUB: プロセス実行と案内送信`: `XRqcykbG2LuAjGG2`
|
||||
- ファイル: `workflows/hc-sub-run-process-and-notify.json`(Execute Workflow Trigger、入力`{resultId, processId}`)
|
||||
- **プロジェクト全体のn8nワークフロー構築ルール(2026-09-05改訂)に合わせて7ノード構成へ全面リビルド。旧19ノード版(個別キーごとのData Table Getノード×7、HTTP Requestノード×6を並べた構成)を置き換え、同じワークフローID`XRqcykbG2LuAjGG2`へPUTで上書き済み(2026-09-05、コントローラー実施、HTTP 200)。**
|
||||
- active化済み(Execute Workflow Triggerサブワークフローは呼び出し元から実行可能にするためactive必須、`asKvC5LSVg3q8tuU`と同じ運用)
|
||||
- ノード構成(7ノード、リニア接続):
|
||||
1. `Execute Workflow Trigger`(入力: `resultId`, `processId`)
|
||||
2. `Data Table`「設定値一括取得」: `workflow_config_values`(`bNkadTyDgDepYx2p`)を`filters`無し・`returnAll:true`で一括取得(個別キーごとのGetノードを廃止)
|
||||
3. `Code`「Pleasanter照会・選択肢組み立て」: 設定値のMap化、(任意)Process実行、レコード取得、サイト設定取得(getsite)、Botマスタ取得(484184)、対象者メール解決、選択肢組み立てまでを1ノードに集約。Pleasanter API呼び出しはすべて`this.helpers.httpRequest`(n8n Codeノード公式ヘルパー)で発行
|
||||
4. `Code`「JWTクレーム組み立て」: JWTクレームJSON文字列の組み立てとLINEWORKS `button_template`のactions配列組み立てを1ノードに集約
|
||||
5. `JWT`ノード「Sign JWT」(`operation: sign`, `algorithm: RS256`, Credential: 「LINEWORKS Bot Private Key (v4)」id `Hw0qlEaGfLPnQWp1`)
|
||||
6. `Code`「アクセストークン取得・LINEWORKS送信」: LINEWORKSアクセストークン取得とメッセージ送信を1ノードに集約(`this.helpers.httpRequest`)
|
||||
7. `Data Table`「状態更新」: `healthcheck_bot_state`(`jqMDa2YZTI4f0iQ7`)へ`insert`
|
||||
- 秘密情報・SiteId等はすべて`workflow_config_values`(`bNkadTyDgDepYx2p`)から「設定値一括取得」1ノードで取得し、直後のCodeノードで`configKey`→`configValue`のMapに変換して参照。ワークフローJSONに秘密値のハードコードなし
|
||||
- Execute Workflow Triggerの入力(`resultId`/`processId`)は、後続ノードの直接の入力でない箇所では必ず`$('Execute Workflow Trigger').item.json...`のようにノード名を明示して参照している(旧版にあった「Data Tableノード通過後に裸の`$json`を参照してしまうバグ」の再発防止)
|
||||
- 未検証・要判断の暫定実装あり(Task 12以降で要確認):
|
||||
①484184(`LINEWORKS_BOT_MASTER_SITE_ID`)からのBotId解決ロジック(実データ構造未調査のため`SiteSettings.BotIds`または`Processes[].BotId`を仮定し、無ければ例外を投げる。「JWTクレーム組み立て」ノードに実装)
|
||||
②`/api/users/get`のレスポンス形状(配列/単一オブジェクト両対応の防御的実装、「Pleasanter照会・選択肢組み立て」ノードに実装)
|
||||
③LINEWORKSトークン取得は`URLSearchParams`インスタンスをそのまま`body`に渡す形に修正済み(`.toString()`化や`json:true`併用はn8nのform-urlencoded自動処理と衝突する懸念があったため回避)。レスポンスは文字列/オブジェクトいずれでも`access_token`を読めるよう防御的にパースしている。ただしn8n本体での実機動作は本タスクでは未検証
|
||||
④`Data Table`Insertノードの`columns.mappingMode`/`value`/`schema`の形状(旧19ノード版から流用。旧版はデプロイ実績があるためスキーマ自体は疑わしくないが、実書き込みでの動作は本タスクでは未再検証)
|
||||
- テスト実行(Step 4相当)は本タスクでは未実施。次回、捨てレコードでの疎通確認とユーザー確認が必要
|
||||
|
||||
- ワークフロー `HC-WP: Statusプッシュ通知`: `swXfpoDtwZDT3Hwk`
|
||||
- ファイル: `workflows/hc-wp-status-push.json`
|
||||
- Webhook: `https://n8n32.next-hd.net/webhook/healthcheck-status-push`(`X-Api-Key`ヘッダー認証、body `{resultId, processId}`)
|
||||
- active化済み
|
||||
- `X-Api-Key`は`workflow_config_values`の`HEALTHCHECK_WP_API_KEY`と照合。秘密値のハードコードなし
|
||||
- 実際のcurl疎通テスト(本番508971書き込み・LINEWORKS実送信を伴う)は未実施。ユーザー確認後に実施する
|
||||
- デプロイ・活性化はサブエージェントのBashサンドボックスが実APIキー使用を一律ブロックしたためコントローラーが直接実行した(詳細: `task-10-report.md`)。設定値取得もfilter付き単一キーGetから一括取得+Codeフィルタへ変更済み
|
||||
|
||||
- ワークフロー `HC-WA: LINEWORKS応答受信`: `0i0Ze3Gq0Waof3Fa`
|
||||
- ファイル: `workflows/hc-wa-lineworks-response.json`
|
||||
- Webhook: `https://n8n32.next-hd.net/webhook/healthcheck-lineworks-response`(LINE WORKS本体からの直接コールバック、`x-works-signature`検証)
|
||||
- active化済み(2026-09-05、コントローラーが新規デプロイ・activate、HTTP 200)
|
||||
- **プロジェクト全体のn8nワークフロー構築ルール(2026-09-05改訂)に合わせて14ノード構成で全面リビルド。旧60ノード版(個別キーごとのData Table/HTTP Requestノードを積み上げ、awaitInput分岐をSwitch+各分岐に案内送信一式を重複配置した構成)を置き換えた。**IF・JWT・Execute Workflow・Data Tableは各ルールに従い専用ノードのままだが、それらは機械的に必要な最小限(IF×2, JWT×1, Execute Workflow×1, Data Table×3)であり、本来Codeノードに集約可能な分岐ロジック・Pleasanter/LINEWORKS API呼び出しはすべて4つのCodeノードに集約した(ブリーフの目安「11ノード」に対し実際は14ノードだが、これはIF/JWT/Execute Workflowを専用ノードに保つというルール自体が要求する固定オーバーヘッドであり、60ノードからの削減という趣旨は達成している)
|
||||
- LINEWORKS Developer Console側のBot Callback URL設定・疎通テストは未実施。ユーザー確認後に実施する
|
||||
- ノード構成(14ノード):
|
||||
1. `Webhook`(`healthcheck-lineworks-response`, `rawBody:true`)
|
||||
2. `Data Table`「設定値一括取得」: `workflow_config_values`を`filters`無し・`returnAll:true`で一括取得
|
||||
3. `Code`「署名検証・対象レコード特定」: HMAC-SHA256署名検証、対象者メール→PleasanterUserId解決、508971の対象レコード検索(`ColumnFilterHash`)までを1ノードに集約。0件/複数件はthrow(自動判定しない)
|
||||
4. `Data Table`「待機状態取得」: `healthcheck_bot_state`から該当resultIdの行を取得
|
||||
5. `Code`「分岐処理・アクション決定」: `awaitInput`(none/date/file、または待機状態自体が無い場合)に応じた分岐ロジック全体を1ノードに集約。`action`(execute/send/error)と次の待機状態(`nextAwaitInput`/`nextAwaitProcessId`/`nextAwaitColumn`)を1つのJSONオブジェクトとして出力する
|
||||
6. `Data Table`「状態更新」: 5の出力を使い`healthcheck_bot_state`を無条件で`update`
|
||||
7. `IF`「action==execute」: true→8、false→9
|
||||
8. `Execute Workflow`「HC-SUB実行」(Task 9, `XRqcykbG2LuAjGG2`)→13
|
||||
9. `IF`「action==send」: true→10、false→14
|
||||
10. `Code`「JWTクレーム組み立て」: 484184からのBotId解決とJWTクレーム・LINEWORKSメッセージ内容の組み立てを1ノードに集約
|
||||
11. `JWT`「Sign JWT」(`operation:sign`, `RS256`, Credential: 「LINEWORKS Bot Private Key (v4)」`Hw0qlEaGfLPnQWp1`)
|
||||
12. `Code`「アクセストークン取得・LINEWORKS送信」: LINEWORKSアクセストークン取得とメッセージ送信を1ノードに集約→13
|
||||
13. `Respond to Webhook(成功)`: 8と12の両方から接続される共有の成功応答ノード
|
||||
14. `Respond to Webhook(異常系)`: 9のfalse分岐(action=="error")専用
|
||||
- **分岐ノードの出力の流れ方(設計上の重要ポイント)**: ノード5は「今回どうするか(action/processId/messageText)」と「次に書き込む待機状態(next*)」を1つの出力オブジェクトにまとめて返す。ノード6(状態更新)はノード5の直接の後続ノードなのでそのまま`$json.next*`を参照して無条件更新するが、ノード6通過後の`$json`はData Table Updateの返り値(更新後の行データ)に置き換わり、ノード5の`action`/`processId`/`messageText`等は失われる。そのため、ノード7以降(IF・Execute Workflow・JWTクレーム組み立て・Respond to Webhook(異常系))はすべて裸の`$json`ではなく`$('分岐処理・アクション決定').item.json...`とノード名を明示して参照している(HC-SUBの`$('Execute Workflow Trigger').item.json`と同じ規約)
|
||||
- 秘密情報・SiteId等はすべて`workflow_config_values`(`bNkadTyDgDepYx2p`)から「設定値一括取得」1ノードで取得し、直後のCodeノードで`configKey`→`configValue`のMapに変換して参照。ワークフローJSONに秘密値のハードコードなし
|
||||
- 未検証・要判断の暫定実装・既知の未実装あり(詳細は`task-11-rebuild-report.md`):
|
||||
①Data Table `update`操作のパラメータ形状(get/insertは実機確認済みだが、updateは本タスクで初採用のため未検証、ブリーフ記載のサンプル形状をそのまま採用)
|
||||
②`/api/users/get`・`api/items/{SiteId}/get`(ColumnFilterHash検索)のレスポンス形状はHC-SUB・旧WA版と同じ想定を踏襲
|
||||
③484184(`LINEWORKS_BOT_MASTER_SITE_ID`)からのBotId解決ロジックはHC-SUBと同じ暫定実装(`SiteSettings.BotIds`または`Processes[].BotId`を仮定)
|
||||
④**LINEWORKSファイル添付(`awaitInput:"file"`分岐)の実処理は未実装。** ファイルダウンロード(`GET /v1.0/bots/{botId}/attachments/{fileId}`)にはLINEWORKS Bot APIのアクセストークンが必要だが、JWT署名は専用のJWTノード(ノード11、action=="send"経路にのみ存在しノード5より後段)でしか行えないため、ノード5内ではトークンを取得する手段がない。現状はこのケースを検出すると`action:"send"`でユーザーに「準備中」メッセージを返し、待機状態は`file`のまま維持する(ユーザーが詰まらないための安全側実装)。実装するにはグラフ構成の見直し(ファイル受信専用のJWT/Tokenペアを追加する等)が必要で、フォローアップタスクで対応すること
|
||||
- 設計上の注記: HC-SUB(Task 9)が`healthcheck_bot_state`に保存する`pendingProcesses`は`{processId, label, tooltip}`のみで`ValidateInputs`を持たないため、none分岐での`classifyAwaitInput`実行時はHC-WA内で`サイト設定取得`(getsite)を再実行し、ProcessId一致でフルのProcess定義を引き直す設計にした(Task 9側のファイルは変更していない)
|
||||
|
||||
## n8nフォルダ
|
||||
|
||||
- フォルダ `健康診断ワークフロー`: `hOx8WKrdOucllbcU`(projectId `LOcxF69Gm4PvnkqA`配下に2026-09-05作成)
|
||||
- HC-SUB/HC-WP/HC-WAの3ワークフローをこのフォルダへ移動する予定だが、Public APIの`/workflows/{id}/transfer`は別プロジェクトへの移動専用(同一プロジェクト内移動は拒否される)で、実行するとactive状態が解除される副作用があるため、**ユーザーがn8n UI上で手動ドラッグして移動する**運用にした(コントローラーからは未実施)
|
||||
|
||||
## workflow_config_values 追加登録キー(2026-09-05、healthcheck-survey-bot用)
|
||||
|
||||
既存の`workflow_config_values`(`bNkadTyDgDepYx2p`、org-master-sync用と共用)に以下を追加登録済み:
|
||||
- `LW_BOT_CLIENT_ID` / `LW_BOT_CLIENT_SECRET` / `LW_BOT_SERVICE_ACCOUNT`(LINEWORKS通知送信ワークフローと同じBot認証情報)
|
||||
- `HEALTHCHECK_SITE_ID`(508971)/ `LINEWORKS_BOT_MASTER_SITE_ID`(484184)/ `HEALTHCHECK_DATA_TABLE_ID`(`jqMDa2YZTI4f0iQ7`)
|
||||
- `HEALTHCHECK_WP_API_KEY`(HC-WP Webhook認証用に新規生成)
|
||||
- `LINEWORKS_BOT_SECRET`(署名検証用。値は現状プレースホルダー`UNSET_PENDING_BOT_CALLBACK_CONFIRMATION`。LINEWORKS Developer ConsoleでBot Callback URL設定時に実際のBot Secretへ更新が必要)
|
||||
@ -0,0 +1,261 @@
|
||||
{
|
||||
"name": "HC-SUB: プロセス実行と案内送信",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "trigger-1",
|
||||
"name": "Execute Workflow Trigger",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"workflowInputs": {
|
||||
"values": [
|
||||
{
|
||||
"name": "resultId"
|
||||
},
|
||||
{
|
||||
"name": "processId"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dt-get-config",
|
||||
"name": "設定値一括取得",
|
||||
"type": "n8n-nodes-base.dataTable",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"operation": "get",
|
||||
"dataTableId": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "bNkadTyDgDepYx2p"
|
||||
},
|
||||
"returnAll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "code-pleasanter-query",
|
||||
"name": "Pleasanter照会・選択肢組み立て",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"jsCode": "function isUnsetSentinel(value) {\n return typeof value === \"string\" && value.startsWith(\"1899\");\n}\nfunction fillTemplate(template, columns, valueHash) {\n const labelToColumnName = new Map();\n for (const column of columns) {\n if (column.LabelText) labelToColumnName.set(column.LabelText, column.ColumnName);\n }\n return template.replace(/\\{([^{}]+)\\}/g, (matched, label) => {\n const columnName = labelToColumnName.get(label);\n if (!columnName) return matched;\n const value = valueHash[columnName];\n if (value === undefined || value === null || value === \"\" || isUnsetSentinel(value)) return \"未設定\";\n return String(value);\n });\n}\nfunction extractProcessesForStatus(processes, status) {\n return processes.filter((p) => p.CurrentStatus === status || p.CurrentStatus === -1);\n}\n\nconst configRows = $('設定値一括取得').all().map((item) => item.json);\nconst config = Object.fromEntries(configRows.map((row) => [row.configKey, row.configValue]));\n\nconst trigger = $('Execute Workflow Trigger').item.json;\nconst baseUrl = config.PLEASANTER_BASE_URL_PROD;\nconst apiKey = config.PLEASANTER_API_KEY_PROD;\n\nasync function pleasanterPost(path, body) {\n const res = await this.helpers.httpRequest({\n method: \"POST\",\n url: `${baseUrl}${path}`,\n body: { ApiVersion: 1.1, ApiKey: apiKey, ...body },\n json: true,\n });\n return res;\n}\n\nif (trigger.processId) {\n await pleasanterPost.call(this, `api/items/${trigger.resultId}/update`, { ProcessId: trigger.processId });\n}\n\nconst recordRes = await pleasanterPost.call(this, `api/items/${trigger.resultId}/get`, {});\nconst record = recordRes.Response.Data;\n\nconst siteRes = await pleasanterPost.call(this, `api/items/${config.HEALTHCHECK_SITE_ID}/getsite`, {});\nconst siteSettings = siteRes.Response.Data.SiteSettings;\nconst columns = siteSettings.Columns || [];\nconst processes = siteSettings.Processes || [];\n\nconst botMasterRes = await pleasanterPost.call(this, `api/items/${config.LINEWORKS_BOT_MASTER_SITE_ID}/getsite`, {});\n// TODO(Task 12): 484184(LINEWORKS_BOT_MASTER_SITE_ID)の実データ構造を見てBotIdの解決方法を確定させる。\n// 暫定: SiteSettings.BotIds配列 または Processes[].BotId のいずれかを想定し、\n// 生データのまま後段(JWTクレーム組み立て)へ渡して解決する。\nconst botMasterSiteSettings = botMasterRes.Response.Data.SiteSettings;\n\nconst valueHash = {\n ...record.ClassHash, ...record.NumHash, ...record.DateHash, ...record.DescriptionHash,\n};\n\nconst candidates = extractProcessesForStatus(processes, record.Status);\nconst options = candidates.map((p) => ({\n processId: p.Id,\n label: p.DisplayName || p.Name,\n tooltip: fillTemplate(p.ToolTip || \"\", columns, valueHash),\n}));\n\nconst userRes = await pleasanterPost.call(this, `api/users/get`, {\n View: { ApiGetMailAddresses: true },\n Where: { UserId: record.ClassHash.ClassC },\n});\n// Pleasanter /api/users/get のレスポンス形状は実機未検証のため、配列/単一\n// オブジェクトどちらでも動くように防御的に処理する。\nconst usersResponseData = userRes.Response.Data;\nconst userRecord = Array.isArray(usersResponseData) ? usersResponseData[0] : usersResponseData;\nconst targetEmail = userRecord && (userRecord.MailAddress\n || (Array.isArray(userRecord.MailAddresses) && userRecord.MailAddresses[0]));\nif (!targetEmail) {\n throw new Error('対象者のメールアドレスを解決できませんでした。');\n}\n\nreturn [{\n json: {\n resultId: record.ResultId,\n currentStatus: record.Status,\n options,\n targetEmail,\n config,\n botMasterSiteSettings,\n },\n}];"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "code-jwt-claims",
|
||||
"name": "JWTクレーム組み立て",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"jsCode": "const now = Math.floor(Date.now() / 1000);\nconst { config, botMasterSiteSettings, options, resultId, currentStatus, targetEmail } = $json;\n\nconst jwtClaims = JSON.stringify({\n iss: config.LW_BOT_CLIENT_ID,\n sub: config.LW_BOT_SERVICE_ACCOUNT,\n iat: now,\n exp: now + 3600,\n aud: 'https://auth.worksmobile.com/oauth2/v2.0/token',\n});\n\n// --- Bot ID解決 ---\n// TODO(Task 12): 484184(LINEWORKS_BOT_MASTER_SITE_ID)の実データ構造は未調査\n// (このワークフローは実データ確認前に構築している)。\n// Task 12で508971のProcess⇔Bot対応表を設計する際、実データに合わせて\n// この解決方法を書き換えるか、Execute Workflow Triggerの入力にbotIdを追加して\n// 呼び出し元(WP/WA)から明示的に渡す方式へ変更すること。\n// 暫定実装: getsiteのSiteSettings直下にBotIds配列がある、または\n// Processes[].BotIdが定義されているケースを想定し、最初の1件を採用する。\n// どちらの構造も無ければ、誤ったBotへの送信を避けるため明示的にエラーとする。\nconst candidateBotIds = botMasterSiteSettings.BotIds\n || (botMasterSiteSettings.Processes || []).map((p) => p.BotId).filter(Boolean);\nconst botId = candidateBotIds && candidateBotIds[0];\nif (!botId) {\n throw new Error('BotIdを484184(LINEWORKS_BOT_MASTER_SITE_ID)から解決できませんでした。Task 12でBot選択ロジックを実装してください。');\n}\n\nconst apiUrl = `https://www.worksapis.com/v1.0/bots/${botId}/users/${targetEmail}/messages`;\n\n// --- button_template組み立て ---\nconst actions = options.map((opt) => ({\n type: 'message',\n label: opt.label,\n postback: JSON.stringify({ resultId, processId: opt.processId }),\n displayText: opt.label,\n}));\n\nconst messageContent = {\n type: 'button_template',\n contentText: options.map((opt) => `${opt.label}: ${opt.tooltip}`).join('\\n') || '対応可能な操作がありません',\n actions,\n};\n\nreturn [{\n json: {\n jwtClaims,\n apiUrl,\n messageContent,\n targetEmail,\n resultId,\n currentStatus,\n options,\n config,\n },\n}];"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "jwt-sign",
|
||||
"name": "Sign JWT",
|
||||
"type": "n8n-nodes-base.jwt",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"operation": "sign",
|
||||
"useJson": true,
|
||||
"claimsJson": "={{ $json.jwtClaims }}",
|
||||
"options": {
|
||||
"algorithm": "RS256"
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"jwtAuth": {
|
||||
"id": "Hw0qlEaGfLPnQWp1",
|
||||
"name": "LINEWORKS Bot Private Key (v4)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "code-lineworks-send",
|
||||
"name": "アクセストークン取得・LINEWORKS送信",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1320,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"jsCode": "const claims = $('JWTクレーム組み立て').item.json;\nconst config = claims.config;\nconst assertion = $json.token;\n\n// LINEWORKS tokenエンドポイントはx-www-form-urlencodedのみ受け付ける。\n// this.helpers.httpRequestはbodyにURLSearchParamsインスタンスを渡すと\n// application/x-www-form-urlencodedへの変換とContent-Type設定を自動で\n// 行うため、素のURLSearchParamsを渡す(.toString()しない、ヘッダーも手動指定しない)。\n// jsonオプションは指定しないため、レスポンスは自前でJSON.parseする。\nconst tokenBody = new URLSearchParams({\n assertion,\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n client_id: config.LW_BOT_CLIENT_ID,\n client_secret: config.LW_BOT_CLIENT_SECRET,\n scope: 'bot',\n});\n\nconst tokenRes = await this.helpers.httpRequest({\n method: 'POST',\n url: 'https://auth.worksmobile.com/oauth2/v2.0/token',\n body: tokenBody,\n});\nconst tokenJson = typeof tokenRes === 'string' ? JSON.parse(tokenRes) : tokenRes;\nconst accessToken = tokenJson.access_token;\nif (!accessToken) {\n throw new Error('LINEWORKSアクセストークンの取得に失敗しました。');\n}\n\nconst sendRes = await this.helpers.httpRequest({\n method: 'POST',\n url: claims.apiUrl,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json;charset=UTF-8',\n },\n body: { content: claims.messageContent },\n json: true,\n});\n\nreturn [{\n json: {\n resultId: claims.resultId,\n currentStatus: claims.currentStatus,\n options: claims.options,\n targetEmail: claims.targetEmail,\n lineworksResponse: sendRes,\n },\n}];"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dt-state-insert",
|
||||
"name": "状態更新",
|
||||
"type": "n8n-nodes-base.dataTable",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1540,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"operation": "insert",
|
||||
"dataTableId": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "jqMDa2YZTI4f0iQ7"
|
||||
},
|
||||
"columns": {
|
||||
"mappingMode": "defineBelow",
|
||||
"value": {
|
||||
"resultId": "={{ $json.resultId }}",
|
||||
"targetEmail": "={{ $json.targetEmail }}",
|
||||
"currentStatus": "={{ String($json.currentStatus) }}",
|
||||
"pendingProcesses": "={{ JSON.stringify($json.options) }}",
|
||||
"awaitInput": "none",
|
||||
"awaitProcessId": "",
|
||||
"awaitColumn": ""
|
||||
},
|
||||
"schema": [
|
||||
{
|
||||
"id": "resultId",
|
||||
"displayName": "resultId",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true
|
||||
},
|
||||
{
|
||||
"id": "targetEmail",
|
||||
"displayName": "targetEmail",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true
|
||||
},
|
||||
{
|
||||
"id": "currentStatus",
|
||||
"displayName": "currentStatus",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true
|
||||
},
|
||||
{
|
||||
"id": "pendingProcesses",
|
||||
"displayName": "pendingProcesses",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true
|
||||
},
|
||||
{
|
||||
"id": "awaitInput",
|
||||
"displayName": "awaitInput",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true
|
||||
},
|
||||
{
|
||||
"id": "awaitProcessId",
|
||||
"displayName": "awaitProcessId",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true
|
||||
},
|
||||
{
|
||||
"id": "awaitColumn",
|
||||
"displayName": "awaitColumn",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Execute Workflow Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "設定値一括取得",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"設定値一括取得": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Pleasanter照会・選択肢組み立て",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Pleasanter照会・選択肢組み立て": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "JWTクレーム組み立て",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"JWTクレーム組み立て": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Sign JWT",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Sign JWT": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "アクセストークン取得・LINEWORKS送信",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"アクセストークン取得・LINEWORKS送信": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "状態更新",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
142
Pleasanter/健康診断管理/docs/n8n/workflows/hc-wp-status-push.json
Normal file
142
Pleasanter/健康診断管理/docs/n8n/workflows/hc-wp-status-push.json
Normal file
@ -0,0 +1,142 @@
|
||||
{
|
||||
"name": "HC-WP: Statusプッシュ通知",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "webhook-status-push",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"webhookId": "healthcheck-status-push",
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "healthcheck-status-push",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dt-get-healthcheck_wp_api_key",
|
||||
"name": "設定値一括取得",
|
||||
"type": "n8n-nodes-base.dataTable",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"operation": "get",
|
||||
"dataTableId": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "bNkadTyDgDepYx2p"
|
||||
},
|
||||
"returnAll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "code-validate",
|
||||
"name": "検証",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"jsCode": "const configRows = $('設定値一括取得').all().map((item) => item.json);\nconst expectedApiKey = configRows.find((row) => row.configKey === \"HEALTHCHECK_WP_API_KEY\")?.configValue;\nconst headers = $input.first().json.headers || {};\nif (headers[\"x-api-key\"] !== expectedApiKey) {\n throw new Error(\"Unauthorized: invalid API key\");\n}\nconst body = $input.first().json.body || {};\nif (!body.resultId || !body.processId) {\n throw new Error(\"Bad Request: resultId, processId は必須です\");\n}\nreturn [{ json: { resultId: body.resultId, processId: body.processId } }];"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "execute-hc-sub",
|
||||
"name": "HC-SUB実行",
|
||||
"type": "n8n-nodes-base.executeWorkflow",
|
||||
"typeVersion": 1.2,
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"workflowId": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "XRqcykbG2LuAjGG2"
|
||||
},
|
||||
"workflowInputs": {
|
||||
"mappingMode": "defineBelow",
|
||||
"value": {
|
||||
"resultId": "={{ $json.resultId }}",
|
||||
"processId": "={{ $json.processId }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "respond-ok",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ result: \"ok\" }) }}",
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "設定値一括取得",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"設定値一括取得": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "検証",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"検証": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HC-SUB実行",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HC-SUB実行": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
BIN
Pleasanter/健康診断管理/★健康診断プリザンター化_対応エクセル作成途中.xlsx
Normal file
BIN
Pleasanter/健康診断管理/★健康診断プリザンター化_対応エクセル作成途中.xlsx
Normal file
Binary file not shown.
BIN
Pleasanter/健康診断管理/プリザンターテーブル設計書_健康診断.xlsx
Normal file
BIN
Pleasanter/健康診断管理/プリザンターテーブル設計書_健康診断.xlsx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user