⬅ 入口 📖 マニュアル

パトミル原反管理システム

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

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

パトミル原反管理 v1.6 (2026-06-21)

データを読み込み中...
`); w.document.close(); w.print(); } function printQRBatch(){ const live=rolls.filter(r=>!isDeleted(r)); if(!live.length){showToast(t('toast_no_rolls'));return;} const w=window.open('','_blank'); const items=live.map(r=>{ const url=encodeURIComponent(location.origin+location.pathname+'?roll='+encodeURIComponent(r.code||'')); return `
` +`` +`
${r.code||''}
` +`
${r.kind||''}
` +`
${r.name||''}
` +'
'; }).join(''); w.document.write(`QRラベル一覧${items}`); w.document.close(); w.print(); } /* ===== 削除(論理/復元/完全) ===== */ async function deleteRow(id) { if (!canDelete()) { showToast(t('toast_no_perm')); return; } const r = rolls.find(r=>String(r.id)===String(id)); if (!r) return; if (!confirm(t('confirm_delete').replace('{code}',r.code||'').replace('{name}',r.name||''))) return; try { await gas('update',{table:'rolls',id,fields:{deleted:'true',deletedBy:currentUser.name,deletedAt:jstStampStr()}}); showToast(t('toast_deleted')); await loadRows(); } catch(e) { showToast(t('toast_del_fail')); } } async function restoreRow(id) { if (!canDelete()) return; try { await gas('update',{table:'rolls',id,fields:{deleted:'false',deletedBy:'',deletedAt:''}}); showToast(t('toast_restored')); await loadRows(); } catch(e) { showToast(t('toast_restore_fail')); } } async function hardDeleteRow(id) { if (!canHardDelete()) { showToast(t('toast_no_hard_perm')); return; } const r = rolls.find(r=>String(r.id)===String(id)); if (!r) return; if (!confirm(t('confirm_hard_delete').replace('{code}',r.code||''))) return; try { await gas('delete',{table:'rolls',id}); showToast(t('toast_hard_deleted')); await loadRows(); } catch(e) { showToast(t('toast_hard_del_fail')); } } /* ===== 廃棄承認(廃棄だけ承認・社長指示2026-06-22)===== */ async function disposeRoll(id) { const r = rolls.find(x=>String(x.id)===String(id)); if (!r) return; if (canDelete()) { if (!confirm(t('confirm_dispose_direct').replace('{code}', r.code||''))) return; try { await gas('update',{table:'rolls',id,fields:{status:'廃棄'}}); await gas('add',{table:'ops',row:{type:'廃棄',date:jstDateStr(),inIds:String(id),outIds:'',staff:currentUser.name,note:'',createdAt:jstStampStr()}}); showToast(t('toast_disposed')); await loadRows(); } catch(e){ showToast(t('toast_save_fail')+(e.message||e)); } } else { if (!confirm(t('confirm_dispose_request').replace('{code}', r.code||''))) return; try { await gas('update',{table:'rolls',id,fields:{status:'廃棄申請中'}}); notifyDisposalRequest((r.code||'')+' '+(r.name||'')); showToast(t('toast_disposal_requested')); await loadRows(); } catch(e){ showToast(t('toast_save_fail')+(e.message||e)); } } } async function approveDisposal(id) { if (!canDelete()) { showToast(t('toast_no_perm')); return; } const r = rolls.find(x=>String(x.id)===String(id)); if (!r) return; if (!confirm(t('confirm_dispose_direct').replace('{code}', r.code||''))) return; try { await gas('update',{table:'rolls',id,fields:{status:'廃棄'}}); await gas('add',{table:'ops',row:{type:'廃棄',date:jstDateStr(),inIds:String(id),outIds:'',staff:currentUser.name,note:'承認',createdAt:jstStampStr()}}); showToast(t('toast_disposal_approved')); await loadRows(); } catch(e){ showToast(t('toast_save_fail')+(e.message||e)); } } async function rejectDisposal(id) { if (!canDelete()) { showToast(t('toast_no_perm')); return; } const r = rolls.find(x=>String(x.id)===String(id)); if (!r) return; if (!confirm(t('confirm_reject_disposal').replace('{code}', r.code||''))) return; try { await gas('update',{table:'rolls',id,fields:{status:'在庫'}}); showToast(t('toast_disposal_rejected')); await loadRows(); } catch(e){ showToast(t('toast_save_fail')+(e.message||e)); } } function filterPendingDisposal() { const s=document.getElementById('filter-status'); if(s){ s.value='廃棄申請中'; renderCards(); } } /* 廃棄申請メール(承認者=社長・事務へ・名簿のメールから自動宛先・社長指示2026-06-22) */ function approverEmails_() { try { return (allMembers || []).filter(function (m) { return (m.role === 'president' || m.role === 'admin') && m.email && String(m.active) !== 'false'; }).map(function (m) { return m.email; }); } catch (e) { return []; } } function notifyDisposalRequest(label) { var to = approverEmails_(); var body = '原反の廃棄申請が出ました。アプリで承認をお願いします。\n\n対象:' + (label || '') + '\n申請者:' + ((currentUser && currentUser.name) || '') + '\n日時:' + jstStampStr() + '\n\n承認・却下は原反アプリ(https://kowa-genhan.pages.dev)の🔔バナーから行えます。'; try { gas('notify', { subject: '【興和】原反の廃棄申請(承認待ち)', body: body, to: to.length ? to : undefined }).catch(function () {}); } catch (e) {} } function hl(text,kw){ if(!kw||!text) return text||''; const escaped=kw.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); return String(text).replace(new RegExp(`(${escaped})`,'gi'),'$1'); } function pEsc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(m){return ({'&':'&','<':'<','>':'>','"':'"'})[m];}); } function pNow(){ var d=new Date(); function p(n){return ('0'+n).slice(-2);} return d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate()); } function printRolls(){ var arr=getFilteredRows(); var rs=arr.map(function(r){ return ''+pEsc(r.code)+''+pEsc(r.kind)+''+pEsc(r.name)+''+pEsc([r.material,r.color,r.width?('幅'+r.width+'mm'):''].filter(Boolean).join(' '))+''+pEsc(r.status)+''+pEsc(r.location)+''+pEsc(r.lenInit)+''+pEsc(r.lenRemain)+''; }).join(''); var html='原反 在庫一覧

🧵 興和化学 原反 在庫一覧

出力日:'+pNow()+' / 件数:'+arr.length+'件
'+rs+'
コード種別品名素材・色・幅状態場所初期長残長
'; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } function printOneRoll(id){ var r=(rolls||[]).find(function(x){return String(x.id)===String(id);}); if(!r)return; var data=[['コード',r.code],['種別',r.kind],['品名',r.name],['素材',r.material],['色',r.color],['幅',r.width?r.width+'mm':''],['状態',r.status],['場所',r.location],['初期長',r.lenInit],['残長',r.lenRemain]]; var body=data.map(function(x){return ''+x[0]+''+pEsc(x[1]).replace(/\n/g,'
')+'';}).join(''); var html='原反 '+pEsc(r.code)+'

🧵 原反 記録

'+body+'
'; var w=window.open('','_blank'); if(!w){alert('ポップアップを許可してください');return;} w.document.write(html); w.document.close(); } 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 = rolls.filter(r=>!isDeleted(r)).length; const delCount = rolls.filter(r=>isDeleted(r)).length; // 廃棄申請バナー(承認者のみ・社長指示2026-06-22) const _dispBar = document.getElementById('disp-pending-bar'); if (_dispBar) { const _pend = rolls.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) { body.innerHTML='
'+t('empty_trash')+'
'; return; } } else { if (!liveCount) { body.innerHTML='
'+t('empty_rolls').replace(/\n/g,'
')+'
'; countEl.textContent=''; return; } const hasFilter = kw||(document.getElementById('filter-kind')||{}).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) { body.innerHTML='
'+t('empty_filter')+'
'; return; } } body.innerHTML = filtered.map(r => { const parentR = r.parentId ? rolls.find(x=>String(x.id)===String(r.parentId)) : null; const parentInfo = parentR ? `↑${parentR.code||''}` : ''; const remain = r.lenRemain!=null ? Number(r.lenRemain) : null; const init = r.lenInit!=null ? Number(r.lenInit) : null; const pct = (remain!=null && init && init>0) ? Math.round(remain/init*100) : null; const pctColor = pct===null?'var(--text3)':pct<=20?'var(--danger)':pct<=50?'#d97706':'var(--accent)'; let actions; if (viewTrash) { actions = `${canDelete()?``:''}${canHardDelete()?``:''}`; } else if (r.status === '廃棄申請中') { actions = canDelete() ? `` : ``; } else { actions = `${r.status==='在庫'?``:''}${canDelete()?``:''}`; } const delMeta = (viewTrash&&(r.deletedBy||r.deletedAt))?`🗑 ${r.deletedBy||''} ${r.deletedAt||''}`:''; return `
${r.kind||'—'} ${hl(r.code||'—',kw)} ${parentInfo}
${hl(r.name||'—',kw)}
${hl(r.material||'',kw)}${r.color?' / '+hl(r.color,kw):''}${r.width?' | 幅'+r.width+'mm':''}
${r.status||'—'} ${r.location?`${hl(r.location,kw)}`:''} ${delMeta}
${t('card_init')}
${init!=null?init:'—'}
${t('card_remain_lbl')}
${remain!=null?remain:'—'}
${t('card_rate')}
${pct!=null?pct+'%':'—'}
${actions}
`; }).join(''); } function closeImg() { document.getElementById('img-viewer').classList.remove('open'); } function exportExcel() { const target=getFilteredRows(); if(!target.length){showToast(t('toast_excel_none'));return;} const headers=['コード','種別','品名','素材','色','幅(mm)','初期(m)','残量(m)','単位','状態','親コード','生まれ方','仕入先','ロット','保管場所','備考','登録者','登録日時']; const data=target.map(r=>{ const p=r.parentId?rolls.find(x=>String(x.id)===String(r.parentId)):null; return[r.code,r.kind,r.name,r.material,r.color,r.width,r.lenInit,r.lenRemain,r.unit,r.status,p?p.code:'',r.genType,r.vendor,r.lot,r.location,r.note,r.staff,r.createdAt]; }); const ws=XLSX.utils.aoa_to_sheet([headers,...data]); ws['!cols']=[{wch:10},{wch:10},{wch:20},{wch:14},{wch:8},{wch:8},{wch:8},{wch:8},{wch:6},{wch:8},{wch:10},{wch:8},{wch:16},{wch:12},{wch:14},{wch:20},{wch:10},{wch:16}]; const wb=XLSX.utils.book_new();XLSX.utils.book_append_sheet(wb,ws,'原反ロール'); XLSX.writeFile(wb,'パトミル原反管理_'+jstDateStr().replace(/-/g,'')+'.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 = rolls.filter(r => !isDeleted(r)); const ctx = live.slice(0, 200).map(r => `${r.code||''} ${r.kind||''} ${r.name||''} 素材:${r.material||''} 幅:${r.width||''}mm 残:${r.lenRemain||'?'}${r.unit||'m'} 状態:${r.status||''} 場所:${r.location||''}` ).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(); });
処理中…
📖 マニュアルAI