日別集計に期間の任意指定とプロジェクト除外を追加
/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:
+71
-6
@@ -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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user