Files
sap-erp/public/item-group-classification.html
T
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
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>