179 lines
11 KiB
HTML
179 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<script src="/auth-guard.js?v=4"></script>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"/>
|
|
<title>Item Group Rules — SAP B1</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/>
|
|
<script src="/lucide.min.js?v=399"></script>
|
|
<style>
|
|
:root{
|
|
--igc-blue:#2563eb;--igc-blue2:#1d4ed8;--igc-blue-light:#eff6ff;
|
|
--igc-text:#0f172a;--igc-text2:#334155;--igc-text3:#64748b;--igc-text4:#94a3b8;
|
|
--igc-bg:#f4f6f9;--igc-surface:#fff;--igc-surface2:#f8fafc;--igc-surface3:#eef1f5;--igc-border:#e2e8f0;
|
|
--igc-font:'Space Grotesk',sans-serif;--igc-mono:'JetBrains Mono',ui-monospace,monospace;
|
|
--igc-red:#dc2626;--igc-green:#16a34a;--igc-amber:#d97706;--igc-purple:#7c3aed;
|
|
--igc-shadow:0 1px 3px rgba(15,23,42,.06),0 1px 2px rgba(15,23,42,.04);
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:var(--igc-font);background:var(--igc-bg)!important;color:var(--igc-text);min-height:100vh;-webkit-font-smoothing:antialiased}
|
|
main{padding:24px;max-width:980px;margin:0 auto}
|
|
.page-title{font-size:22px;font-weight:800;display:flex;align-items:center;gap:10px}
|
|
.page-sub{font-size:13px;color:var(--igc-text3);margin-top:4px;line-height:1.5;max-width:700px}
|
|
.card{background:var(--igc-surface);border:1px solid var(--igc-border);border-radius:12px;box-shadow:var(--igc-shadow);margin-top:18px;overflow:hidden}
|
|
.card-hd{padding:14px 18px;border-bottom:1px solid var(--igc-border);font-size:13px;font-weight:700;background:var(--igc-surface2);display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
.card-hd input{margin-left:auto;padding:7px 11px;border:1.5px solid var(--igc-border);border-radius:8px;font-size:12.5px;font-family:var(--igc-font);width:220px;outline:none;background:var(--igc-surface)!important;color:var(--igc-text)!important}
|
|
.card-hd input:focus{border-color:var(--igc-blue)}
|
|
.card table{width:100%;border-collapse:collapse;font-size:13px;background:var(--igc-surface)!important;color:var(--igc-text)!important}
|
|
.card th{background:var(--igc-surface3)!important;color:var(--igc-text3)!important;font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;text-align:left;padding:9px 14px;border-color:var(--igc-border)!important}
|
|
.card td{padding:9px 14px;border-top:1px solid var(--igc-border);vertical-align:middle;color:var(--igc-text)!important;border-color:var(--igc-border)!important}
|
|
.card tbody tr:hover td{background:var(--igc-surface2)!important}
|
|
.g-code{font-family:var(--igc-mono);font-size:11.5px;color:var(--igc-text3)!important}
|
|
.g-name{font-weight:600;color:var(--igc-text)!important}
|
|
.cls-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
.chip{display:inline-flex;align-items:center;gap:5px;padding:6px 11px;border-radius:20px;border:1.5px solid var(--igc-border);background:var(--igc-surface)!important;color:var(--igc-text3)!important;font-size:11.5px;font-weight:700;cursor:pointer;user-select:none;transition:all .12s}
|
|
.chip input{display:none}
|
|
.chip.on.c-RAW{border-color:#16a34a;background:#f0faf0!important;color:#15803d!important}
|
|
.chip.on.c-PACK{border-color:var(--igc-blue);background:var(--igc-blue-light)!important;color:var(--igc-blue2)!important}
|
|
.chip.on.c-COMPONENT{border-color:var(--igc-purple);background:#f5f0fe!important;color:var(--igc-purple)!important}
|
|
.chip.on.c-SFG{border-color:var(--igc-amber);background:#fef8ef!important;color:var(--igc-amber)!important}
|
|
.chip:not(.on):hover{border-color:var(--igc-text4)}
|
|
.row-saved{font-size:11px;color:var(--igc-green);display:inline-flex;align-items:center;gap:4px;margin-left:8px;opacity:0;transition:opacity .2s}
|
|
.row-saved.show{opacity:1}
|
|
.empty-msg{text-align:center;padding:44px;color:var(--igc-text3);font-size:13px}
|
|
.access-denied{text-align:center;padding:80px 20px}
|
|
.ad-title{font-size:22px;font-weight:800;color:var(--igc-red);margin-bottom:8px}
|
|
.ad-msg{font-size:13px;color:var(--igc-text3);max-width:360px;margin:0 auto}
|
|
.legend{display:flex;gap:16px;flex-wrap:wrap;padding:12px 18px;font-size:11.5px;color:var(--igc-text3);border-top:1px solid var(--igc-border);background:var(--igc-surface2)}
|
|
.legend b{color:var(--igc-text2)}
|
|
.toast-c{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px}
|
|
.toast{padding:11px 16px;border-radius:9px;font-size:12px;font-weight:600;box-shadow:0 4px 16px rgba(15,23,42,.18);color:#fff;max-width:340px}
|
|
.t-ok{background:#16a34a}.t-err{background:#dc2626}
|
|
</style>
|
|
<link rel="stylesheet" href="/theme.css?v=3"/>
|
|
</head>
|
|
<body>
|
|
<nav></nav>
|
|
<main id="app">
|
|
<div style="padding:60px;text-align:center;color:var(--igc-text3)">Loading…</div>
|
|
</main>
|
|
<div class="toast-c" id="toast-c"></div>
|
|
<script src="/sidebar.js?v=25"></script>
|
|
<script>
|
|
'use strict';
|
|
let _tok=null,_user=null;
|
|
(function(){try{_tok=sessionStorage.getItem('portal_token');const u=sessionStorage.getItem('portal_user');if(u)_user=JSON.parse(u);}catch{}})();
|
|
if(!_tok){window.location.href='/';}
|
|
|
|
const BACKEND='/api';
|
|
let _groups=[], _loading=true, _q='';
|
|
|
|
async function api(m,p,b=null){
|
|
const h={'Content-Type':'application/json'};if(_tok)h['Authorization']='Bearer '+_tok;
|
|
const o={method:m.toUpperCase(),headers:h};if(b)o.body=JSON.stringify(b);
|
|
let r,d;try{r=await fetch(BACKEND+p,o);d=await r.json();}catch(e){throw new Error('Cannot reach server');}
|
|
if(r.status===401){window.location.href='/';throw new Error('Session expired');}
|
|
if(!r.ok)throw new Error(d?.message||'HTTP '+r.status);return d;
|
|
}
|
|
function esc(v){return(v==null?'':v).toString().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
function toast(msg,type='ok'){const tc=document.getElementById('toast-c');const el=document.createElement('div');el.className=`toast t-${type}`;el.textContent=msg;tc.appendChild(el);setTimeout(()=>el.remove(),3200);}
|
|
|
|
async function loadGroups(){
|
|
_loading=true;render();
|
|
try{const r=await api('GET','/item-group-classification');_groups=r.data||[];}
|
|
catch(e){toast(e.message,'err');_groups=[];}
|
|
_loading=false;render();
|
|
}
|
|
|
|
async function saveClass(code,name,cls,rowEl){
|
|
try{
|
|
await api('PUT',`/item-group-classification/${code}`,{name,classification:cls});
|
|
const g=_groups.find(x=>x.code===code);if(g)g.classification=cls;
|
|
const flag=rowEl.querySelector('.row-saved');
|
|
if(flag){flag.classList.add('show');setTimeout(()=>flag.classList.remove('show'),1400);}
|
|
}catch(e){toast(e.message,'err');}
|
|
}
|
|
|
|
function render(){
|
|
const app=document.getElementById('app');
|
|
const isAdmin=_user?.role==='admin'||_user?.role==='sap_adder'||_user?.role==='system_admin';
|
|
if(!isAdmin){
|
|
app.innerHTML=`
|
|
<div class="access-denied">
|
|
<div class="ad-title">Access Denied</div>
|
|
<div class="ad-msg">Item Group Rules requires <strong>Admin</strong> or <strong>SAP Adder</strong> role. Your current role is <strong>${esc(_user?.role||'unknown')}</strong>.</div>
|
|
<div style="margin-top:20px"><a href="/" style="text-decoration:none;color:var(--igc-blue);font-weight:700">← Back to Home</a></div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
const filtered=_q?_groups.filter(g=>(g.name||'').toLowerCase().includes(_q.toLowerCase())||String(g.code).includes(_q)):_groups;
|
|
const chip=(g,tag,label)=>{
|
|
const tags=(g.classification||'').split(',').filter(Boolean);
|
|
const on=tags.includes(tag);
|
|
return `<label class="chip ${on?'on c-'+tag:''}" data-code="${g.code}" data-name="${esc(g.name)}" data-tag="${tag}"><input type="checkbox" ${on?'checked':''}/>${label}</label>`;
|
|
};
|
|
const rows=_loading?'':filtered.length?filtered.map(g=>`
|
|
<tr data-code="${g.code}">
|
|
<td class="g-code">${esc(g.code)}</td>
|
|
<td class="g-name">${esc(g.name)}</td>
|
|
<td>
|
|
<div class="cls-chips">
|
|
${chip(g,'RAW','Raw Material')}
|
|
${chip(g,'PACK','Packing Material')}
|
|
${chip(g,'COMPONENT','Component')}
|
|
${chip(g,'SFG','Without Intimation')}
|
|
</div>
|
|
</td>
|
|
<td style="width:70px"><span class="row-saved"><i data-lucide="check" style="width:12px;height:12px"></i> Saved</span></td>
|
|
</tr>`).join(''):`<tr><td colspan="4"><div class="empty-msg">No item groups match “${esc(_q)}”.</div></td></tr>`;
|
|
|
|
app.innerHTML=`
|
|
<div class="page-title"><i data-lucide="layers-3" style="width:22px;height:22px"></i> Item Group Rules</div>
|
|
<div class="page-sub">Configure which Work Order table(s) each SAP Item Group's items land in. Raw Material and Packing Material can both be selected together for the same group — items in that group will then appear in BOTH tables. Component is exclusive of the other two. When a group has no chips selected, the Work Order module falls back to its default Solution-name / PK-code detection.</div>
|
|
<div class="card">
|
|
<div class="card-hd"><i data-lucide="search" style="width:14px;height:14px"></i> Item Groups ${_groups.length?`<span style="color:var(--igc-text3);font-weight:400">(${_groups.length})</span>`:''}<input id="q" placeholder="Search item group…" value="${esc(_q)}"/></div>
|
|
${_loading?`<div class="empty-msg">Loading item groups from SAP…</div>`:`
|
|
<table>
|
|
<thead><tr><th style="width:90px">Code</th><th>Item Group</th><th>Classification</th><th style="width:70px"></th></tr></thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>`}
|
|
<div class="legend">
|
|
<span><b>Raw Material</b> — items land in the Raw Material table.</span>
|
|
<span><b>Packing Material</b> — items land in the Packing Material table.</span>
|
|
<span><b>Component</b> — items land in the Components table, shown as themselves (not exploded).</span>
|
|
<span><b>Without Intimation</b> — independent of the other three; marks this group's items as eligible for Batch Issuance's "Without Intimation" workflow, letting them be intimated directly without a Requirement.</span>
|
|
</div>
|
|
</div>`;
|
|
if(window.lucide)lucide.createIcons();
|
|
const qEl=document.getElementById('q');
|
|
if(qEl){qEl.oninput=function(){_q=this.value;render();document.getElementById('q').focus();document.getElementById('q').selectionStart=document.getElementById('q').selectionEnd=this.value.length;};}
|
|
document.querySelectorAll('.chip').forEach(ch=>{
|
|
ch.onclick=function(e){
|
|
e.preventDefault();
|
|
const code=+this.dataset.code,name=this.dataset.name,tag=this.dataset.tag;
|
|
const g=_groups.find(x=>x.code===code);
|
|
let tags=(g.classification||'').split(',').filter(Boolean);
|
|
const turningOn=!tags.includes(tag);
|
|
if(turningOn){
|
|
// SFG is an independent axis — toggles on its own, never clears or is
|
|
// cleared by RAW/PACK/COMPONENT (which stay mutually exclusive among
|
|
// themselves as before).
|
|
if(tag==='SFG')tags.push(tag);
|
|
else if(tag==='COMPONENT')tags=tags.filter(t=>t==='SFG').concat(['COMPONENT']);
|
|
else{tags=tags.filter(t=>t!=='COMPONENT');tags.push(tag);}
|
|
}else{
|
|
tags=tags.filter(t=>t!==tag);
|
|
}
|
|
g.classification=[...new Set(tags)].join(',');
|
|
render();
|
|
saveClass(code,name,g.classification,document.querySelector(`tr[data-code="${code}"]`));
|
|
};
|
|
});
|
|
}
|
|
|
|
loadGroups();
|
|
</script>
|
|
</body>
|
|
</html>
|