⬅ 入口 📖 マニュアル

試験反在庫管理システム

社内専用 — 名前を選んでログイン

パスワードが正しくありません
🔑 パスワードをお忘れの方は、社長・事務にご連絡ください
v2.7 (2026-06-21)

試験反在庫管理 v2.7 (2026-06-21)

データを読み込み中...
'; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } function printOneRoll(id){ var r=(rows||[]).find(function(x){return String(x.id)===String(id);}); if(!r)return; /* 全項目を載せる(パレット=P/L数・本数=R数・M も含めて漏らさない)。ラベルはt()で表示言語に追従 */ var data=[ ['No',r.no], [t('lbl_product'),r.product], [t('lbl_requester'),r.requester], [t('lbl_date'),r.date], [t('lbl_usedate'),r.useDate], [t('lbl_rm'),r.rm], [t('lbl_pl'),r.pl], [t('lbl_stock'),r.stock], [t('lbl_status'),r.status], [t('lbl_test'),r.test] ]; var body=data.map(function(x){return ''+pEsc(x[0])+''+pEsc(x[1]).replace(/\n/g,'
')+'';}).join(''); /* 写真(あれば全部) */ var phs=(r.photo?photoList_(r.photo):[]); var photoHtml=phs.length?('
'+phs.map(function(p){return '';}).join('')+'
'):''; var html='試験反 '+pEsc(r.no)+'

🧪 試験反 記録

出力日:'+pNow()+'
'+body+'
'+photoHtml+''; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } /* ===== 👥 依頼者別 集計(パレット=P/L数・本数=R数 を依頼者ごとに合計) ===== */ var aggScope='stock'; /* 'stock'=在庫ありのみ / 'all'=全件 */ /* 自由表記から数値を取り出す(無ければ0)。"1パレ"→1/"3パレット"→3/"2パレ・1層"→2(先頭) */ function aggParsePal_(s){ var m=String(s==null?'':s).match(/(\d+(?:\.\d+)?)/); return m?parseFloat(m[1]):0; } /* "2R2500M"→ R数(本数)=2 */ function aggParseRolls_(s){ var m=String(s==null?'':s).match(/(\d+(?:\.\d+)?)\s*[RrR]/); return m?parseFloat(m[1]):0; } /* "2R2500M"→ メートル=2500 */ function aggParseM_(s){ var m=String(s==null?'':s).match(/(\d+(?:\.\d+)?)\s*[MmM]/); return m?parseFloat(m[1]):0; } /* 数値表示:整数はそのまま、端数は小数1桁、3桁区切り */ function aggNum_(n){ if(!n) return '0'; var r=Math.round(n*10)/10; return (r%1===0?r:r.toFixed(1)).toLocaleString?Number(r).toLocaleString(undefined,{maximumFractionDigits:1}):String(r); } function aggRows_(){ var live=(rows||[]).filter(function(r){return !isDeleted(r);}); if(aggScope==='stock') live=live.filter(function(r){return r.stock==='あり';}); return live; } /* 任意の行配列を依頼者でグループ化(パレット降順)。在庫一覧印刷など外からも使う */ function aggGroupsOf_(arr){ var map={}; (arr||[]).forEach(function(r){ var k=(String(r.requester||'').trim())||t('agg_noreq'); if(!map[k]) map[k]={req:k,count:0,rolls:0,pal:0,m:0,items:[]}; var g=map[k]; g.count++; g.rolls+=aggParseRolls_(r.rm); g.pal+=aggParsePal_(r.pl); g.m+=aggParseM_(r.rm); g.items.push(r); }); return Object.keys(map).map(function(k){return map[k];}).sort(function(a,b){ return (b.pal-a.pal)||(b.count-a.count)||String(a.req).localeCompare(String(b.req)); }); } function aggGroups_(){ return aggGroupsOf_(aggRows_()); } /* 依頼者別 集計表(印刷用HTML)。class名は呼び出し側のCSSに依存 */ function aggSummaryTableHtml_(arr){ var gs=aggGroupsOf_(arr); if(!gs.length) return ''; var tot={count:0,rolls:0,pal:0,m:0}; gs.forEach(function(g){ tot.count+=g.count; tot.rolls+=g.rolls; tot.pal+=g.pal; tot.m+=g.m; }); var body=gs.map(function(g){ return ''+pEsc(g.req)+''+aggNum_(g.pal)+''+aggNum_(g.rolls)+''+aggNum_(g.m)+''+g.count+''; }).join(''); body+=''+t('agg_total')+''+aggNum_(tot.pal)+''+aggNum_(tot.rolls)+''+aggNum_(tot.m)+''+tot.count+''; return ''+body+'
'+t('agg_req')+''+t('agg_pal')+''+t('agg_rolls')+''+t('agg_m')+''+t('agg_count')+'
'; } function openAgg(){ document.getElementById('agg-overlay').classList.add('open'); aggSetScope(aggScope); } function aggSetScope(s){ aggScope=s; ['stock','all'].forEach(function(k){ var b=document.getElementById('agg-sc-'+k); if(b){ var on=(k===aggScope); b.style.background=on?'var(--accent)':'var(--surface2)'; b.style.color=on?'#fff':'var(--text)'; b.style.borderColor=on?'var(--accent)':'var(--border)'; } }); renderAgg(); } function renderAgg(){ var el=document.getElementById('agg-body'); if(!el) return; var gs=aggGroups_(); if(!gs.length){ el.innerHTML='
'+t('agg_none')+'
'; return; } var tot={count:0,rolls:0,pal:0,m:0}; gs.forEach(function(g){ tot.count+=g.count; tot.rolls+=g.rolls; tot.pal+=g.pal; tot.m+=g.m; }); var th='style="border:1px solid var(--border);padding:7px 8px;background:#e8f4fa;font-size:12px;text-align:left;white-space:nowrap;"'; var td='style="border:1px solid var(--border);padding:7px 8px;font-size:13px;text-align:right;"'; var tdl='style="border:1px solid var(--border);padding:7px 8px;font-size:13px;text-align:left;"'; var h='' +''; gs.forEach(function(g){ h+='' +'' +'' +'' +'' +'' +'' +''; }); h+='' +'' +'' +'' +'' +'' +'' +'
'+t('agg_req')+''+t('agg_pal')+''+t('agg_rolls')+''+t('agg_m')+''+t('agg_count')+'
'+pEsc(g.req)+''+aggNum_(g.pal)+''+aggNum_(g.rolls)+''+aggNum_(g.m)+''+g.count+'
'+t('agg_total')+''+aggNum_(tot.pal)+''+aggNum_(tot.rolls)+''+aggNum_(tot.m)+''+tot.count+'
' +'
'+t('agg_scope_label')+':'+(aggScope==='stock'?t('agg_scope_stock'):t('agg_scope_all'))+' / 🖨 '+t('agg_print_one')+' = '+t('agg_req')+'ごと
'; el.innerHTML=h; } /* 印刷:依頼者別の集計表(上)+依頼者ごとの明細(下) */ function printAgg(){ var gs=aggGroups_(); if(!gs.length){ alert(t('agg_none')); return; } var tot={count:0,rolls:0,pal:0,m:0}; gs.forEach(function(g){ tot.count+=g.count; tot.rolls+=g.rolls; tot.pal+=g.pal; tot.m+=g.m; }); var sumRows=gs.map(function(g){ return ''+pEsc(g.req)+''+aggNum_(g.pal)+''+aggNum_(g.rolls)+''+aggNum_(g.m)+''+g.count+''; }).join(''); sumRows+=''+t('agg_total')+''+aggNum_(tot.pal)+''+aggNum_(tot.rolls)+''+aggNum_(tot.m)+''+tot.count+''; var details=gs.map(function(g){ return printAggDetailHtml_(g); }).join(''); var scopeLbl=(aggScope==='stock'?t('agg_scope_stock'):t('agg_scope_all')); var html=''+t('agg_title')+'' +'

👥 '+t('agg_title').replace('👥 ','')+'

'+t('agg_scope_label')+':'+pEsc(scopeLbl)+' / '+t('lbl_date')+':'+pNow()+'
' +''+sumRows+'
'+t('agg_req')+''+t('agg_pal')+''+t('agg_rolls')+''+t('agg_m')+''+t('agg_count')+'
' +'

'+t('lbl_test')+' / '+t('agg_req')+'

'+details +''; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } /* 依頼者1名だけ印刷(明細+その人の合計) */ function printAggOne(req){ var g=aggGroups_().filter(function(x){return String(x.req)===String(req);})[0]; if(!g){ alert(t('agg_none')); return; } var html=''+pEsc(g.req)+' '+t('agg_title')+'' +'

👥 '+pEsc(g.req)+'

'+(aggScope==='stock'?t('agg_scope_stock'):t('agg_scope_all'))+' / '+t('lbl_date')+':'+pNow()+'
' +printAggDetailHtml_(g) +''; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } /* 依頼者1グループの明細テーブル(No/製品名/入荷日/R数/パレット/状況)+合計行 */ function printAggDetailHtml_(g){ var rows2=g.items.map(function(r){ return ''+pEsc(r.no)+''+pEsc(r.product)+''+pEsc(r.date)+''+pEsc(r.rm)+''+pEsc(r.pl)+''+pEsc(r.status)+''; }).join(''); return '
'+pEsc(g.req)+' '+t('agg_pal')+' '+aggNum_(g.pal)+'/'+t('agg_rolls')+' '+aggNum_(g.rolls)+'/'+t('agg_m')+' '+aggNum_(g.m)+'/'+t('agg_count')+' '+g.count+t('agg_unit_case')+'
' +''+rows2+'
No'+t('lbl_product')+''+t('lbl_date')+''+t('lbl_rm')+''+t('lbl_pl')+''+t('lbl_status')+'
'; } function aggPrintCss_(){ return '@page{size:portrait;}body{font-family:sans-serif;padding:16px;color:#222;}h1{font-size:18px;margin:0 0 4px;}h2.dh{font-size:13px;margin:18px 0 6px;border-top:2px solid #0d5a8a;padding-top:8px;color:#0d5a8a;}.meta{font-size:11px;color:#555;margin-bottom:10px;}table{border-collapse:collapse;width:100%;font-size:12px;margin-bottom:6px;}th,td{border:1px solid #999;padding:5px 7px;text-align:left;vertical-align:top;}th{background:#e8f1f8;}td.n,th.n{text-align:right;}table.sum td.l{font-weight:600;}table.sum tr.tot{background:#f1f6fb;font-weight:800;}.grp{margin-bottom:14px;}.gh{font-size:13px;margin:10px 0 4px;}.gsub{font-size:11px;color:#555;font-weight:600;}@media print{.noprint{display:none;}}button.noprint{margin-top:14px;padding:8px 16px;font-size:14px;}'; } function renderCards() { const body = document.getElementById('app-body'); const kw = document.getElementById('search-text').value.trim().toLowerCase(); const filtered = getFilteredRows(); const countEl = document.getElementById('result-count'); const liveCount = rows.filter(r => !isDeleted(r)).length; const delCount = rows.filter(r => isDeleted(r)).length; // 廃棄申請バナー(承認者のみ・社長指示2026-06-22) const _dispBar = document.getElementById('disp-pending-bar'); if (_dispBar) { const _pend = rows.filter(r => !isDeleted(r) && r.status === '廃棄申請中').length; if (_pend > 0 && canDelete() && !viewTrash) { _dispBar.style.cssText = 'margin:6px 0 2px;padding:9px 12px;background:linear-gradient(135deg,#fff4e6,#ffe9d1);border:2px solid #e8762c;border-radius:10px;color:#9a4a00;font-weight:700;font-size:13px;cursor:pointer;'; _dispBar.textContent = t('pending_disp_banner').replace('{n}', _pend); } else { _dispBar.style.display = 'none'; } } if (viewTrash) { countEl.textContent = t('count_trash').replace('{n}', delCount); if (filtered.length === 0) { body.innerHTML = '
' + t('empty_trash') + '
'; return; } } else { if (liveCount === 0) { body.innerHTML = '
' + t('empty_data').replace('\n', '
') + '
'; countEl.textContent = ''; return; } const hasFilter = kw || document.getElementById('filter-stock').value || document.getElementById('filter-status').value; countEl.textContent = hasFilter ? t('count_filter').replace('{shown}', filtered.length).replace('{total}', liveCount) : t('count_all').replace('{n}', liveCount); if (filtered.length === 0) { body.innerHTML = '
' + t('empty_filter') + '
'; return; } } /* 在庫・状況の表示ラベルをt()で変換(保存値は日本語のまま) */ function stockLabel(v) { if (v === 'あり') return t('stock_yes'); if (v === 'なし') return t('stock_no'); return v || '—'; } function statusLabel(v) { if (v === '保管中') return t('status_stored'); if (v === '出荷済') return t('status_shipped'); if (v === '廃棄申請中') return t('status_pending_disposal'); if (v === '廃棄') return t('status_disposed'); return v || ''; } body.innerHTML = filtered.map(r => { const actions = viewTrash ? `${canDelete() ? `` : ''}${canHardDelete() ? `` : ''}` : `${canDelete() ? `` : ''}`; const dispActions = (!viewTrash && r.status === '廃棄申請中' && canDelete()) ? `` : ''; const delMeta = (viewTrash && (r.deletedBy || r.deletedAt)) ? `🗑 ${r.deletedBy || ''} ${r.deletedAt || ''}` : ''; const dates = [r.date, r.useDate].filter(Boolean).join(' → '); const noPhoto = t('no_photo').replace('\n', '
'); const _ph = photoList_(r.photo); const _first = _ph[0]; return `
${_first ? `${_ph.length > 1 ? `📷${_ph.length}` : ''}` : `${noPhoto}`}
${hl(t('card_no') + String(r.no), kw)}${dates ? ' ' + hl(dates, kw) + '' : ''}
${hl(r.product, kw) || '—'}
${t('card_requester')}${hl(r.requester, kw) || '—'}${r.test ? ' / ' + hl(r.test, kw) : ''}
${stockLabel(r.stock)} ${statusLabel(r.status)} ${delMeta}
${t('card_rm')}
${hl(r.rm, kw) || '—'}
${t('card_pl')}
${hl(r.pl, kw) || '—'}
${t('card_date')}
${hl(r.date, kw) || '—'}
${dispActions}${actions}
`; }).join(''); } function showImg(id) { const r = rows.find(r => String(r.id) === String(id)); if (!r || !r.photo) return; const ph = photoList_(r.photo); document.getElementById('img-viewer-body').innerHTML = ph.map(function (p) { return ''; }).join(''); document.getElementById('img-viewer').classList.add('open'); } function closeImg() { document.getElementById('img-viewer').classList.remove('open'); } function exportExcel() { const target = getFilteredRows(); if (target.length === 0) { showToast(t('toast_excel_none')); return; } const headers = ['登録No','製品名','依頼者名','入荷日','使用日','R数・M','P/L数','在庫','保管状況','試験内容・Lot・その他']; const data = target.map(r => [ 'No.' + String(r.no), r.product, r.requester, r.date, r.useDate, r.rm, r.pl, r.stock, r.status, r.test ]); const ws = XLSX.utils.aoa_to_sheet([headers, ...data]); ws['!cols'] = [{wch:8},{wch:28},{wch:12},{wch:12},{wch:12},{wch:14},{wch:8},{wch:6},{wch:10},{wch:30}]; const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '試験反在庫'); const stamp = jstDateStr().replace(/-/g, ''); XLSX.writeFile(wb, `試験反在庫管理_${stamp}.xlsx`); showToast(t('toast_excel_done').replace('{n}', target.length)); } /* ===== 🤖 在庫AIアシスタント(共用GAS経由・社長の方針=グラスボックス対応)===== */ let aiHistory = []; function openAIModal() { document.getElementById('ai-overlay').classList.add('open'); if (!aiHistory.length) { addAIMsg('ai', t('ai_welcome')); } setTimeout(() => { const i = document.getElementById('ai-input'); if (i) i.focus(); }, 100); } function closeAI() { document.getElementById('ai-overlay').classList.remove('open'); } function addAIMsg(role, html) { const box = document.getElementById('ai-messages'); const div = document.createElement('div'); div.className = 'ai-msg ' + (role === 'user' ? 'user' : 'ai'); div.innerHTML = html; box.appendChild(div); box.scrollTop = box.scrollHeight; return div; } function formatAIReply(text) { text = String(text || ''); text = text.replace(/&/g,'&').replace(//g,'>'); text = text.replace(/\*\*(.+?)\*\*/g, '$1'); text = text.replace(/`(.+?)`/g, '$1'); text = text.replace(/\n/g, '
'); return text; } function aiSystem() { const live = rows.filter(r => !isDeleted(r)); const ctx = live.slice(0, 300).map(r => `No.${String(r.no)} 製品:${r.product||''} 依頼者:${r.requester||''} 入荷:${r.date||''} 使用:${r.useDate||''} R数M:${r.rm||''} PL:${r.pl||''} 在庫:${r.stock||''} 状況:${r.status||''} 備考:${r.test||''}` ).join('\n'); return 'あなたは有限会社興和化学工業所の「試験反在庫管理」アシスタントです。以下は現在の在庫データ(実データ)。これに基づいて日本語で簡潔に答えてください。' + 'データに無いことは推測せず「データにありません」と答える。在庫の有無は最終的に現物確認を促す(断定保証しない)。\n\n' + '【現在の在庫データ(' + live.length + '件)】\n' + (ctx || '(データなし)'); } async function aiSend() { const input = document.getElementById('ai-input'); const msg = input.value.trim(); if (!msg) return; input.value = ''; addAIMsg('user', formatAIReply(msg)); aiHistory.push({ role: 'user', content: msg }); const btn = document.getElementById('ai-send'); btn.disabled = true; const typing = addAIMsg('ai', '…'); try { const data = await gas('ai', { app: '試験反', system: aiSystem(), messages: aiHistory, max_tokens: 900 }); const reply = (data && data.reply) || 'エラーが発生しました。'; aiHistory.push({ role: 'assistant', content: reply }); typing.innerHTML = formatAIReply(reply); } catch (e) { typing.innerHTML = '⚠️ 接続エラーが発生しました。時間をおいて再度お試しください。'; } btn.disabled = false; document.getElementById('ai-messages').scrollTop = document.getElementById('ai-messages').scrollHeight; } /* ===== 起動 ===== */ document.addEventListener('DOMContentLoaded', () => { buildLangGrids(); applyI18n(); loadUser(); initLogin(); if (currentUser) enterApp(); }); /* =================================================================== 🧪 試験加工依頼モジュール(依頼+配方薬剤+配合計算+ファイル添付+評価+在庫連携) 専用GAS(kowa-shiken-kakou)を使用。認証/AIは共有GAS(gas())のまま。 ================================================================= */ const SHIKEN_GAS_URL = 'https://script.google.com/macros/s/AKfycbzUSELHg6Gpao9S17i9TijVhQI7LnkOMhW67s_v3b-9ahKu1n-ARCfBXiVFjWvOANHW8g/exec'; /* 試験加工依頼 専用GAS(kowa-shiken-kakou) */ const SK_CATS=['主剤','溶剤','添加剤','補強剤','インク','カラー','硬化剤','その他']; const SK_MACHINES=['フレキソ','グラビア1','ラミネート','小巻1号機','小巻2号機','小巻3号機','小巻4号機','小巻5号機']; /* 試験機械プルダウン */ const SK_UNITS=['kg','g','mg','t','ml','L','本','枚','個','%']; /* 配方の単位 */ let skReqs=[], skChems=[], skEditId=null, skMatRows=[], skRecRows=[], skPendFiles=[], skPendPhotos=[], skMode='nodoc', skOcrFilled=false; let skFormOpen=false, skDirtyBase=''; /* 編集中(dirty)判定:消える問題の根治 */ let skExpandedId=null, skSeed=null; /* v4.6 インライン展開:一覧の中でその場で開いて編集(別ポップアップ廃止)。'__new__'=新規 */ let skAllCollapsed=false; /* v4.14 全カードの表示を一括「全部開く(全項目)/全部閉じる(1行)」既定=全部開く */ function skEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(m){return({'&':'&','<':'<','>':'>','"':'"'})[m];});} function skParse(s){ try{ var v=(typeof s==='string')?JSON.parse(s||'[]'):s; return Array.isArray(v)?v:[]; }catch(e){ return []; } } function skToday(){ var d=new Date(Date.now()+9*3600*1000); return d.getUTCFullYear()+'.'+(d.getUTCMonth()+1)+'.'+d.getUTCDate(); } /* JST日付化(kowa-app-check §2):GASがSheets日付型をUTC前日ISO(…T15:00:00.000Z)で返す「1日早い/生ISO」を吸収。 既に YYYY-MM-DD / YYYY.M.D 等で入っている手入力値はそのまま尊重。表示専用=保存値は触らない。 */ function jdate(v){ if(v==null||v==='') return ''; var s=String(v); // 既に日付文字列(区切り . / -)かつ T(時刻ISO)を含まない → そのまま(手入力の "2026.6.25" 等) if(/^\d{4}[-./]\d{1,2}[-./]\d{1,2}/.test(s) && s.indexOf('T')<0) return s; var d=new Date(v); if(isNaN(d.getTime())) return s.slice(0,10); var j=new Date(d.getTime()+9*3600*1000); var p=function(n){return ('0'+n).slice(-2);}; return j.getUTCFullYear()+'-'+p(j.getUTCMonth()+1)+'-'+p(j.getUTCDate()); } async function skGas(action,payload){ if(!SHIKEN_GAS_URL){ throw new Error('SHIKEN_GAS_URL未設定(専用GASのデプロイ待ち)'); } var res=await fetch(SHIKEN_GAS_URL,{method:'POST',headers:{'Content-Type':'text/plain;charset=utf-8'},body:JSON.stringify(Object.assign({action:action,token:APP_TOKEN},payload||{}))}); var data=await res.json(); if(data&&data.error) throw new Error(data.error); return data; } function skOpenOv(id){ document.getElementById(id+'-overlay').classList.add('open'); } function skClose(id){ document.getElementById(id+'-overlay').classList.remove('open'); } /* メインタブ切替:在庫一覧(inv) ⇄ 加工依頼(req)。加工依頼はポップアップでなくメイン画面に一体化 */ var mainView='req'; function switchMain(v){ mainView=(v==='req')?'req':'inv'; var inv=(mainView==='inv'); /* ページごとに色を変える:試験反在庫=緑 / 加工依頼=青(今どちらのページか一目で分かるように)*/ var rootS=document.documentElement.style; if(inv){ rootS.setProperty('--accent','#2c5f3f'); rootS.setProperty('--accent-light','#e8f2ec'); } else { rootS.setProperty('--accent','#0d5a8a'); rootS.setProperty('--accent-light','#e6f1f8'); } var ic=document.getElementById('inv-controls'); if(ic) ic.style.display=inv?'':'none'; var ab=document.getElementById('app-body'); if(ab) ab.style.display=inv?'':'none'; var rq=document.getElementById('view-req'); if(rq) rq.style.display=inv?'none':''; [['mtab-inv','inv'],['mtab-req','req']].forEach(function(p){ var b=document.getElementById(p[0]); if(b){ var on=(mainView===p[1]); b.style.background=on?'var(--accent)':'var(--surface2)'; b.style.color=on?'#fff':'var(--text)'; b.style.borderColor=on?'var(--accent)':'var(--border)'; b.style.fontWeight=on?'800':'600'; } }); if(!inv) skLoadReq_(); try{ window.scrollTo(0,0); }catch(e){} } /* 加工依頼ビューを読み込む(旧 skOpen の中身・オーバーレイは出さない) */ async function skLoadReq_(){ skExpandedId=null; skSeed=null; skFormOpen=false; skDirtyBase=''; var b=document.getElementById('sk-list-body'); if(b) b.innerHTML='
'; await skLoad(); } /* 後方互換:skOpen()=加工依頼タブへ切替 */ function skOpen(){ switchMain('req'); } async function skLoad(){ try{ var a=await skGas('get',{table:'shiken_requests'}); skReqs=Array.isArray(a)?a:[]; var b=await skGas('get',{table:'shiken_chemicals'}); skChems=Array.isArray(b)?b:[]; }catch(e){ showToast('読込失敗: '+e.message); } skRenderList(); } function skLive(){ return (skReqs||[]).filter(function(r){ return String(r.deleted)!=='true'; }); } function skChemByName(n){ return (skChems||[]).find(function(c){ return String(c.name)===String(n)&&String(c.name)!==''; }); } function skCost(r){ // 試験費用(新)。無ければ旧データの配合×単価から推定 var tc=parseFloat(r.testCost); if(!isNaN(tc)&&tc>0) return Math.round(tc); var rec=skParse(r.recipe), batch=parseFloat(r.batchKg)||0, cost=0; rec.forEach(function(x){ var parts=parseFloat(x.parts)||0; var c=skChemByName(x.name); var price=c?(parseFloat(c.unitPrice)||0):0; if(price>0&&batch>0) cost+=(parts/100)*batch*price; }); return Math.round(cost); } /* ----- 履歴メイン一覧 ----- */ function skVerdictBadge(v,score){ if(!v) return ''+t('sk_pending')+''; var c={'合格':'#2e7d32','条件付':'#e8920a','不合格':'#c62828'}[v]||'#888'; var lbl={'合格':t('sk_pass'),'条件付':t('sk_condpass'),'不合格':t('sk_fail')}[v]||v; return ''+lbl+(score?(' ★'+score):'')+''; } /* 長い文字列は省略(…)。見える化=1枚で要点が分かるが詰め込みすぎない */ function skClip(s,n){ s=String(s==null?'':s); return s.length>n?(s.slice(0,n)+'…'):s; } /* ===== 添付(PDF/Excel/写真)を「写真のように」カード表面に常時サムネ表示+クリックで原本 ===== */ function skFileExt_(n){ var m=String(n||'').toLowerCase().match(/\.([a-z0-9]+)(?:\?|$)/); return m?m[1]:''; } function skFileIcon_(ext){ if(/^(xlsx|xls|csv)$/.test(ext))return '📊'; if(ext==='pdf')return '📕'; if(/^(docx?|txt|rtf)$/.test(ext))return '📄'; if(/^(pptx?|key)$/.test(ext))return '📑'; if(/^(png|jpe?g|gif|webp|bmp|heic|heif)$/.test(ext))return '🖼️'; if(/^(zip|rar|7z)$/.test(ext))return '🗜️'; return '📎'; } /* Drive URL → 閲覧URL(/file/d//view)。それ以外はそのまま */ function skFileViewUrl_(u){ var s=String(u||''); var m=s.match(/(?:[?&]id=|\/d\/)([-\w]{20,})/); return m?('https://drive.google.com/file/d/'+m[1]+'/view'):s; } /* サムネ1枚(画像が出ない形式は絵文字アイコンにフォールバック) */ function skThumbCell_(imgSrc,icon,label,onclick,href){ var box='border:1px solid var(--border);border-radius:9px;overflow:hidden;width:82px;flex:0 0 auto;cursor:pointer;background:var(--surface);text-decoration:none;display:block;'; var inner='' +'' +'
'+icon+(label?(' '+skEsc(label)):'')+'
'; if(href) return ''+inner+''; return '
'+inner+'
'; } function skSurfaceThumbs_(r){ var files=skParse(r.files)||[], photos=(r.photo?photoList_(r.photo):[]), cells=[]; files.forEach(function(f){ if(!f||!f.url)return; var ext=skFileExt_(f.name); cells.push(skThumbCell_(driveImg_(f.url,240),skFileIcon_(ext),skClip(f.name||('file.'+ext),16),'',skFileViewUrl_(f.url))); }); photos.forEach(function(p){ cells.push(skThumbCell_(driveImg_(p,240),'🖼️','',"skShowReqPhotos('"+r.id+"')",'')); }); if(!cells.length) return ''; return '
'+cells.join('')+'
'; } /* 写真ライトボックス(メインアプリ共有の img-viewer を流用) */ function skShowReqPhotos(id){ var r=skReqs.find(function(x){return String(x.id)===String(id);}); if(!r||!r.photo)return; var ph=photoList_(r.photo); var b=document.getElementById('img-viewer-body'); if(!b)return; b.innerHTML=ph.map(function(p){return '';}).join(''); document.getElementById('img-viewer').classList.add('open'); } /* 編集中(インライン展開中)は検索で再描画しない=入力中の内容を消さない */ function skSearchInput(){ if(skExpandedId) return; skRenderList(); } /* ===== 一覧カードに「全データを整然と」表示(読み取り用・社長指示2026-06-29) ===== */ /* ラベル:値 の1行(値が空なら出さない=散らからない)。長文は折返し・改行保持 */ function skV_(label,val){ if(val==null||String(val).trim()==='') return ''; return '
'+skEsc(label)+''+skEsc(val)+'
'; } /* セクション小見出し */ function skSecH_(txt){ return '
'+skEsc(txt)+'
'; } /* 加工資材を1行文字列に */ function skMatsStr_(r){ return (skParse(r.materials)||[]).map(function(m){ return [m.maker,m.spec,(((m.qty!=null&&m.qty!=='')?m.qty:'')+(m.unit||''))].filter(function(x){return String(x).trim();}).join(' '); }).filter(Boolean).join(' / '); } /* 配方を1行文字列に */ function skRecsStr_(r){ return (skParse(r.recipe)||[]).map(function(x){ var nm=x.name||''; if(!nm)return''; var qu=((x.qty!=null&&x.qty!=='')?String(x.qty):'')+(x.unit||''); return nm+(qu?(' '+qu):''); }).filter(Boolean).join(' / '); } /* 1依頼の全データを整然と表示(読み取り用)。翻訳のある自由記述は表示言語で出す */ function skReqCardBody_(r){ var open=(String(skExpandedId)===String(r.id)); var f=function(field){ return trPickField(r,field,r[field]); }; var f1=((skParse(r.files)[0]||{}).name)||''; var title=skEsc(f('purpose')||r.no||r.client||f1||('No.'+r.id)); /* 全部閉じる=1行コンパクト(タイトル+依頼番号/依頼元/納期+評価)。開いている編集中カードは全表示 */ if(skAllCollapsed && !open){ var meta=[r.no, r.client, jdate(r.dueDate)].filter(function(x){return x&&String(x).trim();}).join(' / '); return '
' +'
'+((r.mode==='doc')?'📄 ':'')+title+'' +(meta?''+skEsc(meta)+'':'')+'
' +skVerdictBadge(r.evalVerdict,r.evalScore)+'
'; } var h='
' +''+(open?'▼ ':'')+((r.mode==='doc')?'📄 ':'')+title+'' +skVerdictBadge(r.evalVerdict,r.evalScore)+'
'; /* 📋 依頼内容 */ var reqB=skV_(t('sk_no'),r.no)+skV_(t('sk_client'),r.client)+skV_(t('sk_issue'),jdate(r.issueDate)) +skV_(t('sk_due'),jdate(r.dueDate)||(t('sk_no_due')))+skV_(t('sk_aim'),f('aim')) +skV_(t('sk_materials'),skMatsStr_(r))+skV_(t('sk_proc'),f('processContent'))+skV_(t('sk_cond'),f('processCond')); if(reqB) h+=skSecH_('📋 '+t('sk_reqinfo'))+reqB; /* 🧪 試験結果 */ var tt=[r.testStart,r.testEnd].filter(Boolean).join(' 〜 '); var resB=skV_(t('sk_testdate'),jdate(r.testDate))+skV_(t('sk_machine'),r.machine)+skV_(t('sk_testtime'),tt) +skV_(t('sk_temp'),r.temp)+skV_(t('sk_humidity'),r.humidity)+skV_(t('sk_testcontent'),f('testContent')) +skV_(t('sk_remarks'),f('remarks'))+skV_(t('sk_testcost'),(r.testCost?('¥'+(Number(r.testCost).toLocaleString())):'')); if(resB) h+=skSecH_('🧪 '+t('sk_result_h'))+resB; /* 🧬 配方 */ var rec=skRecsStr_(r); if(rec) h+=skSecH_('🧬 '+t('sk_recipe'))+'
'+skEsc(rec)+'
'; /* 📈 評価 */ if(r.evalVerdict||(r.evalComment&&String(r.evalComment).trim())){ h+=skSecH_('📈 '+t('sk_eval')); if(r.evalVerdict) h+='
'+skVerdictBadge(r.evalVerdict,r.evalScore)+'
'; h+=skV_(t('sk_comment'),r.evalComment); } /* 💰 原価・📦 在庫連携 */ var cost=skCost(r); var inv=(cost>0?('💰 ¥'+cost.toLocaleString()):'')+(r.inventoryId?(' 📦 '+t('sk_linked')+(r.inventoryId?('(No.'+skEsc(r.inventoryId)+')'):'')):''); if(inv.trim()) h+='
'+inv+'
'; /* 🖊 登録者・登録日(ありったけの情報=最後に小さく) */ var ca=jdate(r.createdAt); if((r.createdBy&&String(r.createdBy).trim())||ca){ h+='
🖊 '+skEsc(r.createdBy||'—')+(ca?(' 📅 '+skEsc(ca)):'')+'
'; } return h; } function skRenderList(){ var q=(document.getElementById('sk-search').value||'').toLowerCase(); var list=skLive().slice().sort(function(a,b){ return String(b.createdAt||'').localeCompare(String(a.createdAt||'')); }); if(q) list=list.filter(function(r){ return JSON.stringify(r).toLowerCase().indexOf(q)>=0; }); var el=document.getElementById('sk-list-body'); var canM=canManage(); var html=''; /* 新規依頼:別ポップアップを出さず、一覧トップにその場で開くエディタ */ if(skExpandedId==='__new__'){ html+='
' +'
'+t('sk_new')+'
' +'
'; } if(!list.length && skExpandedId!=='__new__'){ el.innerHTML='
'+t('sk_none')+'
'; return; } html+=list.map(function(r){ var open=(String(skExpandedId)===String(r.id)); var aBtn='style="flex:1;border:1px solid var(--border);background:var(--surface2);border-radius:8px;padding:8px 4px;font-size:13px;font-weight:700;cursor:pointer;color:var(--text);"'; /* カードに全データを整然と表示(読み取り用)。タップでその場展開して編集 */ var summary='
'+skReqCardBody_(r)+'
'; /* 全部閉じる時は本体だけ(アクション/サムネは隠してコンパクトに) */ var actions=open?'
' :(skAllCollapsed?'':('
' +'' +'' +(canM?(''):'') +'
')); var compact=(skAllCollapsed&&!open); return '
' +summary +(compact?'':skSurfaceThumbs_(r)) +actions +'
'; }).join(''); el.innerHTML=html; var tb=document.getElementById('sk-toggle-all'); if(tb) tb.innerHTML = skAllCollapsed?('▶ '+t('sk_expand_all')+''):('🔽 '+t('sk_collapse_all')+''); if(skExpandedId) skFillInline(); } /* 全カードを一括で「全部開く(全項目)/全部閉じる(1行)」*/ function skToggleAll(){ skAllCollapsed=!skAllCollapsed; skRenderList(); } /* タップでその場展開/再タップで閉じる */ function skToggleCard(id){ if(String(skExpandedId)===String(id)){ skCollapseInline(); } else { skForm(id); } } /* 一覧の中にフォームを描画(別ポップアップ廃止)。skExpandedId のカードに skFormHtml を流し込む */ function skFillInline(){ var id=skExpandedId; if(!id) return; var isNew=(id==='__new__'); var r=isNew?(skSeed||{}):(skReqs.find(function(x){return String(x.id)===String(id);})||{}); var target=document.getElementById('sk-exp-'+id); if(!target) return; skEditId=isNew?null:id; skMode=(r.mode==='doc'||r.mode==='nodoc')?r.mode:'nodoc'; skOcrFilled=(r.mode==='doc' && !!(r.no||r.client||r.purpose||r.aim||r.processContent||skParse(r.materials).length)); skMatRows=skParse(r.materials); if(!skMatRows.length) skMatRows=[{maker:'',spec:'',qty:'',unit:''}]; skRecRows=skParse(r.recipe).map(function(x){ return {name:x.name||'',qty:(x.qty!=null&&x.qty!=='')?x.qty:(x.parts!=null?x.parts:''),unit:x.unit||''}; }); if(!skRecRows.length) skRecRows=[{name:'',qty:'',unit:''}]; skPendFiles=skParse(r.files); skPendPhotos=r.photo?photoList_(r.photo):[]; target.innerHTML=skFormHtml(r) +'
' +'' +'' +'
'; skRenderMats(); skRenderRecs(); skRenderFiles(); skRenderFPhotos(); skApplyMode(); skFormOpen=true; skDirtyBase=skFormSnapshot(); } /* 閉じる(編集中なら確認) */ function skCollapseInline(){ if(skIsDirty() && !confirm(t('sk_discard_confirm'))) return; skExpandedId=null; skSeed=null; skFormOpen=false; skDirtyBase=''; skRenderList(); } /* ----- 依頼フォーム ----- */ /* id を渡すと編集、seed(コピー元の引き継ぎオブジェクト)を渡すと内容を引き継いだ新規 */ /* v4.6 インライン展開エントリ:id=既存編集/無し=新規(一覧トップに展開)。別ポップアップは廃止 */ function skForm(id,seed){ var target=id?String(id):'__new__'; // 別カードを編集中で内容に変更があれば確認 if(skExpandedId && String(skExpandedId)!==target && skIsDirty() && !confirm(t('sk_discard_confirm'))) return; skExpandedId=target; skSeed=seed||null; skRenderList(); // 末尾で skFillInline() が該当カードにフォームを描画する var c=document.getElementById('sk-card-'+target); if(c){ try{ c.scrollIntoView({block:'start',behavior:'smooth'}); }catch(e){} } } /* 📋 コピー(複製):内容を引き継いだ新規依頼を開く。番号・各日付・試験結果・評価・在庫連携・翻訳はクリア=「良かったらコピー」 */ function skCopyReq(id){ if(!canManage()){ showToast(t('no_perm')||'権限がありません'); return; } var s=skReqs.find(function(x){return String(x.id)===String(id);}); if(!s) return; var seed={ mode:(s.mode==='doc'||s.mode==='nodoc')?s.mode:'nodoc', // 引き継ぐ=同種の試験を繰り返すための再利用フィールド client:s.client||'', purpose:s.purpose||'', aim:s.aim||'', materials:s.materials||'', recipe:s.recipe||'', processContent:s.processContent||'', processCond:s.processCond||'', machine:s.machine||'' // クリア=no/issueDate/dueDate/testDate/testStart/testEnd/temp/humidity/testContent/remarks/ // testCost/evalVerdict/evalScore/evalComment/files/photo/inventoryId/tr/src(seedに入れない=空のまま) }; skForm(null,seed); showToast(t('sk_copy_done')); } /* 依頼書あり/なし切替:依頼内容ブロックの表示を切り替える(入力値は消さない=show/hide方式) */ function skSetMode(m){ skMode=m; skApplyMode(); } function skApplyMode(){ var req=document.getElementById('sk-req-block'), doc=document.getElementById('sk-doc-block'); // 依頼内容ブロックは「依頼書なし」のとき、または「依頼書あり+AI読み取り済」のとき表示(確認・修正用) if(req) req.style.display=(skMode==='nodoc'||skOcrFilled)?'':'none'; if(doc) doc.style.display=(skMode==='doc')?'':'none'; var head=document.getElementById('sk-req-head'); if(head) head.innerHTML=(skMode==='doc'&&skOcrFilled)?('🤖 '+t('sk_reqinfo')+' ('+t('sk_ocr_badge')+')'):('📝 '+t('sk_reqinfo')); ['sk-mode-doc','sk-mode-nodoc'].forEach(function(bid){ var b=document.getElementById(bid); if(b){ var on=(bid==='sk-mode-'+skMode); b.style.background=on?'var(--accent)':'var(--surface2)'; b.style.color=on?'#fff':'var(--text)'; b.style.borderColor=on?'var(--accent)':'var(--border)'; } }); } /* ===== 消える問題の根治:編集中(dirty)なら確認してから閉じる。背景タップでは閉じない ===== */ /* フォームの全入力+行データ+添付を1つの文字列にまとめて、開いた時との差分で「編集したか」を判定 */ function skFormSnapshot(){ var ids=['sk-no','sk-issue','sk-due','sk-client','sk-purpose','sk-aim','sk-proc','sk-cond', 'sk-tdate','sk-tstart','sk-tend','sk-temp','sk-humidity','sk-machine','sk-tcontent','sk-remarks', 'sk-tcost','sk-verdict','sk-score','sk-ecomment']; var vals=ids.map(function(id){ var el=document.getElementById(id); return el?String(el.value||''):''; }); try{ return JSON.stringify({v:vals,m:skMode,mat:skMatRows,rec:skRecRows,f:skPendFiles,p:(skPendPhotos||[]).length}); } catch(e){ return vals.join(''); } } function skIsDirty(){ if(!skFormOpen) return false; try{ return skFormSnapshot()!==skDirtyBase; }catch(e){ return false; } } /* 明示クローズ(✕/閉じる)専用。dirtyなら破棄確認。背景クリックでは絶対に呼ばない */ function skCloseForm(){ if(skIsDirty() && !confirm(t('sk_discard_confirm'))) return; skFormOpen=false; skDirtyBase=''; skClose('sk-form'); } /* ===== 🤖 依頼書 AI読み取り(OCR):写真/PDF/Excel → Claude vision → 依頼内容・配方を自動入力 ===== */ function skOcrStatus(msg,isErr){ var el=document.getElementById('sk-ocr-status'); if(el) el.innerHTML=''+skEsc(msg)+''; } function skTopOcrStatus(msg,isErr){ var el=document.getElementById('sk-top-ocr-status'); if(el) el.innerHTML=msg?(''+skEsc(msg)+''):''; } function skOcrHasData(x){ return !!(x&&(x.no||x.client||x.purpose||x.aim||x.processContent||x.processCond||(Array.isArray(x.materials)&&x.materials.length)||(Array.isArray(x.recipe)&&x.recipe.length))); } /* 読み取り結果 → 新規依頼フォーム(依頼書あり)を開いて自動入力(=振り分け)。fileがあれば原本も添付 */ async function skOcrRoute(x,file){ skTopOcrStatus(''); skForm(); // 新規 skSetMode('doc'); if(file){ var lower=(file.name||'').toLowerCase(), isXlsx=/\.(xlsx|xls|csv)$/.test(lower); if(!isXlsx){ skOcrStatus(t('sk_ocr_attach')); try{ var durl=await skReadFile(file); var sv=await skGas('savefile',{name:file.name,mime:file.type,dataUrl:durl}); if(sv&&sv.url){ skPendFiles.push({name:sv.name||file.name,url:sv.url}); skRenderFiles(); } }catch(e){} } } skApplyOcr(x); skOcrStatus(t('sk_ocr_done')); } /* 一覧トップ:📷カメラ / 🖼️ファイル(写真・PDF・Excel)→ AIで読む→振り分け */ async function skOcrFromTop(input){ if(!canManage()){ showToast(t('no_perm')||'権限がありません'); return; } var f=input.files&&input.files[0]; input.value=''; if(!f) return; if(f.size>25*1024*1024){ skTopOcrStatus('25MB以下のファイルにしてください',true); return; } var lower=(f.name||'').toLowerCase(), isXlsx=/\.(xlsx|xls|csv)$/.test(lower); skTopOcrStatus(isXlsx?t('sk_ocr_xlsx'):t('sk_ocr_busy')); var x; try{ x=await skOcrExtract(f); } catch(e){ skTopOcrStatus(t('sk_ocr_fail')+(e.message||e),true); return; } if(!skOcrHasData(x)){ skTopOcrStatus(t('sk_ocr_empty'),true); return; } await skOcrRoute(x,f); } /* 一覧トップ:✍️文章を貼り付け→ AIで読む→振り分け */ async function skOcrFromText(){ if(!canManage()){ showToast(t('no_perm')||'権限がありません'); return; } var txt=window.prompt(t('sk_ocr_paste_prompt')); if(!txt||!String(txt).trim()) return; skTopOcrStatus(t('sk_ocr_busy')); var x; try{ x=await skOcrExtractText(txt); } catch(e){ skTopOcrStatus(t('sk_ocr_fail')+(e.message||e),true); return; } if(!skOcrHasData(x)){ skTopOcrStatus(t('sk_ocr_empty'),true); return; } await skOcrRoute(x,null); } async function skOcrPick(input){ var f=input.files&&input.files[0]; input.value=''; if(!f) return; if(f.size>15*1024*1024){ skOcrStatus('15MB以下のファイルにしてください',true); return; } // 1) 原本を添付(Driveに保存)※画像/PDFのみ。Excelは添付せず読み取りのみ var lower=(f.name||'').toLowerCase(), isXlsx=/\.(xlsx|xls|csv)$/.test(lower); if(!isXlsx){ skOcrStatus(t('sk_ocr_attach')); try{ var durl=await skReadFile(f); var sv=await skGas('savefile',{name:f.name,mime:f.type,dataUrl:durl}); if(sv&&sv.url){ skPendFiles.push({name:sv.name||f.name,url:sv.url}); skRenderFiles(); } }catch(e){} } // 2) AI読み取り skOcrStatus(isXlsx?t('sk_ocr_xlsx'):t('sk_ocr_busy')); try{ var x=await skOcrExtract(f); if(!x||!(x.no||x.client||x.purpose||x.aim||x.processContent||(Array.isArray(x.materials)&&x.materials.length)||(Array.isArray(x.recipe)&&x.recipe.length))){ skOcrStatus(t('sk_ocr_empty'),true); return; } skApplyOcr(x); skOcrStatus(t('sk_ocr_done')); }catch(e){ skOcrStatus(t('sk_ocr_fail')+(e.message||e),true); } } function skFileB64_(f){ return new Promise(function(res,rej){ var r=new FileReader(); r.onload=function(){ var s=String(r.result||''); var m=/^data:.*?;base64,(.*)$/.exec(s); res(m?m[1]:''); }; r.onerror=rej; r.readAsDataURL(f); }); } function skEnsureXLSX(){ return new Promise(function(res,rej){ if(window.XLSX) return res(); var s=document.createElement('script'); s.src='https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js'; s.onload=function(){res();}; s.onerror=function(){rej(new Error('Excelライブラリを読み込めません'));}; document.head.appendChild(s); }); } async function skXlsxText_(f){ await skEnsureXLSX(); var buf=await f.arrayBuffer(); var wb=XLSX.read(buf,{type:'array'}); var out=''; wb.SheetNames.forEach(function(n){ out+='【'+n+'】\n'+XLSX.utils.sheet_to_csv(wb.Sheets[n])+'\n'; }); return out.slice(0,12000); } var SK_OCR_SYS='あなたは工業用フィルム/シートの「試験加工依頼書(注文書・試作指示書)」を読み取る担当です。書かれている内容だけを抽出し、指定のJSONだけを返します。説明や```(コードフェンス)は一切付けません。読めない項目は空文字、勝手に創作しない。'; var SK_OCR_PROMPT='これは取引先からの試験加工依頼書です。記載内容を読み取り、次のJSONだけを返してください(前後に文章や```を付けない)。\n' +'{"no":"依頼番号/注文番号","client":"依頼元/取引先名","purpose":"用途","aim":"目的","dueDate":"希望納期","processContent":"加工内容","processCond":"加工条件(温度/速度/張力など)","machine":"指定の試験機械があれば次のいずれか厳密一致[フレキソ,グラビア1,ラミネート,小巻1号機,小巻2号機,小巻3号機,小巻4号機,小巻5号機]・無ければ\\"\\"","materials":[{"maker":"メーカー","spec":"規格/品名","qty":"数量","unit":"単位"}],"recipe":[{"name":"薬剤名","qty":"数量","unit":"単位(kg/g/ml等)"}]}\n' +'注意: 配方(薬剤)は薬剤名・数量・単位を正確に分けて。数量と単位を取り違えない。合計や小計の行は除外。試験後に決まる結果(試験日・実測の温湿度・評価)は依頼書に無いので空でよい。'; async function skOcrAI_(content){ var res=await gas('ai',{app:'試験加工',system:SK_OCR_SYS,messages:[{role:'user',content:content}],max_tokens:2000}); return skParseJsonReply_((res&&res.reply)||''); } /* ファイル(カメラ写真/画像/PDF/Excel)→ Claude vision/読み取り */ async function skOcrExtract(f){ var content, lower=(f.name||'').toLowerCase(); if(f.type==='application/pdf'||/\.pdf$/.test(lower)){ var b64=await skFileB64_(f); if(!b64) throw new Error('PDFを読み込めません'); content=[{type:'document',source:{type:'base64',media_type:'application/pdf',data:b64}},{type:'text',text:SK_OCR_PROMPT}]; } else if((f.type&&f.type.indexOf('image/')===0)||/\.(jpe?g|png|gif|webp|bmp|heic|heif)$/.test(lower)){ var raw=await skReadFile(f); var du=await compressDataUrl_(raw,1600,0.82); var m=/^data:(.*?);base64,(.*)$/.exec(du); if(!m) throw new Error('画像を変換できません'); content=[{type:'image',source:{type:'base64',media_type:m[1],data:m[2]}},{type:'text',text:SK_OCR_PROMPT}]; } else if(/\.(xlsx|xls|csv)$/.test(lower)){ var txt=await skXlsxText_(f); content=[{type:'text',text:SK_OCR_PROMPT+'\n\n――― 依頼書(Excel)の中身 ―――\n'+txt}]; } else { throw new Error(t('sk_ocr_unsupport')); } return await skOcrAI_(content); } /* 文章(貼り付け・メール本文・チャット等)→ 読み取り */ async function skOcrExtractText(txt){ return await skOcrAI_([{type:'text',text:SK_OCR_PROMPT+'\n\n――― 依頼書の文章 ―――\n'+String(txt).slice(0,12000)}]); } function skParseJsonReply_(s){ s=String(s||'').replace(/```json/gi,'').replace(/```/g,'').trim(); var i=s.indexOf('{'), j=s.lastIndexOf('}'); if(i<0||j<0||j=0){ var ms=document.getElementById('sk-machine'); if(ms) ms.value=String(x.machine).trim(); } if(Array.isArray(x.materials)&&x.materials.length){ skMatRows=x.materials.map(function(m){return {maker:(m.maker||''),spec:(m.spec||m.name||''),qty:(m.qty==null?'':String(m.qty)),unit:(m.unit||'')};}); skRenderMats(); } if(Array.isArray(x.recipe)&&x.recipe.length){ skRecRows=x.recipe.map(function(r){return {name:(r.name||''),qty:(r.qty==null?'':String(r.qty)),unit:(r.unit||'')};}); skRenderRecs(); } skOcrFilled=true; skApplyMode(); var head=document.getElementById('sk-req-head'); if(head) head.scrollIntoView({block:'nearest'}); } function skFld(lbl,id,val,ph){ return '
'; } function skFormHtml(r){ var canM=canManage(); var btnAdd='style="background:var(--surface2);border:1px dashed var(--border);border-radius:8px;padding:6px 10px;font-size:12px;"'; var h=''; // ① 依頼書あり/なし を最初に選択 h+='
'+t('sk_mode_hint')+'
'; h+='
' +'' +'' +'
'; // ② 依頼書あり:ヒント+🤖AIで依頼書を読み取り→自動入力 h+=''; // ③ 依頼書なし:従来どおり依頼内容を入力(依頼書ありでもAI読み取り後は確認用に表示) h+='
'; h+='
📝 '+t('sk_reqinfo')+'
'; h+='
'+skFld(t('sk_no'),'sk-no',r.no,'5798H2H-R030')+skFld(t('sk_issue'),'sk-issue',jdate(r.issueDate),skToday())+'
'; h+='
'+skFld(t('sk_due'),'sk-due',jdate(r.dueDate),'')+skFld(t('sk_client'),'sk-client',r.client,'')+'
'; h+='
'+skFld(t('sk_purpose'),'sk-purpose',r.purpose,'ルーフィング')+skFld(t('sk_aim'),'sk-aim',r.aim,'')+'
'; h+='
'+t('sk_materials')+'
'; h+='
'+trReadLine(r,'processContent')+'
'; h+='
'+trReadLine(r,'processCond')+'
'; h+='
'; // /sk-req-block // ④ 試験結果(両モード共通・試験後に入力) h+='
🧪 '+t('sk_result_h')+'
'; h+='
' +'
'; h+='
' +'
'; h+='
'+skFld(t('sk_temp'),'sk-temp',r.temp,'例:23℃')+skFld(t('sk_humidity'),'sk-humidity',r.humidity,'例:50%')+'
'; h+='
'+trReadLine(r,'testContent')+'
'; h+='
'+trReadLine(r,'remarks')+'
'; h+=skFld(t('sk_testcost'),'sk-tcost',r.testCost,'例:15000'); // 配方(薬剤名・数量・単位の複数行) h+='
'+t('sk_recipe')+'
'; h+=''+(skChems||[]).filter(function(c){return c.name&&String(c.active)!=='false';}).map(function(c){return '';}).join('')+''; h+='
'; // ファイル添付(依頼書・資料)+写真 h+='
'+t('sk_files')+'
'; h+=''; h+='
'+t('sk_photo')+'
'; h+=''; // 評価 h+='
'+t('sk_eval')+'
'; h+='
'; h+='
'; h+='
'; // 在庫連携 if(r.inventoryId){ h+='
📦 '+t('sk_linked')+'(No.'+skEsc(r.inventoryId)+')
'; } else if(r.id){ h+=''; } if(r.id&&canM){ h+=''; } return h; } function skRenderMats(){ var el=document.getElementById('sk-mats'); if(!el)return; el.innerHTML=skMatRows.map(function(m,i){ return '
'; }).join(''); } function skAddMat(){ skMatRows.push({maker:'',spec:'',qty:'',unit:''}); skRenderMats(); } /* 配方:薬剤名(自由入力+マスター候補)/数量/単位 の複数行 */ function skRenderRecs(){ var el=document.getElementById('sk-recs'); if(!el)return; var uopts=SK_UNITS.map(function(u){return '';}).join(''); el.innerHTML=skRecRows.map(function(rc,i){ return '
' +'' +'' +'' +'
'; }).join('')+''+uopts+''; } function skAddRec(){ skRecRows.push({name:'',qty:'',unit:''}); skRenderRecs(); } /* ファイル添付(Drive) */ async function skFilePick(input){ var files=Array.from(input.files||[]); input.value=''; for(var i=0;i8*1024*1024){ showToast(f.name+': 8MB以下にしてください'); continue; } try{ var dataUrl=await skReadFile(f); var res=await skGas('savefile',{name:f.name,mime:f.type,dataUrl:dataUrl}); if(res&&res.url){ skPendFiles.push({name:res.name||f.name,url:res.url}); skRenderFiles(); } } catch(e){ showToast('添付失敗: '+e.message); } } } function skReadFile(f){ return new Promise(function(res,rej){ var r=new FileReader(); r.onload=function(){res(r.result);}; r.onerror=rej; r.readAsDataURL(f); }); } function skRenderFiles(){ document.getElementById('sk-files').innerHTML=(skPendFiles||[]).map(function(f,i){ return '
📄 '+skEsc(f.name)+'
'; }).join(''); } async function skPhotoPick(input){ var files=Array.from(input.files||[]); input.value=''; for(var i=0;i'; }).join(''); } /* 保存 */ /* ===== 双方向7言語(自由記述を7言語に翻訳保存+自分の言語で読む)=寮/安全と同方式 ===== */ var TR7=['ja','en','vi','si','id','fil','zh']; function trVlang(){ try{ return (typeof _lang!=='undefined'&&_lang)||document.documentElement.lang||'ja'; }catch(e){ return 'ja'; } } function trNorm7(o){ var m={}; TR7.forEach(function(l){ m[l]=(o&&typeof o==='object')?String(o[l]||'').trim():''; }); return m; } function trPickField(r,field,fb){ try{ var m=(typeof r.tr==='string')?JSON.parse(r.tr):r.tr; var node=m&&m[field]; if(node&&typeof node==='object'){ var L=trVlang(); return node[L]||node.ja||fb||''; } }catch(e){} return fb||''; } function trReadLine(r,field){ if(!r||!r.tr)return ''; try{ var m=(typeof r.tr==='string')?JSON.parse(r.tr):r.tr; var node=m&&m[field]; if(node&&typeof node==='object'){ var L=trVlang(); if(String(r.src||'')===L)return ''; var v=node[L]; if(v&&v!==(r[field]||'')) return '
🌐 '+skEsc(v)+'
'; } }catch(e){} return ''; } async function trAI7_(sys,user,maxtok){ try{ var d=await gas('ai',{app:'試験加工翻訳',system:sys,messages:[{role:'user',content:user}],max_tokens:maxtok||1800}); var raw=String((d&&d.reply)||'').trim().replace(/^```[a-zA-Z]*/,'').replace(/```$/,'').trim(); var mm=raw.match(/\{[\s\S]*\}/); return JSON.parse(mm?mm[0]:raw); }catch(e){ return null; } } async function trFields7(f, keys){ keys=keys.filter(function(k){ return f[k]&&String(f[k]).trim(); }); if(!keys.length) return {src:'',tr:null}; var sys='You are a precise translation engine for factory/lab work records written by Japanese staff or foreign technical interns. Detect the source language, then translate each given field into all 7 languages (ja,en,vi,si,id,fil,zh) naturally and simply for workers. Keep proper nouns, numbers, machine names, chemical names, emojis and line breaks intact; never change meaning or add content. Output JSON only — no prose, no code fences.'; var shape='"src":"ja|en|vi|si|id|fil|zh"'; keys.forEach(function(k){ shape+=', "'+k+'":{"ja":"","en":"","vi":"","si":"","id":"","fil":"","zh":""}'; }); var user='Fields:\n'+keys.map(function(k){return k+': '+f[k];}).join('\n')+'\n\nReturn JSON EXACTLY: {'+shape+'}\nsrc=detected language of the fields. Put the original text in the src language slot.'; var obj=await trAI7_(sys,user,1900); if(!obj) return {src:'',tr:null}; var src=TR7.indexOf(obj.src)>=0?obj.src:'ja'; var tr={}; keys.forEach(function(k){ var m=trNorm7(obj[k]); if(!m[src])m[src]=f[k]; TR7.forEach(function(l){ if(!m[l])m[l]=m[src]||f[k]; }); tr[k]=m; }); return {src:src, tr:tr}; } async function skSave(){ if(!canManage()){ showToast(t('no_perm')||'権限がありません'); return; } var g=function(id){ var el=document.getElementById(id); return el?el.value:''; }; var tcontent=g('sk-tcontent'); var rec={ mode:skMode, // 試験結果(両モード共通) testDate:g('sk-tdate'),testStart:g('sk-tstart'),testEnd:g('sk-tend'),temp:g('sk-temp'),humidity:g('sk-humidity'), machine:g('sk-machine'),testContent:tcontent,remarks:g('sk-remarks'),testCost:g('sk-tcost'), result:tcontent, // 互換:旧resultにも試験内容を反映 recipe:JSON.stringify(skRecRows.filter(function(r){return r.name||r.qty||r.unit;})), files:JSON.stringify(skPendFiles), evalVerdict:g('sk-verdict'),evalScore:g('sk-score'),evalComment:g('sk-ecomment'), createdBy:(currentUser&¤tUser.name)||'' }; // 依頼内容:依頼書なし、または 依頼書あり+AI読み取り済 のとき保存(AIが読み取った内容を構造化データとして残す) if(skMode==='nodoc'||skOcrFilled){ rec.no=g('sk-no'); rec.issueDate=g('sk-issue'); rec.dueDate=g('sk-due'); rec.client=g('sk-client'); rec.purpose=g('sk-purpose'); rec.aim=g('sk-aim'); rec.materials=JSON.stringify(skMatRows.filter(function(m){return m.maker||m.spec||m.qty;})); rec.processContent=g('sk-proc'); rec.processCond=g('sk-cond'); } try{ rec.photo=skPendPhotos.length?await packPhotos_(skPendPhotos):''; }catch(e){ rec.photo=skPendPhotos[0]||''; } // 🌐 双方向7言語:自由記述(用途/目的/加工内容/条件/試験内容/備考)を7言語に翻訳して保存(書いた言語が全員に伝わる) try{ var _tf={ purpose:rec.purpose||'', aim:rec.aim||'', processContent:rec.processContent||'', processCond:rec.processCond||'', testContent:rec.testContent||'', remarks:rec.remarks||'' }; var _t=await trFields7(_tf,['purpose','aim','processContent','processCond','testContent','remarks']); if(_t&&_t.tr){ rec.tr=JSON.stringify(_t.tr); rec.src=_t.src||''; } }catch(e){} try{ if(skEditId){ await skGas('update',{table:'shiken_requests',id:skEditId,fields:rec}); } else { await skGas('add',{table:'shiken_requests',row:rec}); } showToast('💾 '+(t('btn_save')||'保存')); skExpandedId=null; skSeed=null; skFormOpen=false; skDirtyBase=''; await skLoad(); }catch(e){ showToast('保存失敗: '+e.message); } } async function skDelReq(id){ if(!canManage()) return; if(!confirm('🗑 '+(t('sk_act_del')||'削除')+' ?')) return; try{ await skGas('update',{table:'shiken_requests',id:id,fields:{deleted:'true',deletedBy:(currentUser&¤tUser.name)||'',deletedAt:skToday()}}); skExpandedId=null; skSeed=null; skFormOpen=false; skDirtyBase=''; await skLoad(); showToast(t('toast_deleted')||'削除しました'); } catch(e){ showToast('失敗: '+e.message); } } /* 在庫連携:合格依頼→試験反在庫に登録(共有GAS inventory) */ async function skToInventory(id){ var r=skReqs.find(function(x){return String(x.id)===String(id);}); if(!r) return; if(!confirm('📦 この依頼を試験反在庫に登録しますか?')) return; var rec=skParse(r.recipe), chems=rec.map(function(x){return x.name;}).filter(Boolean).join('・'); var f1=(skParse(r.files)[0]&&skParse(r.files)[0].name)||''; var invRow={ no:getNextNo(), date:skToday(), product:(r.purpose||r.aim||r.no||f1||'試作品'), requester:(r.client||''), test:'試験加工依頼 '+(r.no||('No.'+r.id))+(chems?(' / 配方:'+chems):'')+(r.evalVerdict?(' / 評価:'+r.evalVerdict):''), stock:'あり', status:'保管中', photo:'' }; try{ var res=await gas('add',{table:'inventory',row:invRow}); var invId=(res&&res.id)||''; await skGas('update',{table:'shiken_requests',id:id,fields:{inventoryId:String(invId)}}); if(typeof loadRows==='function') await loadRows(); skExpandedId=null; skSeed=null; skFormOpen=false; skDirtyBase=''; await skLoad(); showToast('📦 在庫に登録しました(No.'+invRow.no+')'); }catch(e){ showToast('在庫登録失敗: '+e.message); } } /* ----- 薬剤マスター ----- */ async function skOpenChems(){ skOpenOv('sk-chems'); skRenderChems(); } function skRenderChems(){ var el=document.getElementById('sk-chems-body'); var canM=canManage(); var h='
'+t('sk_costnote')+'
'; h+='
'; var live=(skChems||[]).filter(function(c){return c.name&&String(c.active)!=='false';}); if(!live.length){ h+='
'+t('sk_none')+'
'; } else { h+=live.map(function(c){ return '
'+skEsc(c.name)+''+skEsc(c.category||'')+'' +'' +'¥/kg' +(canM?'':'')+'
'; }).join(''); } el.innerHTML=h; } async function skAddChem(){ var name=(document.getElementById('sk-nc-name').value||'').trim(); if(!name){ showToast(t('sk_cname')); return; } var cat=document.getElementById('sk-nc-cat').value; var price=document.getElementById('sk-nc-price').value; try{ await skGas('add',{table:'shiken_chemicals',row:{name:name,category:cat,unitPrice:price,active:'true'}}); var b=await skGas('get',{table:'shiken_chemicals'}); skChems=Array.isArray(b)?b:[]; skRenderChems(); showToast('💾'); } catch(e){ showToast('失敗: '+e.message); } } async function skSetPrice(id,val){ try{ await skGas('update',{table:'shiken_chemicals',id:id,fields:{unitPrice:val}}); var c=skChems.find(function(x){return String(x.id)===String(id);}); if(c)c.unitPrice=val; showToast('💾'); } catch(e){ showToast('失敗: '+e.message); } } async function skDelChem(id){ if(!canManage()) return; if(!confirm('×?')) return; try{ await skGas('update',{table:'shiken_chemicals',id:id,fields:{active:'false'}}); skChems=skChems.filter(function(x){return String(x.id)!==String(id);}); skRenderChems(); } catch(e){ showToast('失敗: '+e.message); } } /* ----- 評価画面(量産候補ランキング・見える化) ----- */ function skOpenEval(){ skOpenOv('sk-eval'); skRenderEval(); } function skRenderEval(){ var el=document.getElementById('sk-eval-body'); var rated=skLive().filter(function(r){return r.evalVerdict;}); var rank={'合格':3,'条件付':2,'不合格':1}; rated.sort(function(a,b){ var d=(rank[b.evalVerdict]||0)-(rank[a.evalVerdict]||0); if(d)return d; return (parseInt(b.evalScore)||0)-(parseInt(a.evalScore)||0); }); var counts={'合格':0,'条件付':0,'不合格':0}; skLive().forEach(function(r){ if(r.evalVerdict in counts) counts[r.evalVerdict]++; }); var h='
' +'
'+counts['合格']+'
'+t('sk_pass')+'
' +'
'+counts['条件付']+'
'+t('sk_condpass')+'
' +'
'+counts['不合格']+'
'+t('sk_fail')+'
'; h+='
🏆 '+t('sk_rank')+'
'; if(!rated.length){ h+='
'+t('sk_none')+'
'; } else { h+=rated.map(function(r,i){ var c={'合格':'#2e7d32','条件付':'#e8920a','不合格':'#c62828'}[r.evalVerdict]||'#888'; return '
' +'
'+(i+1)+'
' +'
'+skEsc(trPickField(r,'purpose',r.purpose)||r.no||r.client||(r.machine?(r.machine+' '+jdate(r.testDate)):'')||('No.'+r.id))+'
'+skVerdictBadge(r.evalVerdict,r.evalScore)+(r.evalComment?(' '+skEsc(r.evalComment)):'')+(r.inventoryId?(' 📦'+t('sk_linked')):'')+'
'; }).join(''); } el.innerHTML=h; }
処理中…
📖 マニュアルAI