'use strict';
// 「n8n へ送る」タブ: Claude のセッションから作った作業区間を選び、n8n の work_log に送る。
// app.js の後に読み込む ($ / state / api / post / esc / fmtTime / shortPath を使う)。
const N8N_NAMES_KEY = 'chktime.n8n.names'; // セッション ID -> {task, memo} (ブラウザに覚える)
const n8 = {
cfg: null, // /api/n8n/config (合言葉は含まれない)
from: '',
to: '',
data: null, // /api/n8n/blocks
checked: new Set(), // 送る区間の source
edits: new Map(), // source -> {task, memo}
sameSession: true, // 同じセッションの他の区間にも入れる
settings: false,
busy: false,
result: null, // 直前の送信結果
pick: null, // チケットを選んでいる区間の source
issues: [],
pickMsg: '',
};
function n8LoadNames() {
try {
return JSON.parse(localStorage.getItem(N8N_NAMES_KEY) || '{}') || {};
} catch (e) {
return {};
}
}
function n8SaveName(sessionId, value) {
try {
const all = n8LoadNames();
all[sessionId] = value;
localStorage.setItem(N8N_NAMES_KEY, JSON.stringify(all));
} catch (e) {
/* 保存できなくても動く */
}
}
const n8CanSend = (b) => !b.sent && !b.inProgress;
const n8DefaultOn = (b) => n8CanSend(b) && !b.deleted && !(b.overlaps || []).length;
function n8Query() {
const q = new URLSearchParams();
if (state.project) q.set('project', state.project);
if (n8.from) q.set('from', n8.from);
if (n8.to) q.set('to', n8.to);
return q.toString();
}
async function loadN8n({ silent = false } = {}) {
$('#detail-title').textContent = 'n8n へ送る';
if (!silent) $('#detail-body').innerHTML = '
区間を計算中…
';
try {
n8.cfg = await api('/api/n8n/config');
if (!n8.cfg.url || !n8.cfg.hasToken) n8.settings = true;
n8.data = await api('/api/n8n/blocks?' + n8Query());
n8.from = n8.data.from;
n8.to = n8.data.to;
const names = n8LoadNames();
n8.checked = new Set(n8.data.blocks.filter(n8DefaultOn).map((b) => b.source));
for (const b of n8.data.blocks) {
if (n8.edits.has(b.source)) continue;
// 続きのセッションに写された区間は、写し元のセッションで覚えた名前も使う
const saved = [b.sessionId, ...(b.aliasSessions || [])].map((id) => names[id]).find(Boolean) || {};
n8.edits.set(b.source, { task: saved.task || b.taskName, memo: saved.memo || '' });
}
renderN8n();
} catch (e) {
$('#detail-body').innerHTML = `${esc(e.message)}
`;
}
}
function n8Status(b) {
const marks = [];
if (b.sent) marks.push('送信済み');
if (b.changedSinceSent) marks.push('送信後に変化');
if (b.deleted) marks.push('削除済み');
if (b.inProgress) marks.push('進行中');
if ((b.overlaps || []).length) {
const tip = b.overlaps.map((o) => `${o.kind === 'row' ? 'work_log' : '区間'}: ${fmtTime(o.start)}–${fmtTime(o.end)} ${o.task}`).join('\n');
marks.push(`重なり ${b.overlaps.length}`);
}
return marks.join(' ');
}
function n8Settings() {
const c = n8.cfg || {};
return `
接続設定 ${esc(c.configPath || '')}
`;
}
function n8ResultHtml() {
const r = n8.result;
if (!r) return '';
const errs = (r.errors || []).map((e) => `${esc(e.source)}: ${esc(e.error)}`).join('');
const skips = (r.skipped || []).map((s) => `${esc(s.source)}: ${esc(s.reason)}`).join('');
return `
追加 ${(r.inserted || []).length} 件 / スキップ ${(r.skipped || []).length} 件 / エラー ${(r.errors || []).length} 件
${skips ? `
` : ''}${errs ? `
` : ''}
`;
}
function n8PickHtml() {
if (!n8.pick) return '';
const rows = n8.issues
.map((i) => ` [${esc(i.project)}] ${esc(i.subject)}`)
.join('');
return ``;
}
function renderN8n() {
const d = n8.data;
if (!d) return;
const blocks = d.blocks;
const on = blocks.filter((b) => n8.checked.has(b.source) && n8CanSend(b));
const rows = blocks
.map((b) => {
const e = n8.edits.get(b.source) || { task: b.taskName, memo: '' };
return `
|
${esc(b.date)} |
${fmtTime(b.start)}–${fmtTime(b.end)} |
${b.minutes} 分 |
${esc(shortPath(b.projectPath))} |
|
|
${n8Status(b)} |
`;
})
.join('');
$('#detail-body').innerHTML = ``;
n8Wire();
}
function n8Block(source) {
return n8.data.blocks.find((b) => b.source === source);
}
/** 区間の編集値を変え、必要なら同じセッションの他の区間にも入れて、ブラウザに覚える。 */
function n8SetEdit(source, patch) {
const b = n8Block(source);
const targets = n8.sameSession ? n8.data.blocks.filter((x) => x.sessionId === b.sessionId && !x.sent) : [b];
for (const t of targets) n8.edits.set(t.source, { ...(n8.edits.get(t.source) || {}), ...patch });
n8SaveName(b.sessionId, n8.edits.get(source));
}
function n8Wire() {
$('#n8-reload').onclick = () => {
n8.from = $('#n8-from').value;
n8.to = $('#n8-to').value;
loadN8n();
};
$('#n8-same').onchange = (e) => {
n8.sameSession = e.target.checked;
};
$('#n8-toggle').onclick = () => {
n8.settings = !n8.settings;
renderN8n();
};
$('#n8-send').onclick = n8Send;
if (n8.settings) $('#n8-save').onclick = n8SaveSettings;
if (n8.pick) {
$('#n8-find').onclick = n8Find;
$('#n8-q').onkeydown = (e) => {
if (e.key === 'Enter') n8Find();
};
$('#n8-pick-close').onclick = () => {
n8.pick = null;
renderN8n();
};
document.querySelectorAll('.n8-issue').forEach((btn) => {
btn.onclick = () => {
const issue = n8.issues.find((i) => String(i.id) === btn.dataset.id);
n8SetEdit(n8.pick, { task: `[${issue.project}] #${issue.id} ${issue.subject}` });
n8.pick = null;
renderN8n();
};
});
}
document.querySelectorAll('#n8n-table tbody tr[data-src]').forEach((tr) => {
const source = tr.dataset.src;
tr.querySelector('.n8-on').onchange = (e) => {
if (e.target.checked) n8.checked.add(source);
else n8.checked.delete(source);
renderN8n();
};
tr.querySelector('.n8-task').onchange = (e) => {
n8SetEdit(source, { task: e.target.value });
renderN8n();
};
tr.querySelector('.n8-memo').onchange = (e) => {
n8SetEdit(source, { memo: e.target.value });
};
tr.querySelector('.n8-choose').onclick = () => {
n8.pick = source;
n8.issues = [];
n8.pickMsg = '';
renderN8n();
};
});
}
async function n8Find() {
n8.pickMsg = '検索中…';
const q = $('#n8-q').value;
renderN8n();
try {
n8.issues = (await api('/api/redmine/search?q=' + encodeURIComponent(q))).issues;
n8.pickMsg = n8.issues.length ? '' : '見つかりません';
} catch (e) {
n8.pickMsg = e.message;
}
renderN8n();
$('#n8-q').value = q;
}
async function n8SaveSettings() {
const patch = {
url: $('#n8-url').value,
verifySsl: $('#n8-verify').checked,
gapMinutes: Number($('#n8-gap').value),
minMinutes: Number($('#n8-min').value),
};
const token = $('#n8-token').value;
if (token) patch.token = token;
try {
n8.cfg = await post('/api/n8n/config', patch);
n8.settings = false;
loadN8n();
} catch (e) {
alert('保存できません: ' + e.message);
}
}
async function n8Send() {
const items = n8.data.blocks
.filter((b) => n8.checked.has(b.source) && n8CanSend(b))
.map((b) => ({ project: b.project, source: b.source, ...(n8.edits.get(b.source) || { task: b.taskName, memo: '' }) }));
if (!items.length) return;
n8.busy = true;
renderN8n();
try {
n8.result = await post('/api/n8n/send', { items });
} catch (e) {
n8.result = { ok: false, inserted: [], skipped: [], errors: [{ source: '', error: e.message }] };
}
n8.busy = false;
await loadN8n({ silent: true });
}