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
+198
View File
@@ -0,0 +1,198 @@
<!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>Project Approvals | 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}.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:disabled{opacity:.6;cursor:not-allowed}
.btn-primary{background:#7c3aed;color:#fff}.btn-primary:hover:not(:disabled){background:#6d28d9}
.btn-outline{background:#fff;color:#7c3aed;border:1.5px solid #7c3aed}.btn-outline:hover{background:#f5f3ff}
.btn-success{background:#10b981;color:#fff}.btn-success:hover:not(:disabled){background:#059669}
.btn-info{background:#3b82f6;color:#fff}.btn-info:hover{background:#2563eb}
.btn-warn{background:#f59e0b;color:#fff}.btn-warn:hover{background:#d97706}
.btn-sm{height:30px;padding:0 12px;font-size:11px}
.card{background:#fff;border-radius:14px;box-shadow:0 1px 8px rgba(0,0,0,.08);overflow:hidden;margin-bottom:18px}
.card-header{padding:14px 20px;border-bottom:2px solid #ede9fe;background:#faf5ff;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
.card-header h2{font-size:14px;font-weight:700;color:#7c3aed}
.card-body{padding:20px}
.tbl-wrap{overflow-x:auto}
table.list-tbl{border-collapse:collapse;min-width:100%;font-size:12px}
.list-tbl th{background:#f5f3ff;color:#4b5563;font-weight:700;padding:10px 12px;text-align:left;border-bottom:2px solid #ddd6fe;font-size:10px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}
.list-tbl td{padding:10px 12px;border-bottom:1px solid #f0f2f4;vertical-align:middle}
.empty{text-align:center;padding:48px;color:#9ca3af;font-size:13px}
.status-badge{display:inline-block;padding:3px 10px;border-radius:10px;font-size:10px;font-weight:700;letter-spacing:.3px}
.s-pending{background:#fef3c7;color:#92400e}
.s-purchase{background:#fce7f3;color:#9d174d}
.s-engineering{background:#dbeafe;color:#1d4ed8}
.s-qa{background:#ecfdf5;color:#065f46}
.s-qc{background:#fdf4ff;color:#7e22ce}
.s-legal{background:#fff7ed;color:#c2410c}
.s-owner{background:#e0e7ff;color:#4338ca}
.s-plant{background:#f0fdf4;color:#14532d}
.s-approved{background:#d1fae5;color:#065f46}
.s-rejected{background:#fee2e2;color:#991b1b}
/* quick approve panel */
.approve-panel{background:#fefce8;border:2px solid #fbbf24;border-radius:10px;padding:14px 16px;margin-top:10px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.approve-panel label{font-size:12px;font-weight:700;color:#92400e}
.approve-panel input[type=date]{border:1.5px solid #fbbf24;border-radius:8px;padding:6px 10px;font-size:12px;font-family:inherit;outline:none}
.approve-panel input[type=text]{border:1.5px solid #fbbf24;border-radius:8px;padding:6px 10px;font-size:12px;font-family:inherit;outline:none;width:180px}
.row-expand{background:#fefce8}
.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>Project</span> Approvals</h1>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<select id="filterStatus" style="height:34px;padding:0 12px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:12px;font-family:inherit;outline:none" onchange="loadApprovals()">
<option value="">All In-Progress</option>
<option value="PENDING">Pending (Purchase)</option>
<option value="PURCHASE">Awaiting Engineering</option>
<option value="ENGINEERING">Awaiting QA</option>
<option value="QA_DONE">Awaiting QC</option>
<option value="QC_DONE">Awaiting Legal</option>
<option value="LEGAL">Awaiting Owner</option>
<option value="OWNER">Awaiting Plant Mgr</option>
<option value="PLANT">Awaiting Finance</option>
</select>
<button class="btn btn-outline" onclick="loadApprovals()">Refresh</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Pending Approvals</h2>
<span id="countBadge" style="font-size:12px;color:#6b7280"></span>
</div>
<div class="tbl-wrap">
<table class="list-tbl">
<thead><tr>
<th>Code</th>
<th>Project Name</th>
<th>Current Step</th>
<th>Approver</th>
<th>Est. Cost (&#8377;)</th>
<th>Submitted By</th>
<th>Actions</th>
</tr></thead>
<tbody id="appBody"><tr><td colspan="7" class="empty">Loading&#8230;</td></tr></tbody>
</table>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
(function(){
const tok=sessionStorage.getItem('portal_token');
if(!tok){window.location.href='/';return;}
let user=null;
try{user=JSON.parse(sessionStorage.getItem('portal_user'));}catch(_){}
const isFullAdmin=user?.role==='admin'||user?.role==='sap_adder';
const userDept=user?.approvalDept||null;
// Returns true if this user can approve the given step key
function canApproveStep(stepKey){return isFullAdmin||(user?.role==='project_approver'&&userDept===stepKey);}
const hdrs={'Authorization':'Bearer '+tok,'Content-Type':'application/json'};
const STEPS=[
{key:'purchase', from:'PENDING', to:'PURCHASE', label:'Purchase', person:'Mr. GS Negi / Mr. Rahul'},
{key:'engineering', from:'PURCHASE', to:'ENGINEERING', label:'Engineering', person:'Mr. Piyush / Ms. Piya'},
{key:'qa', from:'ENGINEERING', to:'QA_DONE', label:'QA', person:'Satish Shukla'},
{key:'qc', from:'QA_DONE', to:'QC_DONE', label:'QC', person:'Manoj Srivastav / Divya'},
{key:'legal', from:'QC_DONE', to:'LEGAL', label:'Legal', person:'Mr. Jubin Ahuja'},
{key:'owner', from:'LEGAL', to:'OWNER', label:'Project Owner', person:'Mr. Piyush'},
{key:'plant', from:'OWNER', to:'PLANT', label:'Plant Manager', person:'CV Singh', hasName:true},
{key:'finance', from:'PLANT', to:'APPROVED', label:'Finance & Accounts', person:'Mrs. Babita / Mrs. Rittu'},
];
// active = not DRAFT/APPROVED/REJECTED
const ACTIVE_STATUSES=STEPS.map(s=>s.from);
async function loadApprovals(){
const filterSt=document.getElementById('filterStatus').value;
const statuses=filterSt?[filterSt]:ACTIVE_STATUSES;
const results=await Promise.all(statuses.map(s=>
fetch('/api/projects?status='+s,{headers:hdrs}).then(r=>r.json())
));
const rows=results.flatMap(j=>j.success?j.data:[]);
// deduplicate by id
const seen=new Set();
const unique=rows.filter(r=>{if(seen.has(r.id))return false;seen.add(r.id);return true;});
document.getElementById('countBadge').textContent=unique.length+' project'+(unique.length===1?'':'s');
const tbody=document.getElementById('appBody');
if(!unique.length){tbody.innerHTML='<tr><td colspan="7" class="empty">No projects pending approval</td></tr>';return;}
tbody.innerHTML=unique.map(p=>{
const step=STEPS.find(s=>s.from===p.status);
return`<tr id="row-${p.id}">
<td><b style="color:#7c3aed">${esc(p.project_code||'—')}</b></td>
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis">${esc(p.project_name)}</td>
<td>${step?`<span class="status-badge ${stCls(p.status)}">${esc(step.label)}</span>`:'—'}</td>
<td style="font-size:12px;color:#374151">${step?esc(step.person):'—'}</td>
<td style="text-align:right">&#8377; ${fmtNum(p.total_est_cost)}</td>
<td>${esc(p.submitted_by||'')}</td>
<td style="white-space:nowrap">
<button class="btn btn-info btn-sm" onclick="viewProject(${p.id})">View</button>
${canApproveStep(step?.key)?`<button class="btn btn-success btn-sm" onclick="toggleApprove(${p.id},'${step.key}',${step.hasName||false})">&#10003; Approve</button>`:''}
</td>
</tr>
<tr id="panel-${p.id}" style="display:none"><td colspan="7" style="padding:0 12px 12px">
<div class="approve-panel" id="apanel-${p.id}">
<label>Approval Date:</label>
<input type="date" id="dt-${p.id}" value="${today()}">
${step&&step.hasName?`<input type="text" id="nm-${p.id}" placeholder="Plant Manager name" value="CV Singh">`:'' }
<button class="btn btn-success btn-sm" onclick="doApprove(${p.id},'${step?.key}',true)">&#10003; Approve</button>
<button class="btn btn-outline btn-sm" style="border-color:#7c3aed;color:#7c3aed" onclick="doApprove(${p.id},'${step?.key}',false)">Skip / N/A</button>
<button class="btn btn-warn btn-sm" onclick="toggleApprove(${p.id})">Cancel</button>
</div>
</td></tr>`;
}).join('');
}
window.loadApprovals=loadApprovals;
loadApprovals();
function toggleApprove(id){
const panel=document.getElementById('panel-'+id);
panel.style.display=panel.style.display==='none'?'table-row':'none';
}
window.toggleApprove=toggleApprove;
function viewProject(id){window.location.href='/project-form?id='+id;}
window.viewProject=viewProject;
async function doApprove(id,stepKey,withDate){
const dt=withDate?document.getElementById('dt-'+id)?.value:null;
if(withDate&&!dt){showToast('Please enter the approval date','error');return;}
const nmEl=document.getElementById('nm-'+id);
const name=nmEl?nmEl.value:null;
const r=await fetch('/api/projects/'+id+'/approve',{method:'POST',headers:hdrs,body:JSON.stringify({step:stepKey,applicable:withDate,date:dt,name})});
const j=await r.json();
if(j.success){showToast(withDate?'Approved!':'Marked Not Applicable','success');setTimeout(loadApprovals,600);}
else showToast(j.message,'error');
}
window.doApprove=doApprove;
function stCls(s){return{PENDING:'s-pending',PURCHASE:'s-purchase',ENGINEERING:'s-engineering',QA_DONE:'s-qa',QC_DONE:'s-qc',LEGAL:'s-legal',OWNER:'s-owner',PLANT:'s-plant'}[s]||'s-pending';}
function fmtNum(v,dec=2){return Number(v||0).toLocaleString('en-IN',{minimumFractionDigits:dec,maximumFractionDigits:dec});}
function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function today(){return new Date().toISOString().slice(0,10);}
function showToast(msg,type='info'){const t=document.getElementById('toast');t.textContent=msg;t.className='toast '+type+' show';setTimeout(()=>t.classList.remove('show'),3500);}
})();
</script>
</body>
</html>