first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+611
View File
@@ -0,0 +1,611 @@
<!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">
<title>Business Master Data | MITRA ERP Portal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Space Grotesk',sans-serif;background:#f0f2f5;color:#1a1a2e;min-height:100vh}
.pg{padding:20px 24px}
.hdr{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:18px}
.hdr h1{font-size:20px;font-weight:700;color:#1a1a2e}
.hdr h1 span{color:#7c3aed}
.btn{height:36px;padding:0 16px;border-radius:8px;font-size:12px;font-weight:700;font-family:inherit;border:none;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.btn-primary{background:#7c3aed;color:#fff}.btn-primary:hover{background:#6d28d9}
.btn-primary:disabled{background:#b0bec5;cursor:not-allowed;opacity:.7}
.btn-outline{background:#fff;color:#7c3aed;border:1.5px solid #7c3aed}.btn-outline:hover{background:#f5f3ff}
.card{background:#fff;border-radius:14px;box-shadow:0 1px 8px rgba(0,0,0,.08);overflow:hidden}
.filter-bar{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;padding:16px 20px;border-bottom:1px solid #e8eaed;background:#fff}
.fg{display:flex;flex-direction:column;gap:5px}
.fg label{font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.6px}
.fg input,.fg select{height:36px;border:1.5px solid #dde1e7;border-radius:8px;padding:0 12px;font-size:13px;font-family:inherit;color:#1a1a2e;background:#fff;outline:none;transition:border-color .15s}
.fg input:focus,.fg select:focus{border-color:#7c3aed;box-shadow:0 0 0 3px rgba(124,58,237,.08)}
.fg input{min-width:220px}
.tbl-wrap{overflow-x:auto;overflow-y:auto;max-height:62vh}
.tbl-wrap::-webkit-scrollbar{height:7px;width:7px}
.tbl-wrap::-webkit-scrollbar-track{background:#f1f3f5}
.tbl-wrap::-webkit-scrollbar-thumb{background:#c1c7ce;border-radius:4px}
.tbl-wrap::-webkit-scrollbar-thumb:hover{background:#9ca3af}
table{border-collapse:collapse;min-width:100%}
thead th{background:#f5f3ff;color:#4b5563;font-weight:700;padding:10px 12px;text-align:left;white-space:nowrap;position:sticky;top:0;z-index:2;border-bottom:2px solid #ddd6fe;font-size:10px;text-transform:uppercase;letter-spacing:.5px}
tbody tr{border-bottom:1px solid #f0f2f4;transition:background .1s;cursor:pointer}
tbody tr:hover{background:#f5f3ff}
tbody tr.selected{background:#ede9fe}
tbody td{padding:9px 12px;white-space:nowrap;color:#374151;font-size:12px}
tbody td.num{text-align:right;font-variant-numeric:tabular-nums}
tbody td.trunc{max-width:220px;overflow:hidden;text-overflow:ellipsis}
.badge{padding:2px 8px;border-radius:10px;font-size:10px;font-weight:700}
.badge-cust{background:#dbeafe;color:#1d4ed8}
.badge-vend{background:#fee2e2;color:#991b1b}
.badge-lead{background:#fef3c7;color:#92400e}
.empty{text-align:center;padding:56px 20px;color:#9ca3af;font-size:13px}
.status-bar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 20px;background:#f8fafb;border-top:1px solid #e8eaed;font-size:12px;color:#6b7280;border-radius:0 0 14px 14px}
.status-dot{width:8px;height:8px;border-radius:50%;background:#d1d5db;flex-shrink:0}
.status-dot.ok{background:#10b981}.status-dot.err{background:#ef4444}
.status-dot.loading{background:#f59e0b;animation:pulse 1s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.pager{display:flex;align-items:center;gap:8px}
.pager button{height:28px;padding:0 10px;border:1.5px solid #dde1e7;border-radius:6px;background:#fff;font-size:11px;font-weight:700;color:#374151;cursor:pointer}
.pager button:disabled{opacity:.4;cursor:not-allowed}
.detail-overlay{position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:500;display:none;backdrop-filter:blur(2px)}
.detail-overlay.show{display:block}
.detail-panel{position:fixed;top:0;right:-440px;width:440px;height:100vh;background:#fff;z-index:501;box-shadow:-4px 0 24px rgba(0,0,0,.12);display:flex;flex-direction:column;transition:right .28s cubic-bezier(.4,0,.2,1)}
.detail-panel.show{right:0}
.detail-header{display:flex;align-items:center;justify-content:space-between;padding:18px 20px;border-bottom:1px solid #e8eaed;background:#f8fafb;flex-shrink:0}
.detail-header h3{font-size:14px;font-weight:700;color:#1a1a2e}
.detail-header .row-num{font-size:11px;color:#9ca3af;margin-top:2px}
.detail-close{width:32px;height:32px;border:none;background:#edf2f7;border-radius:8px;cursor:pointer;font-size:18px;color:#6b7280;display:flex;align-items:center;justify-content:center;transition:all .15s}
.detail-close:hover{background:#e2e8f0;color:#1a1a2e}
.detail-body{flex:1;overflow-y:auto;padding:16px 20px}
.detail-body::-webkit-scrollbar{width:4px}.detail-body::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:2px}
.detail-row{display:flex;gap:12px;padding:9px 0;border-bottom:1px solid #f3f4f6;align-items:center}
.detail-row:last-child{border-bottom:none}
.detail-key{font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.4px;width:140px;flex-shrink:0;padding-top:1px}
.detail-val{font-size:13px;color:#1a1a2e;font-weight:500;word-break:break-all;flex:1}
.detail-edit-input{flex:1;height:32px;border:1.5px solid #dde1e7;border-radius:6px;padding:0 10px;font-size:12px;font-family:inherit;color:#1a1a2e;outline:none;width:100%;box-sizing:border-box}
.detail-edit-input:focus{border-color:#7c3aed;box-shadow:0 0 0 3px rgba(124,58,237,.08)}
.detail-edit-select{flex:1;height:32px;border:1.5px solid #dde1e7;border-radius:6px;padding:0 10px;font-size:12px;font-family:inherit;color:#1a1a2e;outline:none;width:100%;box-sizing:border-box;background:#fff}
.detail-footer{display:flex;gap:8px;padding:14px 20px;border-top:1px solid #e8eaed;background:#f8fafb;flex-shrink:0}
.detail-footer .btn{flex:1;justify-content:center;height:34px}
.dtabs{display:flex;border-bottom:1.5px solid #e8eaed;margin:0 -20px 12px;padding:0 20px;flex-wrap:wrap}
.dtab{padding:8px 12px;font-size:11px;font-weight:700;color:#6b7280;cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap}
.dtab:hover{color:#7c3aed}
.dtab.active{color:#7c3aed;border-bottom-color:#7c3aed}
.dtab-content{display:none}
.dtab-content.active{display:block}
.sub-card{border:1.5px solid #e8eaed;border-radius:8px;padding:10px;margin-bottom:10px;position:relative}
.sub-card .remove-row{position:absolute;top:6px;right:6px;background:#fee2e2;color:#991b1b;border:none;border-radius:6px;width:22px;height:22px;cursor:pointer;font-size:12px;line-height:1}
.add-row-btn{width:100%;margin-top:4px}
.sub-title{font-size:10px;font-weight:700;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;margin-bottom:6px}
.toast{position:fixed;bottom:24px;right:24px;z-index:9999;padding:12px 20px;border-radius:10px;font-size:13px;font-weight:600;color:#fff;box-shadow:0 4px 20px rgba(0,0,0,.15);transform:translateY(20px);opacity:0;transition:all .3s;pointer-events:none;max-width:360px}
.toast.show{transform:translateY(0);opacity:1}
.toast.success{background:#10b981}.toast.error{background:#ef4444}.toast.info{background:#3b82f6}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<script src="/sidebar.js?v=25"></script>
<div class="pg">
<div class="hdr">
<h1><span>Business Master</span> Data</h1>
</div>
<div class="card">
<div class="filter-bar">
<div class="fg">
<label>Type</label>
<select id="bpType">
<option value="">All</option>
<option value="cCustomer">Customers</option>
<option value="cSupplier">Vendors</option>
<option value="cLid">Leads</option>
</select>
</div>
<div class="fg">
<label>Search (Name / Code)</label>
<input type="text" id="searchInput" placeholder="Type to search…">
</div>
<button class="btn btn-primary" id="btnLoad">Search</button>
</div>
<div class="tbl-wrap">
<div class="empty" id="emptyMsg">Click Search to load business partners</div>
<table id="tbl" style="display:none">
<thead><tr>
<th>Type</th><th>Code</th><th>Name</th><th>Phone</th><th>Email</th>
<th>Currency</th><th>Credit Limit</th><th>Status</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
<div class="status-bar">
<div style="display:flex;align-items:center;gap:10px">
<div class="status-dot" id="statusDot"></div>
<span id="statusMsg">Select filters and click Search</span>
</div>
<div class="pager">
<button id="btnPrev" disabled>&laquo; Prev</button>
<span id="pageInfo">Page 1</span>
<button id="btnNext" disabled>Next &raquo;</button>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<div class="detail-overlay" id="detailOverlay"></div>
<div class="detail-panel" id="detailPanel">
<div class="detail-header">
<div>
<h3 id="detailTitle">Business Partner</h3>
<div class="row-num" id="detailRowNum"></div>
</div>
<button class="detail-close" id="detailClose">&#x2715;</button>
</div>
<div class="detail-body" id="detailBody"></div>
<div class="detail-footer" id="detailFooterView">
<button class="btn btn-outline" id="detailDeleteBtn" style="color:#dc2626;border-color:#dc2626">Delete</button>
<button class="btn btn-primary" id="detailEditBtn">Edit</button>
</div>
<div class="detail-footer" id="detailFooterEdit" style="display:none">
<button class="btn btn-outline" id="detailCancelBtn">Cancel</button>
<button class="btn btn-primary" id="detailSaveBtn">Save Changes</button>
</div>
</div>
<script>
(function(){
const tok=sessionStorage.getItem('portal_token');
if(!tok){window.location.href='/';return;}
// ── General tab ───────────────────────────────────────────────────────────
const GENERAL_VIEW_FIELDS=[
{k:'CardCode',l:'Card Code'},
{k:'CardType',l:'Type'},
{k:'GroupCode',l:'Group Code'},
{k:'CardName',l:'Name'},
{k:'CardForeignName',l:'Foreign Name'},
{k:'Phone1',l:'Phone'},
{k:'Phone2',l:'Phone 2'},
{k:'Cellular',l:'Mobile'},
{k:'Fax',l:'Fax'},
{k:'EmailAddress',l:'Email'},
{k:'Website',l:'Website'},
{k:'ContactPerson',l:'Contact Person'},
{k:'Currency',l:'Currency'},
{k:'Notes',l:'Notes'},
{k:'FederalTaxID',l:'GSTIN / Tax ID'},
{k:'Valid',l:'Active'},
{k:'Frozen',l:'Frozen'},
];
const GENERAL_EDIT_FIELDS=[
{k:'CardName',l:'Name'},
{k:'CardForeignName',l:'Foreign Name'},
{k:'Phone1',l:'Phone'},
{k:'Phone2',l:'Phone 2'},
{k:'Cellular',l:'Mobile'},
{k:'Fax',l:'Fax'},
{k:'EmailAddress',l:'Email'},
{k:'Website',l:'Website'},
{k:'ContactPerson',l:'Contact Person'},
{k:'Currency',l:'Currency'},
{k:'FederalTaxID',l:'GSTIN / Tax ID'},
{k:'Notes',l:'Notes'},
{k:'Valid',l:'Active',select:['tYES','tNO']},
{k:'Frozen',l:'Frozen',select:['tYES','tNO']},
];
// ── Payment / Accounting tab ────────────────────────────────────────────────
const PAYMENT_VIEW_FIELDS=[
{k:'GroupCode',l:'BP Group Code'},
{k:'PayTermsGrpCode',l:'Payment Terms Code'},
{k:'CreditLimit',l:'Credit Limit',num:true},
];
const PAYMENT_EDIT_FIELDS=[
{k:'PayTermsGrpCode',l:'Payment Terms Code',num:true},
{k:'CreditLimit',l:'Credit Limit',num:true},
];
// ── Address sub-form (BPAddresses) ──────────────────────────────────────────
const ADDRESS_FIELDS=[
{k:'AddressName',l:'Address ID'},
{k:'AddressType',l:'Type',select:['bo_BillTo','bo_ShipTo']},
{k:'Street',l:'Street'},
{k:'Block',l:'Block / Area'},
{k:'City',l:'City'},
{k:'ZipCode',l:'Zip Code'},
{k:'County',l:'State'},
{k:'Country',l:'Country'},
];
// ── Bank account sub-form (BPBankAccounts) ───────────────────────────────────
const BANK_FIELDS=[
{k:'BankCode',l:'Bank Code / Name'},
{k:'Branch',l:'Branch'},
{k:'Account',l:'Account Number'},
{k:'BICSwiftCode',l:'IFSC / SWIFT Code'},
{k:'AccountName',l:'Account Holder Name'},
{k:'IBAN',l:'IBAN'},
];
const bpType=document.getElementById('bpType');
const searchInput=document.getElementById('searchInput');
const btnLoad=document.getElementById('btnLoad');
const statusDot=document.getElementById('statusDot');
const statusMsg=document.getElementById('statusMsg');
const tbl=document.getElementById('tbl');
const emptyMsg=document.getElementById('emptyMsg');
const btnPrev=document.getElementById('btnPrev');
const btnNext=document.getElementById('btnNext');
const pageInfo=document.getElementById('pageInfo');
const TOP=50;
let skip=0,rows=[];
function setStatus(msg,st){statusMsg.textContent=msg;statusDot.className='status-dot'+(st?' '+st:'');}
function showToast(msg,type='info'){const t=document.getElementById('toast');t.textContent=msg;t.className=`toast ${type} show`;setTimeout(()=>t.classList.remove('show'),3500);}
function escHtml(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function typeBadge(t){
const m={cCustomer:['Customer','badge-cust'],cSupplier:['Vendor','badge-vend'],cLid:['Lead','badge-lead']};
const [label,cls]=m[t]||[t||'',''];
return `<span class="badge ${cls}">${label}</span>`;
}
async function loadData(reset=true){
if(reset) skip=0;
setStatus('Loading…','loading');
btnLoad.disabled=true;
try{
const params=new URLSearchParams();
if(bpType.value) params.set('type',bpType.value);
if(searchInput.value.trim()) params.set('search',searchInput.value.trim());
params.set('top',TOP);
params.set('skip',skip);
const resp=await fetch(`/api/business-master?${params}`,{headers:{'Authorization':'Bearer '+tok}});
const json=await resp.json();
if(!json.success){setStatus('Error: '+json.message,'err');showToast(json.message,'error');return;}
rows=json.data||[];
renderTable();
setStatus(`Loaded ${rows.length} record(s)`,'ok');
pageInfo.textContent=`Page ${Math.floor(skip/TOP)+1}`;
btnPrev.disabled=skip===0;
btnNext.disabled=rows.length<TOP;
}catch(e){
setStatus('Failed: '+e.message,'err');
showToast(e.message,'error');
}finally{
btnLoad.disabled=false;
}
}
function renderTable(){
if(!rows.length){
tbl.style.display='none';
emptyMsg.textContent='No matching business partners';
emptyMsg.style.display='block';
return;
}
tbl.querySelector('tbody').innerHTML=rows.map((r,i)=>`
<tr data-idx="${i}">
<td>${typeBadge(r.CardType)}</td>
<td>${escHtml(r.CardCode)}</td>
<td class="trunc">${escHtml(r.CardName)}</td>
<td>${escHtml(r.Phone1)}</td>
<td class="trunc">${escHtml(r.EmailAddress)}</td>
<td>${escHtml(r.Currency)}</td>
<td class="num">${fmtNum(r.CreditLimit)}</td>
<td>${r.Frozen==='Y'?'<span class="badge badge-vend">Frozen</span>':(r.Valid==='N'?'<span class="badge badge-lead">Inactive</span>':'<span class="badge badge-cust">Active</span>')}</td>
</tr>`).join('');
tbl.style.display='table';
emptyMsg.style.display='none';
tbl.querySelector('tbody').addEventListener('click',e=>{
const tr=e.target.closest('tr');
if(!tr)return;
document.querySelectorAll('tbody tr.selected').forEach(r=>r.classList.remove('selected'));
tr.classList.add('selected');
openDetail(rows[+tr.dataset.idx]);
});
}
function fmtNum(v){if(v==null||v==='')return'';return Number(v).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});}
btnLoad.addEventListener('click',()=>loadData(true));
searchInput.addEventListener('keydown',e=>{if(e.key==='Enter')loadData(true);});
btnPrev.addEventListener('click',()=>{if(skip>0){skip-=TOP;loadData(false);}});
btnNext.addEventListener('click',()=>{skip+=TOP;loadData(false);});
// ── Detail panel ─────────────────────────────────────────────────────────────
const detailOverlay=document.getElementById('detailOverlay');
const detailPanel=document.getElementById('detailPanel');
const detailBody=document.getElementById('detailBody');
const detailTitle=document.getElementById('detailTitle');
const detailRowNum=document.getElementById('detailRowNum');
const detailFooterView=document.getElementById('detailFooterView');
const detailFooterEdit=document.getElementById('detailFooterEdit');
const detailEditBtn=document.getElementById('detailEditBtn');
const detailCancelBtn=document.getElementById('detailCancelBtn');
const detailSaveBtn=document.getElementById('detailSaveBtn');
const detailDeleteBtn=document.getElementById('detailDeleteBtn');
let curBp=null,curFull=null;
async function openDetail(row){
curBp=row;
detailTitle.textContent=row.CardName||row.CardCode;
detailRowNum.textContent=row.CardCode;
detailBody.innerHTML='<div class="empty">Loading details…</div>';
detailFooterView.style.display='flex';
detailFooterEdit.style.display='none';
detailOverlay.classList.add('show');
detailPanel.classList.add('show');
try{
const resp=await fetch(`/api/business-master/${encodeURIComponent(row.CardCode)}`,{headers:{'Authorization':'Bearer '+tok}});
const json=await resp.json();
if(!json.success){detailBody.innerHTML=`<div class="empty">${escHtml(json.message)}</div>`;return;}
curFull=json.data;
renderDetailView();
}catch(e){
detailBody.innerHTML=`<div class="empty">${escHtml(e.message)}</div>`;
}
}
const TABS=[
{id:'general',l:'General'},
{id:'addresses',l:'Addresses'},
{id:'bank',l:'Payment / Bank'},
{id:'props',l:'Properties'},
];
let activeTab='general';
let editAddresses=null, editBankAccts=null;
function tabsHtml(){
return `<div class="dtabs">`+TABS.map(t=>`<div class="dtab${t.id===activeTab?' active':''}" data-tab="${t.id}">${t.l}</div>`).join('')+`</div>`;
}
function renderDetailView(){
const addrs=curFull.BPAddresses||[];
const banks=curFull.BPBankAccounts||[];
const udfKeys=Object.keys(curFull).filter(k=>k.startsWith('U_'));
const generalHtml=GENERAL_VIEW_FIELDS.map(f=>{
const v=curFull[f.k];
const disp=v==null||v===''?'—':String(v);
return `<div class="detail-row"><div class="detail-key">${f.l}</div><div class="detail-val${f.num?' num':''}">${escHtml(f.num?fmtNum(v):disp)}</div></div>`;
}).join('');
const addrHtml=addrs.length?addrs.map(a=>{
return `<div class="sub-card">`+ADDRESS_FIELDS.map(f=>{
const v=a[f.k]; const disp=v==null||v===''?'—':String(v);
return `<div class="detail-row"><div class="detail-key">${f.l}</div><div class="detail-val">${escHtml(disp)}</div></div>`;
}).join('')+`</div>`;
}).join(''):'<div class="empty">No addresses on file</div>';
const bankHtml=`<div class="sub-title">Payment Terms / Credit</div>`
+PAYMENT_VIEW_FIELDS.map(f=>{
const v=curFull[f.k]; const disp=v==null||v===''?'—':String(v);
return `<div class="detail-row"><div class="detail-key">${f.l}</div><div class="detail-val${f.num?' num':''}">${escHtml(f.num?fmtNum(v):disp)}</div></div>`;
}).join('')
+`<div class="sub-title" style="margin-top:12px">Bank Accounts</div>`
+(banks.length?banks.map(b=>{
return `<div class="sub-card">`+BANK_FIELDS.map(f=>{
const v=b[f.k]; const disp=v==null||v===''?'—':String(v);
return `<div class="detail-row"><div class="detail-key">${f.l}</div><div class="detail-val">${escHtml(disp)}</div></div>`;
}).join('')+`</div>`;
}).join(''):'<div class="empty">No bank accounts on file</div>');
const propsHtml=udfKeys.length?udfKeys.map(k=>{
const v=curFull[k]; const disp=v==null||v===''?'—':String(v);
return `<div class="detail-row"><div class="detail-key">${k}</div><div class="detail-val">${escHtml(disp)}</div></div>`;
}).join(''):'<div class="empty">No custom fields on file</div>';
detailBody.innerHTML=tabsHtml()
+`<div class="dtab-content${activeTab==='general'?' active':''}" data-content="general">${generalHtml}</div>`
+`<div class="dtab-content${activeTab==='addresses'?' active':''}" data-content="addresses">${addrHtml}</div>`
+`<div class="dtab-content${activeTab==='bank'?' active':''}" data-content="bank">${bankHtml}</div>`
+`<div class="dtab-content${activeTab==='props'?' active':''}" data-content="props">${propsHtml}</div>`;
detailFooterView.style.display='flex';
detailFooterEdit.style.display='none';
}
function fieldRowEdit(f,obj,prefix){
const v=obj[f.k];
const val=v==null?'':String(v);
const dataAttrs=`data-key="${f.k}"${prefix?` data-prefix="${prefix}"`:''}`;
if(f.select){
return `<div class="detail-row"><div class="detail-key">${f.l}</div>`
+`<select class="detail-edit-select" ${dataAttrs}>`
+f.select.map(o=>`<option value="${o}"${val===o?' selected':''}>${o==='tYES'?'Yes':'No'}</option>`).join('')
+`</select></div>`;
}
return `<div class="detail-row"><div class="detail-key">${f.l}</div>`
+`<input class="detail-edit-input${f.num?' num':''}" ${dataAttrs} data-num="${f.num?'1':'0'}" value="${escHtml(val)}"></div>`;
}
function addrCardHtml(a,idx){
return `<div class="sub-card" data-arr-idx="${idx}">`
+`<button type="button" class="remove-row" data-remove-addr="${idx}">×</button>`
+ADDRESS_FIELDS.map(f=>fieldRowEdit(f,a)).join('')
+`</div>`;
}
function bankCardHtml(b,idx){
return `<div class="sub-card" data-arr-idx="${idx}">`
+`<button type="button" class="remove-row" data-remove-bank="${idx}">×</button>`
+BANK_FIELDS.map(f=>fieldRowEdit(f,b)).join('')
+`</div>`;
}
function renderAddressesEdit(){
const wrap=detailBody.querySelector('[data-content="addresses"]');
if(!wrap)return;
wrap.innerHTML=(editAddresses.length?editAddresses.map(addrCardHtml).join(''):'')
+`<button type="button" class="btn btn-outline add-row-btn" id="addAddrBtn">+ Add Address</button>`;
}
function renderBankEdit(){
const wrap=detailBody.querySelector('[data-content="bank"]');
if(!wrap)return;
wrap.innerHTML=`<div class="sub-title">Payment Terms / Credit</div>`
+PAYMENT_EDIT_FIELDS.map(f=>fieldRowEdit(f,curFull)).join('')
+`<div class="sub-title" style="margin-top:12px">Bank Accounts</div>`
+(editBankAccts.length?editBankAccts.map(bankCardHtml).join(''):'')
+`<button type="button" class="btn btn-outline add-row-btn" id="addBankBtn">+ Add Bank Account</button>`;
}
function renderDetailEdit(){
editAddresses=(curFull.BPAddresses||[]).map(a=>({...a}));
editBankAccts=(curFull.BPBankAccounts||[]).map(b=>({...b}));
const udfKeys=Object.keys(curFull).filter(k=>k.startsWith('U_'));
const generalHtml=GENERAL_EDIT_FIELDS.map(f=>fieldRowEdit(f,curFull)).join('');
const propsHtml=udfKeys.length?udfKeys.map(k=>{
const v=curFull[k]; const val=v==null?'':String(v);
return `<div class="detail-row"><div class="detail-key">${k}</div><input class="detail-edit-input" data-key="${k}" data-num="0" value="${escHtml(val)}"></div>`;
}).join(''):'<div class="empty">No custom fields on file</div>';
detailBody.innerHTML=tabsHtml()
+`<div class="dtab-content${activeTab==='general'?' active':''}" data-content="general">${generalHtml}</div>`
+`<div class="dtab-content${activeTab==='addresses'?' active':''}" data-content="addresses"></div>`
+`<div class="dtab-content${activeTab==='bank'?' active':''}" data-content="bank"></div>`
+`<div class="dtab-content${activeTab==='props'?' active':''}" data-content="props">${propsHtml}</div>`;
renderAddressesEdit();
renderBankEdit();
detailFooterView.style.display='none';
detailFooterEdit.style.display='flex';
}
detailBody.addEventListener('click',(e)=>{
const tab=e.target.closest('.dtab');
if(tab){
activeTab=tab.dataset.tab;
detailBody.querySelectorAll('.dtab').forEach(t=>t.classList.toggle('active',t.dataset.tab===activeTab));
detailBody.querySelectorAll('.dtab-content').forEach(c=>c.classList.toggle('active',c.dataset.content===activeTab));
return;
}
if(e.target.id==='addAddrBtn'){
syncEditArraysFromDom();
editAddresses.push({AddressName:'',AddressType:'bo_BillTo'});
renderAddressesEdit();
return;
}
if(e.target.id==='addBankBtn'){
syncEditArraysFromDom();
editBankAccts.push({});
renderBankEdit();
return;
}
const rmAddr=e.target.closest('[data-remove-addr]');
if(rmAddr){
syncEditArraysFromDom();
editAddresses.splice(parseInt(rmAddr.dataset.removeAddr,10),1);
renderAddressesEdit();
return;
}
const rmBank=e.target.closest('[data-remove-bank]');
if(rmBank){
syncEditArraysFromDom();
editBankAccts.splice(parseInt(rmBank.dataset.removeBank,10),1);
renderBankEdit();
return;
}
});
function syncEditArraysFromDom(){
const addrWrap=detailBody.querySelector('[data-content="addresses"]');
if(addrWrap){
addrWrap.querySelectorAll('.sub-card').forEach((card,idx)=>{
const obj=editAddresses[idx]||{};
card.querySelectorAll('[data-key]').forEach(inp=>{ obj[inp.dataset.key]=inp.value; });
editAddresses[idx]=obj;
});
}
const bankWrap=detailBody.querySelector('[data-content="bank"]');
if(bankWrap){
bankWrap.querySelectorAll('.sub-card').forEach((card,idx)=>{
const obj=editBankAccts[idx]||{};
card.querySelectorAll('[data-key]').forEach(inp=>{ obj[inp.dataset.key]=inp.value; });
editBankAccts[idx]=obj;
});
}
}
detailEditBtn.addEventListener('click',()=>renderDetailEdit());
detailCancelBtn.addEventListener('click',()=>renderDetailView());
detailSaveBtn.addEventListener('click',async ()=>{
syncEditArraysFromDom();
const patch={};
detailBody.querySelectorAll('[data-content="general"] [data-key], [data-content="bank"] > .detail-row [data-key], [data-content="props"] [data-key]').forEach(inp=>{
const key=inp.dataset.key;
const raw=inp.value.trim();
patch[key]=inp.dataset.num==='1'?(raw===''?0:parseFloat(raw)):raw;
});
patch.BPAddresses=editAddresses;
patch.BPBankAccounts=editBankAccts;
detailSaveBtn.disabled=true;
try{
const resp=await fetch(`/api/business-master/${encodeURIComponent(curFull.CardCode)}`,{
method:'PATCH',
headers:{'Authorization':'Bearer '+tok,'Content-Type':'application/json'},
body:JSON.stringify(patch),
});
const json=await resp.json();
if(json.success){
showToast('Business partner updated','success');
Object.assign(curFull,patch);
renderDetailView();
loadData(false);
}else{
showToast(json.message,'error');
}
}catch(e){
showToast(e.message,'error');
}finally{
detailSaveBtn.disabled=false;
}
});
detailDeleteBtn.addEventListener('click',async ()=>{
if(!curFull)return;
if(!confirm(`Delete business partner ${curFull.CardCode} — ${curFull.CardName}? This cannot be undone.`))return;
detailDeleteBtn.disabled=true;
try{
const resp=await fetch(`/api/business-master/${encodeURIComponent(curFull.CardCode)}`,{
method:'DELETE',
headers:{'Authorization':'Bearer '+tok},
});
const json=await resp.json();
if(json.success){
showToast('Business partner deleted','success');
closeDetail();
loadData(false);
}else{
showToast(json.message,'error');
}
}catch(e){
showToast(e.message,'error');
}finally{
detailDeleteBtn.disabled=false;
}
});
function closeDetail(){
detailOverlay.classList.remove('show');
detailPanel.classList.remove('show');
document.querySelectorAll('tbody tr.selected').forEach(r=>r.classList.remove('selected'));
}
document.getElementById('detailClose').addEventListener('click',closeDetail);
detailOverlay.addEventListener('click',closeDetail);
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeDetail();});
})();
</script>
</body>
</html>