612 lines
28 KiB
HTML
612 lines
28 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">
|
||
<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>« Prev</button>
|
||
<span id="pageInfo">Page 1</span>
|
||
<button id="btnNext" disabled>Next »</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">✕</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,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||
|
||
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>
|