workspace-actions.js
'use strict';
(() => {
const W=window.WORKSPACE,e=s=>W.ctx.esc(s),notify=s=>W.ctx.notify(s);
const download=(text,name,type='text/csv;charset=utf-8')=>{const url=URL.createObjectURL(new Blob([text],{type})),a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
const csvCell=s=>'"'+String(s??'').replaceAll('"','""')+'"';
W.csvTemplate=()=>download('\uFEFF'+[['対象月','資料','項目','値','単位'],...W.docs.flatMap(d=>d.metrics.map(([,label,unit])=>[W.state.month,d.title,label,'',unit]))].map(r=>r.map(csvCell).join(',')).join('\r\n'),'SAMURAI_数値取り込み書式.csv');
W.valuesCSV=()=>download('\uFEFF'+[['対象月','資料','項目','値','単位','区分'],...W.docs.flatMap(d=>{const info=W.values(d);return d.metrics.filter(([k])=>info.data[k]!==undefined).map(([k,label,unit])=>[W.state.month,d.title,label,info.data[k],unit,info.source]);})].map(r=>r.map(csvCell).join(',')).join('\r\n'),W.state.month+'_数値一覧.csv');
W.importDialog=()=>W.ctx.dialog('数値を取り込む',`<p>CSV・TSV・JSONの数値を、このブラウザ内の資料ページへ反映できます。ファイルは送信しません。</p><div class="import-format"><strong>CSVの列</strong><p>対象月・資料・項目・値・単位</p><p>資料名と項目名は、書式ファイルに一覧で入っています。</p><button class="button" data-csv-template="true">入力用CSVをダウンロード</button></div><label class="import-file-label" for="workspace-import">数値ファイルを選ぶ(2MBまで)</label><input id="workspace-import" type="file" accept=".csv,.tsv,.json,text/csv,application/json"><p id="import-feedback" role="status">取り込み前に、対象月と項目を確認します。</p>`);
function parseCSV(text){const sep=text.split(/\r?\n/)[0].includes('\t')?'\t':',';let rows=[],row=[],field='',quoted=false;for(let n=0;n<text.length;n++){const c=text[n];if(c==='"'){if(quoted&&text[n+1]==='"'){field+='"';n++;}else quoted=!quoted;}else if(c===sep&&!quoted){row.push(field);field='';}else if((c==='\r'||c==='\n')&&!quoted){if(c==='\r'&&text[n+1]==='\n')n++;row.push(field);if(row.some(v=>v.trim()))rows.push(row);row=[];field='';}else field+=c;}if(quoted)throw Error('引用符が閉じていません。');row.push(field);if(row.some(v=>v.trim()))rows.push(row);const headers=(rows.shift()||[]).map(s=>s.trim());return rows.map(r=>Object.fromEntries(headers.map((h,n)=>[h,r[n]?.trim()??''])));}
W.validateImport=text=>{
const cleaned=text.replace(/^\uFEFF/,'');const data=cleaned.trim().startsWith('[')?JSON.parse(cleaned):parseCSV(cleaned);if(!Array.isArray(data)||!data.length||data.length>5000)throw Error('1〜5,000行の数値一覧を選んでください。');
const seen=new Set(),rows=[];for(const [n,row]of data.entries()){
if(!row||typeof row!=='object'||Array.isArray(row))throw Error(`${n+1}行目の形式を確認してください。`);
const month=String(row['対象月']??row.month??''),name=String(row['資料']??row.documentId??''),metric=String(row['項目']??row.metric??''),raw=row['値']??row.value,unit=String(row['単位']??row.unit??'');
const doc=W.docs.find(d=>d.id===name||d.title===name),field=doc?.metrics.find(([id,label])=>id===metric||label===metric);
if(!W.validMonth(month)||!doc||!field)throw Error(`${n+1}行目の対象月・資料・項目が書式と一致しません。`);
if(unit&&unit!==field[2])throw Error(`${n+1}行目の単位は「${field[2]}」にしてください。`);
if(raw===undefined||raw===null||String(raw).trim()===''||!/^[-+]?\d+(?:\.\d+)?$/.test(String(raw).replaceAll(',',''))||!Number.isFinite(Number(String(raw).replaceAll(',','')))||Math.abs(Number(String(raw).replaceAll(',','')))>1e15)throw Error(`${n+1}行目の「値」に数値を入れてください。`);
const key=month+'/'+doc.id+'/'+field[0];if(seen.has(key))throw Error(`${n+1}行目に同じ月・資料・項目が重複しています。`);seen.add(key);rows.push({month,docId:doc.id,metric:field[0],label:field[1],value:Number(String(raw).replaceAll(',','')),unit:field[2]});
}return rows;
};
W.pendingImport=null;
W.editValues=id=>{const doc=W.find(id);if(!doc)return;const own=W.state.values[W.key(id)]||{};W.ctx.dialog(doc.title+' / '+W.state.month+' の数値',`<p>入力値はこのブラウザ内の表示とPDFに反映します。元の基準値サンプルは変更しません。</p><form id="document-value-form" data-document="${id}"><div class="value-fields">${doc.metrics.map(([key,label,unit])=>`<label>${e(label)}<span><input name="${key}" type="number" step="any" min="-1000000000000000" max="1000000000000000" value="${e(own[key]??'')}" inputmode="decimal">${e(unit)}</span></label>`).join('')}</div><button class="button primary" type="submit">この月の数値を反映</button><button class="button" type="button" data-clear-values="${id}">この月の入力を取り消す</button></form>`);};
W.bind=()=>{
const app=document.getElementById('app');
app.addEventListener('click',event=>{const t=event.target.closest('button');if(!t)return;
if(t.dataset.docLook){const id=t.dataset.docLook,style=Number(t.dataset.style);if(W.find(id)&&style>=1&&style<=10){W.state.looks[id]=style;const saved=W.save();W.ctx.refresh();document.querySelector('[data-doc-look="'+id+'"][data-style="'+style+'"]')?.focus({preventScroll:true});notify(saved?'この資料の画像と詳細の表示案を保存しました。':'表示案を切り替えました。ブラウザの保存は利用できません。');}}
if(t.dataset.favorite){const id=t.dataset.favorite;W.state.favorites=W.state.favorites.includes(id)?W.state.favorites.filter(x=>x!==id):[...W.state.favorites,id];W.save();t.setAttribute('aria-pressed',String(W.state.favorites.includes(id)));t.textContent=W.state.favorites.includes(id)?'お気に入り済み':'お気に入りに追加';}
if(t.dataset.editValues)W.editValues(t.dataset.editValues);
if(t.dataset.importValues)W.importDialog();
if(t.dataset.pdf)W.downloadPDF(t.dataset.pdf,t).catch(error=>{notify('PDFを作成できませんでした。もう一度お試しください。');console.error('PDF export failed:',error.message);});
if(t.dataset.valuesCsv)W.valuesCSV();
});
app.addEventListener('change',event=>{const t=event.target;if(t.matches('.report-month')){if(!W.validMonth(t.value)){t.value=W.state.month;notify('有効な対象月を選んでください。');return;}W.state.month=t.value;W.syncPeriodMonth?.(t.value);W.save();W.ctx.refresh();notify(W.state.month+' の資料を表示しています。');}if(t.dataset.monthCheck!==undefined){W.state.checks[W.state.month]??={};W.state.checks[W.state.month][t.dataset.monthCheck]=t.checked;W.save();const counter=document.querySelector('[data-check-count]');if(counter)counter.innerHTML=Object.values(W.state.checks[W.state.month]).filter(Boolean).length+'<small>/ 4</small>';}});
app.addEventListener('input',event=>{const t=event.target;if(t.dataset.note){W.state.notes[W.key(t.dataset.note)]=t.value;const ok=W.save();const status=t.parentElement.querySelector('.local-save-note');if(status)status.textContent=ok?'このブラウザ内に保存しました。':'保存できません。必要な内容はPDFへ保存してください。';}if(t.id==='folder-search'){let count=0;document.querySelectorAll('[data-doc-title]').forEach(card=>{card.hidden=!card.dataset.docTitle.toLocaleLowerCase().includes(t.value.trim().toLocaleLowerCase());if(!card.hidden)count++;});document.getElementById('folder-search-empty').hidden=count>0;}});
const dialog=document.getElementById('dialog');
dialog.addEventListener('change',async event=>{if(event.target.id!=='workspace-import')return;const file=event.target.files?.[0],feedback=document.getElementById('import-feedback');if(!file)return;try{if(file.size>2*1024*1024)throw Error('2MB以内のファイルを選んでください。');const rows=W.validateImport(await file.text());W.pendingImport=rows;feedback.innerHTML=`<strong>${rows.length}項目を確認しました。</strong><p>対象月:${[...new Set(rows.map(r=>r.month))].map(e).join('、')} / ${new Set(rows.map(r=>r.docId)).size}資料</p><p>資料ごとの数値だけを反映します。同じ月・項目の入力済み数値は置き換わります。</p><button class="button primary" data-confirm-import="true">このブラウザに反映</button>`;}catch(error){W.pendingImport=null;feedback.textContent=error.message;}});
dialog.addEventListener('click',event=>{const t=event.target.closest('button');if(!t)return;if(t.dataset.csvTemplate)W.csvTemplate();if(t.dataset.confirmImport&&W.pendingImport){for(const r of W.pendingImport){const key=r.month+'/'+r.docId;W.state.values[key]??={};W.state.values[key][r.metric]=r.value;}W.state.month=W.pendingImport.map(r=>r.month).sort().at(-1);W.syncPeriodMonth?.(W.state.month,{keepAll:true});const count=W.pendingImport.length,ok=W.save();W.pendingImport=null;dialog.close();W.ctx.refresh();notify(`${count}項目を反映しました。${ok?'このブラウザ内に保存しました。':'保存は利用できません。PDFで保存してください。'}`);}if(t.dataset.clearValues){delete W.state.values[W.key(t.dataset.clearValues)];W.save();dialog.close();W.ctx.refresh();notify('この月の入力を取り消しました。');}});
dialog.addEventListener('submit',event=>{if(event.target.id!=='document-value-form')return;event.preventDefault();const id=event.target.dataset.document,doc=W.find(id),data={};for(const [key]of doc.metrics){const input=event.target.elements.namedItem(key);if(input.value.trim()!==''){if(!input.checkValidity()||!Number.isFinite(Number(input.value)))return;data[key]=Number(input.value);}}if(Object.keys(data).length)W.state.values[W.key(id)]=data;else delete W.state.values[W.key(id)];const ok=W.save();dialog.close();W.ctx.refresh();notify(ok?'この月の数値を反映しました。':'数値を反映しました。ブラウザの保存は利用できません。');});
document.addEventListener('click',event=>{if(!event.target.closest('.nav-family'))W.closeNavigation?.();});
document.addEventListener('keydown',event=>{if(event.key!=='Escape')return;const open=document.querySelector('.nav-family.is-open');if(open){W.closeNavigation?.();open.querySelector('[data-nav-toggle]')?.focus();event.preventDefault();}});
};
})();