Files
sap-erp/public/business-master.html
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

615 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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=31"></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');
const total=typeof json.total==='number'?json.total:null;
pageInfo.textContent=total!=null
? `Page ${Math.floor(skip/TOP)+1} of ${Math.max(1,Math.ceil(total/TOP))} · ${total} record${total===1?'':'s'}`
: `Page ${Math.floor(skip/TOP)+1}`;
btnPrev.disabled=skip===0;
btnNext.disabled=total!=null?(skip+TOP)>=total: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>