日別集計に期間の任意指定とプロジェクト除外を追加

/api/daily に from / to / exclude を追加。from/to があれば days より優先し、
その範囲を 0 埋めで返す。exclude のプロジェクトは集計対象から外す
(横断モードのみ送信)。CSV も同じ条件で出力される。

UI は日数ボタンの隣に日付入力を置き、凡例のクリックで除外・「除外中」から
クリックで復帰できるようにした。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 10:30:55 +09:00
co-authored by Claude Opus 5
parent 0c312afaa5
commit 8fd54757e6
4 changed files with 140 additions and 21 deletions
+71 -6
View File
@@ -12,6 +12,9 @@ const state = {
relative: false,
view: 'turns',
days: 30,
dailyFrom: '', // 期間の任意指定 (YYYY-MM-DD)。空なら days を使う
dailyTo: '',
dailyExclude: [], // 集計から外すプロジェクト ID (横断モードのみ)
daily: null,
ganttDays: 7,
ganttUnit: 'task', // task=タスク(セッション)を並べる / turn=選択中タスクのターンを並べる
@@ -316,14 +319,27 @@ function syncTabs() {
$('#tab-gantt').classList.toggle('sel', state.view === 'gantt');
}
function scopeQuery(days, extra) {
const q = new URLSearchParams({ days: String(days), ...extra });
function ganttQuery(extra) {
const q = new URLSearchParams({ days: String(state.ganttDays), ...extra });
if (state.project) q.set('project', state.project);
return q.toString();
}
const dailyQuery = (extra) => scopeQuery(state.days, extra);
const ganttQuery = (extra) => scopeQuery(state.ganttDays, extra);
const customRange = () => Boolean(state.dailyFrom || state.dailyTo);
function dailyQuery(extra) {
const q = new URLSearchParams(extra || {});
if (state.project) q.set('project', state.project);
if (customRange()) {
if (state.dailyFrom) q.set('from', state.dailyFrom);
if (state.dailyTo) q.set('to', state.dailyTo);
} else {
q.set('days', String(state.days));
}
// 除外は横断モードだけ (単一プロジェクト表示では自分自身を消してしまうので送らない)
if (!state.project && state.dailyExclude.length) q.set('exclude', state.dailyExclude.join(','));
return q.toString();
}
function updateCsv() {
const link = $('#csv-turns');
@@ -465,9 +481,16 @@ function renderDaily() {
svg.push(`<rect class="hit" x="${(padL + i * slot).toFixed(1)}" y="${padT}" width="${slot.toFixed(1)}" height="${plotH}"><title>${esc(tip(r))}</title></rect>`);
});
const custom = customRange();
const ranges = [14, 30, 90, 0]
.map((v) => `<button class="rng${v === state.days ? ' sel' : ''}" data-days="${v}">${v ? v + '日' : '全期間'}</button>`)
.map((v) => `<button class="rng${!custom && v === state.days ? ' sel' : ''}" data-days="${v}">${v ? v + '日' : '全期間'}</button>`)
.join('');
const dates = `<span class="dates">
<input type="date" id="d-from" value="${state.dailyFrom}" max="${state.dailyTo}" title="開始日">
<span class="tilde">〜</span>
<input type="date" id="d-to" value="${state.dailyTo}" min="${state.dailyFrom}" title="終了日">
${custom ? '<button class="rng sel" id="d-clear" title="期間指定を解除">✕</button>' : ''}
</span>`;
const active = rows.filter((r) => r.activeMs || r.turns).reverse();
const table = active.length
@@ -506,17 +529,58 @@ function renderDaily() {
</div>
<div class="chart-bar">
<span class="rngs">${ranges}</span>
${dates}
<span class="note">棒=その日の応答時間の合計。セッションを並行して動かした日は 24 時間を超えることがあります。</span>
</div>
<div class="chart"><svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${svg.join('')}</svg></div>
${cross
? `<div class="legend">${legend
.map((p, i) => `<span class="lg" title="${esc(p.path)}"><i style="background:${PALETTE[i]}"></i>${esc(shortPath(p.path))} <b>${fmtDur(p.activeMs)}</b></span>`)
.map((p, i) => `<span class="lg act" data-ex="${esc(p.id)}" title="${esc(p.path)}\nクリックで集計から外す"><i style="background:${PALETTE[i]}"></i>${esc(shortPath(p.path))} <b>${fmtDur(p.activeMs)}</b></span>`)
.join('')}${otherMs ? `<span class="lg"><i style="background:${OTHER_COLOR}"></i>その他 ${d.projects.length - legend.length} 件 <b>${fmtDur(otherMs)}</b></span>` : ''}</div>`
: ''}
${cross && state.dailyExclude.length
? `<div class="legend excl"><span class="cap">除外中</span>${state.dailyExclude
.map((id) => `<span class="lg off" data-ex="${esc(id)}" title="${esc(projectPathOf(id))}\nクリックで戻す">✕ ${esc(shortPath(projectPathOf(id)))}</span>`)
.join('')}<button class="rng" id="ex-clear">すべて戻す</button></div>`
: ''}
${table}`;
wireRanges('days', loadDaily);
wireDaily();
}
/** 日別集計の期間指定・プロジェクト除外を配線する。 */
function wireDaily() {
const setRange = (from, to) => {
state.dailyFrom = from;
state.dailyTo = to;
updateCsv();
loadDaily();
};
const from = $('#d-from');
const to = $('#d-to');
if (from) from.onchange = () => setRange(from.value, state.dailyTo);
if (to) to.onchange = () => setRange(state.dailyFrom, to.value);
const clear = $('#d-clear');
if (clear) clear.onclick = () => setRange('', '');
document.querySelectorAll('#detail-body .legend [data-ex]').forEach((el) => {
el.onclick = () => {
const id = el.dataset.ex;
const at = state.dailyExclude.indexOf(id);
if (at >= 0) state.dailyExclude.splice(at, 1);
else state.dailyExclude.push(id);
updateCsv();
loadDaily();
};
});
const exClear = $('#ex-clear');
if (exClear)
exClear.onclick = () => {
state.dailyExclude = [];
updateCsv();
loadDaily();
};
}
// ---------------------------------------------------------------- ガント
@@ -920,6 +984,7 @@ function wireRanges(key, reload) {
document.querySelectorAll('#detail-body .rng[data-days]').forEach((btn) => {
btn.onclick = () => {
state[key] = Number(btn.dataset.days);
if (key === 'days') state.dailyFrom = state.dailyTo = ''; // 日数指定と期間指定は排他
updateCsv();
reload();
};