sheet-period.js
'use strict';
(() => {
const W=window.WORKSPACE,e=s=>W.ctx.esc(s);
const ordinal=m=>Number(m.slice(0,4))*12+Number(m.slice(5))-1;
const month=n=>String(Math.floor(n/12)).padStart(4,'0')+'-'+String(n%12+1).padStart(2,'0');
const presets=[['all','全期間',null],['month','単月',1],['3m','3か月',3],['6m','6か月',6],['12m','1年',12]];
W.monthLabel=m=>m.slice(0,4)+'年'+Number(m.slice(5))+'月';
W.validatePeriod=(start,end,all=false)=>{if(!W.validMonth(start)||!W.validMonth(end))throw Error('開始月と終了月を選んでください。');if(start>end)throw Error('開始月は終了月以前にしてください。');if(!all&&ordinal(end)-ordinal(start)>=120)throw Error('対象期間は120か月(10年)以内で指定してください。');return{start,end};};
W.availableDataMonths=()=>{const dates=[];for(const[key,data]of Object.entries(W.state.values)){const m=key.slice(0,7),doc=W.find(key.slice(8));if(key[7]==='/'&&W.validMonth(m)&&doc&&doc.metrics.some(([field])=>typeof data?.[field]==='number'&&Number.isFinite(data[field])))dates.push(m);}const baseline=W.find('sheet-05');if(baseline&&baseline.metrics.some(([field])=>Number.isFinite(W.values(baseline,'2026-09').data[field])))dates.push('2026-09');return [...new Set(dates)].sort();};
W.fullPeriod=()=>{const dates=W.availableDataMonths();return{start:dates[0]||W.state.month,end:dates.at(-1)||W.state.month};};
W.syncPeriodMonth=(end,{keepAll=false}={})=>{const p=W.state.sheetPeriod;if(p?.preset==='all'){W.state.sheetPeriod=keepAll?{...W.fullPeriod(),preset:'all'}:{start:end,end,preset:'month'};W.state.month=W.state.sheetPeriod.end;return;}const span=p&&W.validMonth(p.start)&&W.validMonth(p.end)?Math.max(1,Math.min(120,ordinal(p.end)-ordinal(p.start)+1)):1;W.state.sheetPeriod={start:month(Math.max(ordinal('2000-01'),ordinal(end)-span+1)),end,preset:p?.preset||'month'};};
W.period=()=>{let p=W.state.sheetPeriod;try{W.validatePeriod(p?.start,p?.end,p?.preset==='all');}catch{p={start:W.state.month,end:W.state.month,preset:'month'};W.state.sheetPeriod=p;}if(p.preset==='all'){W.state.sheetPeriod={...W.fullPeriod(),preset:'all'};W.state.month=W.state.sheetPeriod.end;}else if(p.end!==W.state.month)W.syncPeriodMonth(W.state.month);return W.state.sheetPeriod;};
W.periodMonths=()=>{const p=W.period();return Array.from({length:ordinal(p.end)-ordinal(p.start)+1},(_,n)=>month(ordinal(p.start)+n));};
W.periodLabel=()=>{const p=W.period();return p.start===p.end?W.monthLabel(p.end):W.monthLabel(p.start)+' ~ '+W.monthLabel(p.end);};
W.periodRows=doc=>W.periodMonths().map(month=>({month,...W.values(doc,month)}));
W.setPeriod=(start,end,preset='custom')=>{W.validatePeriod(start,end,preset==='all');W.state.month=end;W.state.sheetPeriod={start,end,preset};const saved=W.save();W.ctx.refresh();W.ctx.notify((preset==='all'?'全期間:':'')+W.periodLabel()+' を表示します。'+(saved?'':'保存は利用できません。'));};
W.periodControls=doc=>{const p=W.period();return `<div class="sheet-period-row"><span class="control-caption">対象期間</span><div class="period-presets" role="group" aria-label="期間の切り替え">${presets.map(([id,label])=>`<button type="button" data-period-preset="${id}" data-period-document="${e(doc.id)}" aria-pressed="${p.preset===id}">${label}</button>`).join('')}<button type="button" data-period-custom="true" aria-pressed="${p.preset==='custom'}">期間を指定</button></div><span class="period-current">${e(W.periodLabel())}</span><span class="period-end-note">主要数値:${e(W.monthLabel(p.end))}</span></div>`;};
W.sheetControls=doc=>`<section class="sheet-controls" aria-label="期間とグラフの設定">${W.periodControls(doc)}${W.sheetChartPicker(doc)}</section>`;
W.periodDialog=()=>{const p=W.period();W.ctx.dialog('対象期間を設定',`<p>開始月と終了月を指定します。すべての資料ページで同じ期間を使います。</p><form id="sheet-period-form"><div class="period-fields"><label>開始月<input id="period-start" name="start" type="month" min="2000-01" max="2099-12" value="${e(p.start)}" required></label><span>~</span><label>終了月<input id="period-end" name="end" type="month" min="2000-01" max="2099-12" value="${e(p.end)}" required></label></div><p id="period-error" role="alert"></p><p class="local-save-note">月単位・任意指定は最大120か月。登録済みの範囲をすべて見る場合は「全期間」を選んでください。主要数値は終了月、グラフと期間一覧は指定した期間を表示します。</p><button class="button primary" type="submit">この期間を表示</button></form>`);};
W.periodCSV=doc=>{const cell=s=>'"'+String(s??'').replaceAll('"','""')+'"',rows=[['対象月','資料','項目','値','単位','区分'],...W.periodRows(doc).flatMap(r=>doc.metrics.map(([key,label,unit])=>[r.month,doc.title,label,r.data[key]??'',unit,r.source]))],url=URL.createObjectURL(new Blob(['\uFEFF'+rows.map(r=>r.map(cell).join(',')).join('\r\n')],{type:'text/csv;charset=utf-8'})),a=document.createElement('a'),p=W.period();a.href=url;a.download=(p.start+'_'+p.end+'_'+doc.title+'_期間一覧.csv').replace(/[\\/:*?"<>|]/g,'_');a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
W.bindSheetControls=()=>{
document.getElementById('app').addEventListener('click',event=>{const b=event.target.closest('button');if(!b)return;
if(b.dataset.periodPreset){const preset=presets.find(p=>p[0]===b.dataset.periodPreset);if(preset){if(preset[0]==='all'){const full=W.fullPeriod();W.setPeriod(full.start,full.end,'all');}else{const end=W.state.month;W.setPeriod(month(Math.max(ordinal('2000-01'),ordinal(end)-preset[2]+1)),end,preset[0]);}document.querySelector(`[data-period-preset="${preset[0]}"]`)?.focus({preventScroll:true});}}
if(b.dataset.periodCustom)W.periodDialog();
if(b.dataset.sheetChart){const doc=W.find(b.dataset.chartDocument);if(!doc)return;const option=W.sheetChartOptions(doc).find(o=>o.id===b.dataset.sheetChart);if(!option)return;const left=b.closest('.sheet-chart-strip')?.scrollLeft||0;W.state.sheetCharts??={};W.state.sheetCharts[doc.id]??={};W.state.sheetCharts[doc.id][W.hasLegacyComposition(doc)?'legacy':'monthly']=option.id;W.save();W.ctx.refresh();const strip=document.querySelector('.sheet-chart-strip');if(strip)strip.scrollLeft=left;document.querySelector(`[data-sheet-chart="${option.id}"]`)?.focus({preventScroll:true});W.ctx.notify(option.name+' に切り替えました。');}
if(b.dataset.periodCsv){const doc=W.find(b.dataset.periodCsv);if(doc)W.periodCSV(doc);}
});
document.getElementById('dialog').addEventListener('submit',event=>{if(event.target.id!=='sheet-period-form')return;event.preventDefault();try{const data=new FormData(event.target);W.validatePeriod(String(data.get('start')),String(data.get('end')));document.getElementById('dialog').close();W.setPeriod(String(data.get('start')),String(data.get('end')));document.querySelector('[data-period-custom]')?.focus({preventScroll:true});}catch(error){document.getElementById('period-error').textContent=error.message;}});
};
W.period();
})();