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

1354 lines
82 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>Project Approval | 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>
:root{
--p:#7c3aed;--pd:#5b21b6;--pl:#ede9fe;--pg:rgba(124,58,237,.12);
--s:#10b981;--d:#ef4444;--w:#f59e0b;--i:#3b82f6;
--bg1:#eef0ff;--bg2:#faf5ff;
--card:rgba(255,255,255,.92);
--border:rgba(124,58,237,.12);
--text:#1e1b4b;--muted:#6b7280;--surface:#f8f7ff;
--r:18px;--rs:10px;
--sh:0 8px 32px rgba(124,58,237,.08),0 2px 8px rgba(0,0,0,.04);
--sh-sm:0 2px 12px rgba(124,58,237,.08);
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Space Grotesk',sans-serif;background:linear-gradient(135deg,var(--bg1) 0%,var(--bg2) 50%,var(--bg1) 100%);background-attachment:fixed;color:var(--text);min-height:100vh}
.pg{padding:24px 28px}
/* ── Header ── */
.hdr{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:22px}
.hdr h1{font-size:22px;font-weight:800;letter-spacing:-.4px;background:linear-gradient(135deg,#1e1b4b,var(--p));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.hdr h1 span{background:linear-gradient(135deg,var(--p),#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
/* ── Buttons ── */
.btn{height:38px;padding:0 18px;border-radius:var(--rs);font-size:12px;font-weight:700;font-family:inherit;border:none;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:7px;white-space:nowrap;letter-spacing:.2px}
.btn:disabled{opacity:.45;cursor:not-allowed;transform:none!important;box-shadow:none!important}
.btn-primary{background:linear-gradient(135deg,var(--p),var(--pd));color:#fff;box-shadow:0 4px 14px rgba(124,58,237,.35)}
.btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 22px rgba(124,58,237,.4)}
.btn-outline{background:var(--card);color:var(--p);border:1.5px solid rgba(124,58,237,.3);box-shadow:var(--sh-sm)}
.btn-outline:hover{background:var(--pl);border-color:var(--p);transform:translateY(-1px)}
.btn-success{background:linear-gradient(135deg,#10b981,#059669);color:#fff;box-shadow:0 4px 14px rgba(16,185,129,.3)}
.btn-success:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 20px rgba(16,185,129,.38)}
.btn-danger{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff;box-shadow:0 4px 14px rgba(239,68,68,.3)}
.btn-danger:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(239,68,68,.38)}
.btn-warn{background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;box-shadow:0 4px 14px rgba(245,158,11,.3)}
.btn-warn:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(245,158,11,.38)}
.btn-info{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff;box-shadow:0 4px 14px rgba(59,130,246,.3)}
.btn-info:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(59,130,246,.38)}
.btn-sm{height:30px;padding:0 12px;font-size:11px}
/* ── Cards ── */
.card{background:var(--card);backdrop-filter:blur(12px);border-radius:var(--r);box-shadow:var(--sh);border:1px solid var(--border);overflow:hidden;margin-bottom:20px}
.card-header{padding:16px 22px;border-bottom:1px solid rgba(124,58,237,.1);background:linear-gradient(135deg,#faf5ff,#f3f0ff);display:flex;align-items:center;justify-content:space-between}
.card-header h2{font-size:13px;font-weight:800;color:var(--p);letter-spacing:.3px;text-transform:uppercase}
.card-body{padding:22px}
/* ── Form ── */
.form-grid{display:grid;gap:18px}
.g2{grid-template-columns:1fr 1fr}.g3{grid-template-columns:1fr 1fr 1fr}.g4{grid-template-columns:1fr 1fr 1fr 1fr}
@media(max-width:768px){.g2,.g3,.g4{grid-template-columns:1fr}}
.fg{display:flex;flex-direction:column;gap:6px}
.fg label{font-size:10px;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.7px}
.fg label span{color:var(--d)}
.fg input,.fg select,.fg textarea{border:1.5px solid rgba(124,58,237,.18);border-radius:var(--rs);padding:10px 14px;font-size:13px;font-family:inherit;color:var(--text);background:#fff;outline:none;transition:all .2s}
.fg input:focus,.fg select:focus,.fg textarea:focus{border-color:var(--p);box-shadow:0 0 0 3px var(--pg)}
.fg input[readonly]{background:#f3f0ff;color:#374151;font-weight:700;cursor:default;border-color:rgba(124,58,237,.12)}
.fg textarea{resize:vertical;min-height:72px}
/* ── Cost breakdown ── */
.cost-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 0;border-bottom:1px solid rgba(124,58,237,.06)}
.cost-row:last-child{border-bottom:none}
.cost-label{font-size:13px;color:#374151;font-weight:600;flex:1}
.cost-inp-wrap{display:flex;align-items:stretch;border:1.5px solid rgba(124,58,237,.18);border-radius:var(--rs);overflow:hidden;transition:all .2s}
.cost-inp-wrap:focus-within{border-color:var(--p);box-shadow:0 0 0 3px var(--pg)}
.cost-prefix{padding:0 13px;background:linear-gradient(135deg,#f5f3ff,#ede9fe);color:var(--p);font-weight:800;font-size:13px;border-right:1.5px solid rgba(124,58,237,.15);display:flex;align-items:center;white-space:nowrap;flex-shrink:0}
.cost-input{width:160px;text-align:right;border:none;outline:none;padding:10px 14px;font-size:13px;font-family:inherit;color:var(--text);background:#fff}
.cost-total{background:linear-gradient(135deg,#f0fdf4,#dcfce7);font-weight:700;color:#059669;font-size:13px;border-radius:14px;padding:16px 20px;display:flex;justify-content:space-between;align-items:center;margin-top:16px;border:1.5px solid #6ee7b7;box-shadow:0 4px 16px rgba(16,185,129,.12)}
.cost-total-amt{font-size:22px;font-weight:800;color:#10b981;letter-spacing:-.5px}
/* ── Team table ── */
.team-table{width:100%;border-collapse:collapse;table-layout:fixed}
.team-table th{background:linear-gradient(135deg,#f5f3ff,#ede9fe);padding:10px 14px;text-align:left;font-size:10px;font-weight:800;color:var(--pd);text-transform:uppercase;letter-spacing:.7px;border-bottom:2px solid rgba(124,58,237,.15)}
.team-table td{padding:10px 12px;border-bottom:1px solid rgba(124,58,237,.06);vertical-align:middle;overflow:hidden}
.team-table tr:last-child td{border-bottom:none}
.team-table td input,.team-table td select{width:100%;border:1.5px solid rgba(124,58,237,.18);border-radius:8px;padding:7px 10px;font-size:12px;font-family:inherit;outline:none;background:#fff;transition:all .2s}
.team-table td input:focus,.team-table td select:focus{border-color:var(--p);box-shadow:0 0 0 3px var(--pg)}
.team-table td.del-cell{width:38px;text-align:center}
.team-table th:nth-child(1),.team-table td:nth-child(1){width:130px}
.team-table th:nth-child(2),.team-table td:nth-child(2){width:18%}
.team-table th:nth-child(3),.team-table td:nth-child(3){width:12%}
.team-table th:nth-child(4),.team-table td:nth-child(4){width:auto}
.team-table th:nth-child(5),.team-table td:nth-child(5){width:42px}
.team-applicable{cursor:pointer;transition:all .2s}
.team-applicable.ok{border-color:#6ee7b7!important;background:linear-gradient(135deg,#f0fdf4,#dcfce7)!important;color:#065f46!important;font-weight:700}
.team-applicable.na{border-color:#fca5a5!important;background:linear-gradient(135deg,#fef2f2,#fee2e2)!important;color:#dc2626!important;font-weight:700}
.team-func-cell{font-size:12px;font-weight:700;color:#374151}
.team-resp-ta{width:100%;border:1.5px solid rgba(124,58,237,.18);border-radius:8px;padding:8px 10px;font-size:11.5px;line-height:1.55;font-family:inherit;outline:none;resize:none!important;height:72px!important;min-height:72px!important;max-height:72px!important;overflow-y:auto!important;display:block;color:#374151;word-break:break-word;white-space:pre-wrap;background:#fff;transition:all .2s}
.team-resp-ta:focus{border-color:var(--p);box-shadow:0 0 0 3px var(--pg)}
.team-resp-ta[readonly]{background:#f8f7ff;color:#4b5563;cursor:default;border-color:rgba(124,58,237,.1)}
.team-resp-ta[readonly]:focus{border-color:rgba(124,58,237,.1);box-shadow:none}
.team-name-inp{width:100%;border:1.5px solid rgba(124,58,237,.18);border-radius:8px;padding:7px 10px;font-size:12px;font-family:inherit;outline:none;background:#fff;transition:all .2s}
.team-name-inp:focus{border-color:var(--p);box-shadow:0 0 0 3px var(--pg)}
.team-name-inp:disabled{background:linear-gradient(135deg,#fef2f2,#fee2e2);color:#dc2626;font-weight:700;border-color:#fca5a5}
/* ── Status badges ── */
.status-badge{display:inline-flex;align-items:center;gap:5px;padding:4px 12px;border-radius:20px;font-size:10px;font-weight:700;letter-spacing:.3px}
.s-draft{background:#f1f5f9;color:#475569}
.s-pending{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#92400e}
.s-purchase{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#9d174d}
.s-engineering{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1d4ed8}
.s-qa{background:linear-gradient(135deg,#ecfdf5,#a7f3d0);color:#065f46}
.s-qc{background:linear-gradient(135deg,#fdf4ff,#f5d0fe);color:#7e22ce}
.s-legal{background:linear-gradient(135deg,#fff7ed,#fed7aa);color:#c2410c}
.s-owner{background:linear-gradient(135deg,#e0e7ff,#c7d2fe);color:#4338ca}
.s-plant{background:linear-gradient(135deg,#f0fdf4,#bbf7d0);color:#14532d}
.s-approved{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#065f46}
.s-rejected{background:linear-gradient(135deg,#fee2e2,#fecaca);color:#991b1b}
/* ── Approval steps (timeline) ── */
.step-row{display:flex;align-items:flex-start;gap:16px;padding:16px 18px;border-radius:14px;margin-bottom:8px;transition:all .2s;position:relative}
.step-done{background:linear-gradient(135deg,#f0fdf4,#dcfce7);border:1.5px solid #6ee7b7;box-shadow:0 2px 10px rgba(16,185,129,.08)}
.step-skipped{background:linear-gradient(135deg,#f5f3ff,#ede9fe);border:1.5px solid #c4b5fd}
.step-current{background:linear-gradient(135deg,#fffbeb,#fef3c7);border:2px solid #fbbf24;box-shadow:0 4px 16px rgba(251,191,36,.18)}
.step-pending{background:var(--surface);border:1.5px solid rgba(124,58,237,.1)}
.step-num{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;flex-shrink:0;margin-top:1px;box-shadow:0 2px 8px rgba(0,0,0,.12)}
.step-num-done{background:linear-gradient(135deg,#10b981,#059669);color:#fff}
.step-num-skip{background:linear-gradient(135deg,var(--p),var(--pd));color:#fff;font-size:10px}
.step-num-curr{background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff}
.step-num-pend{background:#e2e8f0;color:#9ca3af}
.step-info{flex:1}
.step-label{font-size:13px;font-weight:700;color:var(--text);margin-bottom:3px}
.step-person{font-size:12px;color:var(--muted)}
.step-date{font-size:12px;font-weight:700;color:#059669;white-space:nowrap;margin-top:4px}
.step-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;align-items:center}
.step-actions input[type=date]{border:1.5px solid rgba(124,58,237,.2);border-radius:8px;padding:7px 12px;font-size:12px;font-family:inherit;outline:none;background:#fff;transition:all .2s}
.step-actions input[type=date]:focus{border-color:var(--p);box-shadow:0 0 0 3px var(--pg)}
.step-actions input[type=text]{border:1.5px solid #fbbf24;border-radius:8px;padding:7px 12px;font-size:12px;font-family:inherit;outline:none;width:210px;background:#fff}
/* ── List table ── */
.tbl-wrap{overflow-x:auto}
table.list-tbl{border-collapse:collapse;min-width:100%;font-size:12px}
.list-tbl th{background:linear-gradient(135deg,#f5f3ff,#ede9fe);color:var(--pd);font-weight:800;padding:11px 14px;text-align:left;border-bottom:2px solid rgba(124,58,237,.15);font-size:10px;text-transform:uppercase;letter-spacing:.7px;white-space:nowrap}
.list-tbl td{padding:11px 14px;border-bottom:1px solid rgba(124,58,237,.06);white-space:nowrap;transition:background .15s}
.list-tbl tr:hover td{background:linear-gradient(90deg,rgba(124,58,237,.04),rgba(167,139,250,.03));cursor:pointer}
.empty{text-align:center;padding:56px;color:var(--muted);font-size:13px}
/* ── Category cards ── */
.category-cards{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.cat-card{border:2px solid rgba(124,58,237,.15);border-radius:14px;padding:16px;cursor:pointer;transition:all .2s;background:var(--card)}
.cat-card:hover{border-color:rgba(124,58,237,.3);transform:translateY(-2px);box-shadow:var(--sh)}
.cat-card.selected{border-color:var(--p);background:linear-gradient(135deg,#faf5ff,var(--pl));box-shadow:0 4px 16px var(--pg)}
.cat-card .cat-title{font-weight:800;font-size:13px;margin-bottom:5px}
.cat-card .cat-sub{font-size:11px;color:var(--muted)}
.cat-card.A .cat-title{color:#dc2626}
.cat-card.B .cat-title{color:#059669}
.radio-group{display:flex;gap:20px;margin-top:8px}
.radio-group label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;font-weight:600;color:var(--text)}
.radio-group input[type=radio]{width:16px;height:16px;accent-color:var(--p)}
.category-tag{display:inline-block;padding:4px 12px;border-radius:20px;font-size:11px;font-weight:700}
.cat-a{background:linear-gradient(135deg,#fee2e2,#fecaca);color:#991b1b}
.cat-b{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#065f46}
/* ── Attachments ── */
.att-list{display:flex;flex-direction:column;gap:8px;margin-top:12px}
.att-item{display:flex;align-items:center;justify-content:space-between;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:9px 14px;font-size:12px;transition:all .2s}
.att-item:hover{border-color:rgba(124,58,237,.25);box-shadow:var(--sh-sm)}
.att-item a{color:var(--p);font-weight:600;text-decoration:none;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.att-item a:hover{text-decoration:underline}
.att-size{color:var(--muted);font-size:11px;margin:0 10px;flex-shrink:0}
/* ── Modal ── */
.modal-bg{position:fixed;inset:0;background:rgba(15,10,40,.45);backdrop-filter:blur(4px);z-index:800;display:flex;align-items:center;justify-content:center;padding:16px}
.modal{background:#fff;border-radius:20px;padding:28px;max-width:520px;width:100%;box-shadow:0 24px 80px rgba(0,0,0,.22);border:1px solid var(--border)}
.modal h3{font-size:16px;font-weight:800;margin-bottom:16px;color:var(--text)}
.modal-step{display:flex;align-items:center;gap:12px;padding:10px 14px;border-radius:10px;background:var(--surface);margin-bottom:6px;font-size:12px;border:1px solid var(--border);transition:all .2s}
.modal-step:hover{border-color:rgba(124,58,237,.25)}
.modal-step-num{width:26px;height:26px;border-radius:50%;background:linear-gradient(135deg,var(--p),var(--pd));color:#fff;font-weight:800;font-size:11px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.modal-step-label{font-weight:700;color:var(--text)}
.modal-step-person{color:var(--muted);margin-left:4px}
/* ── Toast ── */
.toast{position:fixed;bottom:28px;right:28px;z-index:9999;padding:14px 22px;border-radius:14px;font-size:13px;font-weight:700;color:#fff;box-shadow:0 8px 32px rgba(0,0,0,.18);transform:translateY(20px);opacity:0;transition:all .35s cubic-bezier(.34,1.56,.64,1);pointer-events:none;max-width:360px}
.toast.show{transform:translateY(0);opacity:1}
.toast.success{background:linear-gradient(135deg,#10b981,#059669)}
.toast.error{background:linear-gradient(135deg,#ef4444,#dc2626)}
.toast.info{background:linear-gradient(135deg,#3b82f6,#2563eb)}
#printArea{display:none}
@media print{
body>*{display:none!important}
#printArea{display:block!important}
.print-page{page:landscape-page}
@page landscape-page{size:A4 landscape;margin:8mm}
}
@media(max-width:768px){.pg{padding:16px 14px}}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<script src="/sidebar.js?v=31"></script>
<div class="pg" id="main">
<!-- LIST VIEW -->
<div id="viewList" style="display:none">
<div class="hdr">
<h1><span>Project</span> Approval Requests</h1>
<button class="btn btn-primary" onclick="showForm()">+ New Project</button>
</div>
<div class="card">
<div class="card-header">
<h2>All Projects</h2>
<select id="filterStatus" class="btn btn-outline" style="height:32px;font-size:11px" onchange="loadList()">
<option value="">All Status</option>
<option value="DRAFT">Draft</option>
<option value="PENDING">Pending (Purchase)</option>
<option value="PURCHASE">Purchase Approved</option>
<option value="ENGINEERING">Engineering Approved</option>
<option value="QA_DONE">QA Done</option>
<option value="QC_DONE">QC Done</option>
<option value="LEGAL">Legal Approved</option>
<option value="OWNER">Owner Approved</option>
<option value="PLANT">Plant Approved</option>
<option value="APPROVED">Fully Approved</option>
<option value="REJECTED">Rejected</option>
</select>
</div>
<div class="tbl-wrap">
<table class="list-tbl">
<thead><tr>
<th>Code</th><th>Project Name</th><th>Cat</th><th>Department</th>
<th>Prepared By</th><th>Est. Cost (&#8377;)</th><th>Start</th><th>End</th>
<th>Status</th><th>Actions</th>
</tr></thead>
<tbody id="listBody"><tr><td colspan="10" class="empty">Loading&#8230;</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- FORM VIEW -->
<div id="viewForm" style="display:none">
<div class="hdr">
<h1 id="formTitle"><span>New</span> Project Request</h1>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-outline" onclick="goList()">&#8592; All Projects</button>
<button class="btn btn-info no-print" id="btnPrint" onclick="printForm()" style="display:none">Print</button>
<button class="btn btn-warn no-print" id="btnDraft" onclick="saveProject('DRAFT')">Save Draft</button>
<button class="btn btn-primary no-print" id="btnSubmit" onclick="confirmSubmit()">&#10003; Submit for Approval</button>
<button class="btn btn-success no-print" id="btnFinanceSave" style="display:none" onclick="saveFinanceEdit()">Save Changes</button>
<button class="btn btn-danger no-print" id="btnReject" style="display:none" onclick="doReject()">&#10007; Reject</button>
</div>
</div>
<div id="statusBar" style="display:none;margin-bottom:14px;background:#fff;border-radius:10px;padding:12px 18px;box-shadow:0 1px 6px rgba(0,0,0,.07);display:flex;align-items:center;gap:12px;flex-wrap:wrap">
<span style="font-size:12px;font-weight:600;color:#6b7280">Code:</span>
<span id="codeDisplay" style="font-size:13px;font-weight:700;color:#7c3aed"></span>
<span style="font-size:12px;font-weight:600;color:#6b7280">Status:</span>
<span id="statusDisplay"></span>
<span style="font-size:12px;font-weight:600;color:#6b7280">Submitted by:</span>
<span id="submittedByDisplay" style="font-size:12px;color:#1a1a2e"></span>
</div>
<!-- S1: Project Info -->
<div class="card">
<div class="card-header"><h2>Project Information</h2></div>
<div class="card-body">
<div class="form-grid g3">
<div class="fg">
<label>Project Code</label>
<input type="text" id="project_code" readonly placeholder="Auto-generated">
</div>
<div class="fg" style="grid-column:span 2">
<label>Project Name <span>*</span></label>
<input type="text" id="project_name" placeholder="Enter project name">
</div>
</div>
<div style="margin-top:16px">
<label style="font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;display:block;margin-bottom:8px">Project Category</label>
<div class="category-cards">
<div class="cat-card A" id="catA" onclick="selectCat('A')">
<div class="cat-title">Category A &#8212; Heavy Investment</div>
<div class="cat-sub">Project Greater than &#8377;2.5 Lakh</div>
</div>
<div class="cat-card B selected" id="catB" onclick="selectCat('B')">
<div class="cat-title">Category B &#8212; Regular / Low Cost</div>
<div class="cat-sub">Project Less than &#8377;2.5 Lakh</div>
</div>
</div>
<input type="hidden" id="project_category" value="B">
</div>
<div class="form-grid g4" style="margin-top:16px">
<div class="fg"><label>Total Days</label><input type="number" id="total_days" min="1" placeholder="e.g. 175"></div>
<div class="fg"><label>Initiation Date</label><input type="date" id="initiation_dt"></div>
<div class="fg"><label>Est. Start Date</label><input type="date" id="est_start_dt"></div>
<div class="fg"><label>Est. Finish Date</label><input type="date" id="est_finish_dt"></div>
</div>
</div>
</div>
<!-- S2: Purpose & ROI -->
<div class="card">
<div class="card-header"><h2>Purpose &amp; Expected Benefits</h2></div>
<div class="card-body">
<div class="form-grid g2">
<div class="fg"><label>Purpose of Project <span>*</span></label><textarea id="purpose" rows="4" placeholder="Describe the purpose&#8230;"></textarea></div>
<div class="fg"><label>Expected Benefits / ROI</label><textarea id="roi" rows="4" placeholder="Describe expected benefits&#8230;"></textarea></div>
</div>
</div>
</div>
<!-- S3: Cost -->
<div class="card">
<div class="card-header"><h2>Cost Breakdown (INR)</h2></div>
<div class="card-body" style="max-width:500px">
<div class="cost-row">
<span class="cost-label">Labour Cost</span>
<div class="cost-inp-wrap">
<span class="cost-prefix">&#8377;</span>
<input type="number" class="cost-input" id="labour_cost" min="0" step="0.01" placeholder="0.00" oninput="calcTotal()">
</div>
</div>
<div class="cost-row">
<span class="cost-label">Material Cost</span>
<div class="cost-inp-wrap">
<span class="cost-prefix">&#8377;</span>
<input type="number" class="cost-input" id="material_cost" min="0" step="0.01" placeholder="0.00" oninput="calcTotal()">
</div>
</div>
<div class="cost-row">
<span class="cost-label">Consultancy Fees</span>
<div class="cost-inp-wrap">
<span class="cost-prefix">&#8377;</span>
<input type="number" class="cost-input" id="consultancy_fees" min="0" step="0.01" placeholder="0.00" oninput="calcTotal()">
</div>
</div>
<div class="cost-row">
<span class="cost-label">Promotional Fees</span>
<div class="cost-inp-wrap">
<span class="cost-prefix">&#8377;</span>
<input type="number" class="cost-input" id="promotional_fees" min="0" step="0.01" placeholder="0.00" oninput="calcTotal()">
</div>
</div>
<div class="cost-row">
<span class="cost-label">Other Cost</span>
<div class="cost-inp-wrap">
<span class="cost-prefix">&#8377;</span>
<input type="number" class="cost-input" id="other_cost" min="0" step="0.01" placeholder="0.00" oninput="calcTotal()">
</div>
</div>
<div class="cost-total"><span>Total Estimated Project Cost</span><span id="totalDisplay">&#8377; 0.00</span></div>
<input type="hidden" id="total_est_cost" value="0">
</div>
</div>
<!-- S4: Project Team -->
<div class="card">
<div class="card-header">
<h2>Project Team</h2>
<button class="btn btn-outline btn-sm no-print" onclick="addCustomTeamRow()">+ Add Custom</button>
</div>
<div class="card-body" style="padding:0">
<table class="team-table">
<thead><tr>
<th style="width:120px">Status</th>
<th style="width:18%">Name</th>
<th style="width:12%">Function</th>
<th>Responsibilities</th>
<th class="del-cell no-print"></th>
</tr></thead>
<tbody id="teamBody"></tbody>
</table>
</div>
</div>
<!-- S5: Prepared By (readonly defaults) -->
<div class="card">
<div class="card-header"><h2>Prepared By</h2></div>
<div class="card-body">
<div class="form-grid g3">
<div class="fg"><label>Department Name</label><input type="text" id="department_name" value="MAINTENANCE" readonly></div>
<div class="fg"><label>Full Name of Person</label><input type="text" id="prepared_by_name" value="Mr. Piyush Sharma" readonly></div>
<div class="fg"><label>Designation</label><input type="text" id="prepared_by_desig" value="Sr. Engineer" readonly></div>
</div>
<div class="form-grid g2" style="margin-top:14px">
<div class="fg"><label>Accountable Person for Project</label><input type="text" id="accountable_person" value="Mr. Piyush" readonly></div>
</div>
</div>
</div>
<!-- S6: Page 2 Details -->
<div class="card">
<div class="card-header"><h2>Project Type &amp; Requirements</h2></div>
<div class="card-body">
<div style="margin-bottom:16px">
<label style="font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;display:block;margin-bottom:6px">Please Tick &#8212; Project Type</label>
<div class="radio-group">
<label><input type="radio" name="project_type" id="type_new" value="NEW" checked> New Project</label>
<label><input type="radio" name="project_type" id="type_upgradation" value="UPGRADATION"> Upgradation</label>
</div>
</div>
<div class="form-grid g2">
<div class="fg"><label>Requirement of New Project</label><textarea id="req_new_project" rows="3" placeholder="e.g. For the ventilation work of Sheet Plant area."></textarea></div>
<div class="fg"><label>Requirement of the Upgradation</label><textarea id="req_upgradation" rows="3" placeholder="e.g. For the ventilation work of Sheet Plant area."></textarea></div>
</div>
<div class="form-grid g2" style="margin-top:14px">
<div class="fg"><label>Time of Cost Recovery</label><input type="text" id="time_cost_recovery" placeholder="e.g. NA"></div>
<div class="fg"><label>Remarks (if any)</label><input type="text" id="remarks_p2" placeholder="e.g. NA"></div>
</div>
</div>
</div>
<!-- S7: Attachments (mandatory) -->
<div class="card">
<div class="card-header"><h2>Attachments <span style="color:#ef4444">*</span></h2></div>
<div class="card-body">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px">
<input type="file" id="attFileInput" multiple style="font-size:12px;flex:1;min-width:200px;border:1.5px solid #e2e8f0;border-radius:8px;padding:7px 10px;font-family:inherit">
<button id="btnManualUpload" class="btn btn-info btn-sm no-print" onclick="uploadAttachments()" style="display:none">Upload Now</button>
</div>
<div style="font-size:11px;color:#9ca3af">Files are saved automatically when you submit / save the form.</div>
<div id="attList" class="att-list" style="margin-top:10px"></div>
</div>
</div>
<!-- S8: Approval Workflow (shown when status != DRAFT) -->
<!-- S8: Approval Workflow (shown when status != DRAFT) -->
<div class="card" id="approvalCard" style="display:none">
<div class="card-header"><h2>Approval Workflow</h2></div>
<div class="card-body">
<div id="approvalSteps"></div>
<!-- Comments — always visible for any non-draft project -->
<div style="margin-top:18px;border-top:2px solid #ede9fe;padding-top:16px">
<div style="font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">Comments / Notes</div>
<!-- existing saved comment (read-only display) -->
<div id="commentsDisplay" style="display:none;background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:8px;padding:10px 14px;font-size:13px;color:#374151;margin-bottom:10px;white-space:pre-wrap"></div>
<!-- add new comment -->
<div id="commentInputWrap" style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-start">
<textarea id="commentNew" rows="2" placeholder="Add a comment or note…"
style="flex:1;min-width:200px;border:1.5px solid #e2e8f0;border-radius:8px;padding:8px 12px;font-size:13px;font-family:inherit;outline:none;resize:vertical"></textarea>
<button class="btn btn-outline btn-sm no-print" style="margin-top:2px" onclick="saveComments()">Save Comment</button>
</div>
</div>
<!-- Reject with reason — only for admin on active projects -->
<div id="rejectSection" style="display:none;margin-top:14px;border-top:2px solid #fee2e2;padding-top:14px">
<div style="font-size:11px;font-weight:700;color:#dc2626;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">&#10007; Reject Project</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-start">
<textarea id="rejectReason" rows="2" placeholder="Reason for rejection (required)…"
style="flex:1;min-width:200px;border:1.5px solid #fca5a5;border-radius:8px;padding:8px 12px;font-size:13px;font-family:inherit;outline:none;resize:vertical"></textarea>
<button class="btn btn-danger btn-sm no-print" style="margin-top:2px" onclick="doRejectInline()">&#10007; Confirm Reject</button>
</div>
</div>
</div>
</div>
</div><!-- /viewForm -->
</div>
<div class="toast" id="toast"></div>
<!-- Submit confirmation modal -->
<div id="submitModal" class="modal-bg" style="display:none">
<div class="modal">
<h3>Submit for Approval</h3>
<p style="font-size:12px;color:#6b7280;margin-bottom:14px">This project will go through the following approval steps in sequence:</p>
<div id="submitModalSteps"></div>
<div style="display:flex;gap:10px;margin-top:20px;justify-content:flex-end">
<button class="btn btn-outline" onclick="document.getElementById('submitModal').style.display='none'">Cancel</button>
<button class="btn btn-primary" onclick="doSubmit()">&#10003; Confirm &amp; Submit</button>
</div>
</div>
</div>
<div id="printArea">
<!-- ═══════════════ PAGE 1 — Main Form (landscape) ═══════════════ -->
<div class="print-page" style="page-break-after:always">
<!-- Title -->
<div style="text-align:center;font-size:14pt;font-weight:900;letter-spacing:.5px;border:2px solid #000;padding:6px 0;margin-bottom:0">
Approval Request Sheet for New Project or Upgradation of Existing
</div>
<!-- Top info row -->
<table style="width:100%;border-collapse:collapse;font-size:9pt;border-top:none">
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:14%">Project Code</td>
<td style="border:1px solid #000;padding:4px 6px;width:12%" id="p1_code"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:14%">Total Number of Days in Project</td>
<td style="border:1px solid #000;padding:4px 6px;width:8%" id="p1_days"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:10%">Initiation Dt.</td>
<td style="border:1px solid #000;padding:4px 6px;width:10%" id="p1_init"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:10%">Est. Start Dt.</td>
<td style="border:1px solid #000;padding:4px 6px;width:10%" id="p1_start"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:10%">Est. Finish Dt.</td>
<td style="border:1px solid #000;padding:4px 6px;width:10%" id="p1_end"></td>
</tr>
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Name of Project</td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;font-size:10pt" colspan="3" id="p1_name"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">PROJECT CATEGORIES</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="5" id="p1_cat"></td>
</tr>
</table>
<!-- Purpose / ROI / Cost / Accountable row -->
<table style="width:100%;border-collapse:collapse;font-size:9pt">
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:9%;vertical-align:top">Purpose</td>
<td style="border:1px solid #000;padding:4px 6px;width:25%;vertical-align:top" id="p1_purpose"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:14%;vertical-align:top">Expected benefits (ROI)</td>
<td style="border:1px solid #000;padding:4px 6px;width:20%;vertical-align:top" id="p1_roi"></td>
<td style="border:1px solid #000;padding:0;vertical-align:top;width:32%" colspan="2">
<!-- cost sub-table -->
<table style="width:100%;border-collapse:collapse;font-size:8.5pt;height:100%">
<tr>
<td style="border-bottom:1px solid #000;border-right:1px solid #000;padding:3px 5px;font-weight:700">Labour Cost</td>
<td style="border-bottom:1px solid #000;padding:3px 5px" id="p1_labour"></td>
<td style="border-bottom:1px solid #000;border-left:1px solid #000;padding:3px 5px;font-weight:700" rowspan="2">Material + Labour Cost</td>
<td style="border-bottom:1px solid #000;padding:3px 5px" rowspan="2" id="p1_mat_lab"></td>
</tr>
<tr>
<td style="border-bottom:1px solid #000;border-right:1px solid #000;padding:3px 5px;font-weight:700">Material Cost</td>
<td style="border-bottom:1px solid #000;padding:3px 5px" id="p1_material"></td>
</tr>
<tr>
<td style="border-bottom:1px solid #000;border-right:1px solid #000;padding:3px 5px;font-weight:700;font-size:8pt" colspan="2">OTHER COST (IF ANY)<br><span style="font-weight:400">CONSULTANCY FEES,<br>PROMOTIONAL FEES AND OTHER</span></td>
<td style="border-bottom:1px solid #000;border-left:1px solid #000;padding:3px 5px;font-weight:700">Total Est. Project Cost</td>
<td style="border-bottom:1px solid #000;padding:3px 5px;font-weight:700" id="p1_total"></td>
</tr>
<tr>
<td style="border-right:1px solid #000;padding:3px 5px;font-weight:700" colspan="2" id="p1_othercost"></td>
<td style="border-left:1px solid #000;padding:3px 5px;font-weight:700">NAME OF PROJECT<br>ACCOUNTABLE FOR PROJECT</td>
<td style="padding:3px 5px" id="p1_acct"></td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Project Team table -->
<table style="width:100%;border-collapse:collapse;font-size:9pt">
<thead>
<tr style="background:#eee">
<th style="border:1px solid #000;padding:4px 6px;text-align:left;width:18%">Name &amp; Signature</th>
<th style="border:1px solid #000;padding:4px 6px;text-align:left;width:10%">Function</th>
<th style="border:1px solid #000;padding:4px 6px;text-align:left">Responsibilities</th>
</tr>
</thead>
<tbody id="p1_team_rows"></tbody>
</table>
<!-- Project Type row -->
<table style="width:100%;border-collapse:collapse;font-size:9pt;margin-top:0">
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:22%">Upgradation / New Project (Please Tick)</td>
<td style="border:1px solid #000;padding:6px 12px;width:30%" id="p1_ptype"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:12%">Requirement of the New Project</td>
<td style="border:1px solid #000;padding:4px 6px" id="p1_req_new"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:12%">Requirement of the Upgradation</td>
<td style="border:1px solid #000;padding:4px 6px" id="p1_req_upg"></td>
</tr>
</table>
</div>
<!-- ═══════════════ PAGE 2 — Approval/Sign-off (portrait) ═══════════════ -->
<div class="print-page">
<table style="width:100%;border-collapse:collapse;font-size:9pt">
<!-- Header roles -->
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:16%;vertical-align:top">Prepared By</td>
<td style="border:1px solid #000;padding:4px 6px;width:16%;vertical-align:top">
<div id="p2_prepname"></div>
<div id="p2_prepdesig" style="font-size:8pt;color:#333"></div>
<div id="p2_dept" style="font-size:8pt"></div>
</td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:12%">Name of Department</td>
<td style="border:1px solid #000;padding:4px 6px" id="p2_dept2"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:12%">Full Name of Person</td>
<td style="border:1px solid #000;padding:4px 6px" id="p2_fullname"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;width:12%">Designation</td>
<td style="border:1px solid #000;padding:4px 6px" id="p2_desig"></td>
</tr>
<!-- Requirement rows -->
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;vertical-align:top">Requirement of the New Project</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_req_new"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;vertical-align:top">Requirement of the Upgradation</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_req_upg"></td>
</tr>
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Time of Cost recovery</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_tcr"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Remarks if any</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_remarks"></td>
</tr>
<!-- Approval chain -->
<tr style="background:#eee">
<th style="border:1px solid #000;padding:4px 6px;text-align:left" colspan="2">Role</th>
<th style="border:1px solid #000;padding:4px 6px;text-align:left" colspan="2">Name</th>
<th style="border:1px solid #000;padding:4px 6px;text-align:left">Status</th>
<th style="border:1px solid #000;padding:4px 6px;text-align:left">Date</th>
<th style="border:1px solid #000;padding:4px 6px;text-align:left" colspan="2">Signature</th>
</tr>
<tbody id="p2_approval_rows"></tbody>
<!-- Authorised By -->
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Authorised By (Plant Manager)</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_auth_by"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Authorization Date</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_auth_dt"></td>
</tr>
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700;vertical-align:top">Comment and Status of Project</td>
<td style="border:1px solid #000;padding:4px 6px;min-height:40px" colspan="7" id="p2_comments"></td>
</tr>
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Date</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="7"></td>
</tr>
<!-- Verified by Finance -->
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Verified by Finance &amp; Accounts</td>
<td style="border:1px solid #000;padding:30px 6px 4px" colspan="3">
<div style="border-top:1px solid #000;padding-top:3px;font-size:8pt" id="p2_sig_finance"></div>
</td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Verified by Legal</td>
<td style="border:1px solid #000;padding:30px 6px 4px" colspan="3">
<div style="border-top:1px solid #000;padding-top:3px;font-size:8pt" id="p2_sig_legal"></div>
</td>
</tr>
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Finance Verified Date</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_fin_dt"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Legal Verified Date</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_legal_dt"></td>
</tr>
<!-- Project Leader / Owner sign -->
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Project Leader / Owner</td>
<td style="border:1px solid #000;padding:30px 6px 4px" colspan="3">
<div style="border-top:1px solid #000;padding-top:3px;font-size:8pt" id="p2_sig_owner"></div>
</td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Authorised By (Plant Manager)</td>
<td style="border:1px solid #000;padding:30px 6px 4px" colspan="3">
<div style="border-top:1px solid #000;padding-top:3px;font-size:8pt" id="p2_sig_plant"></div>
</td>
</tr>
<!-- Mrs. Babita / Finance & Mr. Jubin -->
<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Finance</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_finance_name"></td>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700">Legal</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="3" id="p2_legal_name"></td>
</tr>
</table>
<!-- Category A note -->
<div style="margin-top:6px;font-size:8pt;border:1px solid #000;padding:5px 8px">
<strong>Category A</strong> — Project Review by the Project Owner every 30-day intervals.
Report Submitted to MD's office at Every Interval of Time of 30 Days.
In Case any Problem or Issue arise, the time restriction does not apply.
</div>
</div>
</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(_){}
let currentProjectStatus=null;
const isFullAdmin=user?.role==='admin'||user?.role==='sap_adder';
const userDept=user?.approvalDept||null;
function canApproveStep(stepKey){return isFullAdmin||(user?.role==='project_approver'&&userDept===stepKey);}
const isAdmin=isFullAdmin; // reject button + other admin-only actions
const hdrs={'Authorization':'Bearer '+tok,'Content-Type':'application/json'};
// ── predefined team & approval steps (must be before routing) ─────────────
const PREDEFINED_TEAM=[
{func:'Users', defaultName:'', resp:'Project initiation post feasibility study (technical, operational, time, etc.) and Viability Study/expected benefits, ROI; Specs-URS; Technical Assessment of quote & BOQ; Visit to their factory & existing customers (in case of an unestablished supplier); Draft agreements; status follow-up with Purchase; FAT/In-process inspection (need based); verification of items & documentation; cross-functional communication; record keeping; and any other related task.'},
{func:'Purchase', defaultName:'Mr. GS Negi / Mr. Rahul', resp:'To acquire specification/URS and its clear understanding; Search & quality competent vendors (conduct thorough due diligence in case of unestablished vendors); Sharing of Specs/URs/BOQ and draft agreements with the identified vendors; Shortlisting of 03 vendors (where possible); URS & Agreements Acceptance; Arranging techno-commercial offer; Sharing technical details/BOQ with the User; commercials, terms & conditions finalization; Verification of draft agreements post vendor acceptance & internal clearance; cross-functional communication at various stages; record keeping; Release of PO & Agreements once finalized.'},
{func:'Engineering', defaultName:'Mr. Piyush / Ms. Piya', resp:'Review & approval of the related technical points in URs, quotes & agreements; Conduct required inspection & testing during the various phases of the project; indenting; cross-functional communication; PM plan; record keeping; and any other related task.'},
{func:'QA', defaultName:'Satish Shukla', resp:'Review & approval of the related technical points in URs; Qualification, validation & QMS documentation, if applicable; cross-functional communication; record keeping; and any other related task.'},
{func:'QC', defaultName:'Manoj Srivastav / Divya', resp:'Review & approval of the related technical points in URs; Qualification, validation & QMS documentation, if applicable; cross-functional communication; record keeping; and any other related task.'},
{func:'Finance', defaultName:'Mrs. Babita / Mrs. Rittu', resp:'Review of project ROI etc; commercial terms & conditions in quotes, agreements & PO; review & approval/rejection of invoices; release of funds if all is as per agreed terms; record keeping; and any other related task.'},
{func:'Legal', defaultName:'Mr. Jubin Ahuja', resp:'Review & approval of commercial terms & conditions in agreements & PO; communication with the vendor in case of dispute.'},
{func:'Project Leader/Project Owner', defaultName:'Mr. Piyush', resp:'To ensure feasibility & viability of the project; Monitoring at all stages/phases; ensuring timely execution complying with the stated requirements; required follow-up; arranging required approvals for the budget, deviations, etc.'},
];
// Sequential approval: Purchase → Engineering → QA → QC → Legal → Owner → Plant → Finance
const STEPS=[
{key:'purchase', from:'PENDING', to:'PURCHASE', label:'Purchase', person:'Mr. GS Negi / Mr. Rahul', dtField:'purchase_verified_dt'},
{key:'engineering', from:'PURCHASE', to:'ENGINEERING', label:'Engineering', person:'Mr. Piyush / Ms. Piya', dtField:'engineering_verified_dt'},
{key:'qa', from:'ENGINEERING', to:'QA_DONE', label:'QA', person:'Satish Shukla', dtField:'qa_verified_dt'},
{key:'qc', from:'QA_DONE', to:'QC_DONE', label:'QC', person:'Manoj Srivastav / Divya', dtField:'qc_verified_dt'},
{key:'legal', from:'QC_DONE', to:'LEGAL', label:'Legal', person:'Mr. Jubin Ahuja', dtField:'legal_verified_dt'},
{key:'owner', from:'LEGAL', to:'OWNER', label:'Project Owner', person:'Mr. Piyush', dtField:'owner_approved_dt'},
{key:'plant', from:'OWNER', to:'PLANT', label:'Plant Manager', person:'CV Singh', dtField:'authorised_dt', nameField:'authorised_by'},
{key:'finance', from:'PLANT', to:'APPROVED', label:'Finance & Accounts', person:'Mrs. Babita / Mrs. Rittu', dtField:'finance_verified_dt'},
];
const STATUS_ORDER=['DRAFT','PENDING','PURCHASE','ENGINEERING','QA_DONE','QC_DONE','LEGAL','OWNER','PLANT','APPROVED'];
// ── routing ──────────────────────────────────────────────────────────────────
const params=new URLSearchParams(location.search);
const projectId=params.get('id');
if(params.get('list')==='1'||(!projectId&&!params.has('new'))){showList();}
else{if(projectId)loadProject(projectId);else showForm();}
// ── list ─────────────────────────────────────────────────────────────────────
function showList(){
document.getElementById('viewList').style.display='block';
document.getElementById('viewForm').style.display='none';
loadList();
}
async function loadList(){
const status=document.getElementById('filterStatus')?.value||'';
const resp=await fetch('/api/projects?status='+status,{headers:hdrs});
const json=await resp.json();
const tbody=document.getElementById('listBody');
if(!json.success||!json.data.length){tbody.innerHTML='<tr><td colspan="10" class="empty">No projects found</td></tr>';return;}
tbody.innerHTML=json.data.map(p=>`
<tr onclick="openProject(${p.id})">
<td><b style="color:#7c3aed">${p.project_code||'&#8212;'}</b></td>
<td>${esc(p.project_name)}</td>
<td><span class="category-tag ${p.project_category==='A'?'cat-a':'cat-b'}">Cat ${p.project_category}</span></td>
<td>${esc(p.department_name||'')}</td><td>${esc(p.prepared_by_name||'')}</td>
<td style="text-align:right">&#8377; ${fmtNum(p.total_est_cost)}</td>
<td>${fmtDate(p.est_start_dt)}</td><td>${fmtDate(p.est_finish_dt)}</td>
<td>${statusBadge(p.status)}</td>
<td onclick="event.stopPropagation()" style="white-space:nowrap">
<button class="btn btn-outline btn-sm" onclick="openProject(${p.id})">View</button>
<button class="btn btn-info btn-sm" onclick="printProject(${p.id})">Print</button>
${p.status==='DRAFT'?`<button class="btn btn-danger btn-sm" onclick="delProject(${p.id},event)">Del</button>`:''}
</td>
</tr>`).join('');
}
window.loadList=loadList;
function openProject(id){window.location.href='/project-form?id='+id;}
window.openProject=openProject;
async function delProject(id,e){
e.stopPropagation();
if(!confirm('Delete this draft project?'))return;
const r=await fetch('/api/projects/'+id,{method:'DELETE',headers:hdrs});
const j=await r.json();
if(j.success){showToast('Deleted','success');loadList();}else showToast(j.message,'error');
}
window.delProject=delProject;
function printProject(id){window.open('/project-form?id='+id+'&print=1','_blank');}
window.printProject=printProject;
// ── form ──────────────────────────────────────────────────────────────────────
function showForm(data=null){
document.getElementById('viewList').style.display='none';
document.getElementById('viewForm').style.display='block';
if(!data)resetForm();
}
function resetForm(){
['project_code','project_name','total_days','initiation_dt','est_start_dt','est_finish_dt',
'purpose','roi','labour_cost','material_cost','consultancy_fees','promotional_fees','other_cost',
'req_new_project','req_upgradation','time_cost_recovery','remarks_p2','comments']
.forEach(id=>{const el=document.getElementById(id);if(el)el.value='';});
document.getElementById('type_new').checked=true;
selectCat('B');
renderPredefinedTeam();
document.getElementById('totalDisplay').innerHTML='&#8377; 0.00';
document.getElementById('total_est_cost').value='0';
document.getElementById('statusBar').style.display='none';
document.getElementById('approvalCard').style.display='none';
document.getElementById('btnPrint').style.display='none';
document.getElementById('btnReject').style.display='none';
document.getElementById('btnDraft').style.display='inline-flex';
document.getElementById('btnSubmit').style.display='inline-flex';
document.getElementById('formTitle').innerHTML='<span>New</span> Project Request';
document.getElementById('btnManualUpload').style.display='none';
document.getElementById('btnFinanceSave').style.display='none';
renderAttachments([]);
// Show next code preview
const pcEl=document.getElementById('project_code');
if(pcEl){pcEl.value='';pcEl.placeholder='Generating…';pcEl.style.color='#9ca3af';}
fetch('/api/projects/next-code',{headers:hdrs})
.then(r=>r.json()).then(j=>{if(j.success&&pcEl&&!currentProjectStatus){pcEl.value=j.code;}})
.catch(()=>{if(pcEl)pcEl.placeholder='Auto-generated';});
}
window.showForm=showForm;
async function loadProject(id){
const resp=await fetch('/api/projects/'+id,{headers:hdrs});
const json=await resp.json();
if(!json.success){showToast('Project not found','error');return;}
showForm();
fillForm(json.data);
if(params.get('print')==='1')setTimeout(()=>{populatePrint(json.data);window.print();},600);
}
function fillForm(d){
currentProjectStatus=d.status;
const set=(id,v)=>{const el=document.getElementById(id);if(el&&v!=null)el.value=v;};
set('project_code',d.project_code);
const pcEl=document.getElementById('project_code');if(pcEl)pcEl.style.color='';
set('project_name',d.project_name);set('total_days',d.total_days);
set('initiation_dt',fmtInput(d.initiation_dt));set('est_start_dt',fmtInput(d.est_start_dt));set('est_finish_dt',fmtInput(d.est_finish_dt));
set('purpose',d.purpose);set('roi',d.roi);
set('labour_cost',d.labour_cost);set('material_cost',d.material_cost);
set('consultancy_fees',d.consultancy_fees);set('promotional_fees',d.promotional_fees);set('other_cost',d.other_cost);
set('req_new_project',d.req_new_project);set('req_upgradation',d.req_upgradation);
set('time_cost_recovery',d.time_cost_recovery);set('remarks_p2',d.remarks_p2);
set('comments',d.comments);
const pt=d.project_type||'NEW';
const pto=document.querySelector('input[name="project_type"][value="'+pt+'"]');
if(pto)pto.checked=true;
selectCat(d.project_category||'B');
calcTotal();
renderPredefinedTeam(d.team_members||[]);
document.getElementById('statusBar').style.display='flex';
document.getElementById('codeDisplay').textContent=d.project_code||'';
document.getElementById('statusDisplay').innerHTML=statusBadge(d.status);
document.getElementById('submittedByDisplay').textContent=d.submitted_by||'';
document.getElementById('formTitle').innerHTML='<span>Project:</span> '+esc(d.project_name);
document.getElementById('btnPrint').style.display='inline-flex';
document.title=(d.project_code||'')+' — '+d.project_name;
// attachments — show manual upload button for saved projects
renderAttachments(Array.isArray(d.attachments)?d.attachments:[]);
document.getElementById('btnManualUpload').style.display='inline-flex';
const isDraft = d.status === 'DRAFT';
const isDone = d.status === 'APPROVED' || d.status === 'REJECTED';
if (!isDraft) {
// ── lock all form inputs (view-only mode) ──────────────────────────────
document.querySelectorAll(
'#viewForm input:not([type=hidden]):not([type=radio]):not([type=checkbox]), #viewForm select, #viewForm textarea'
).forEach(el => {
// keep approval-step controls interactive
if (el.closest('#approvalCard') || el.closest('.step-actions') || el.id === 'step_dt_inp' || el.id === 'step_name_inp') return;
el.setAttribute('readonly', true);
el.setAttribute('disabled', true);
el.style.background = '#f0f4f8';
el.style.cursor = 'default';
el.style.color = '#374151';
});
// hide draft/submit buttons
document.getElementById('btnDraft').style.display = 'none';
document.getElementById('btnSubmit').style.display = 'none';
// show approval panel
document.getElementById('approvalCard').style.display = 'block';
renderApprovalSteps(d);
// Finance department can edit while project awaits their approval
if (d.status === 'PLANT' && canApproveStep('finance')) {
document.querySelectorAll(
'#viewForm input:not([type=hidden]):not([type=radio]):not([type=checkbox]), #viewForm select, #viewForm textarea'
).forEach(el => {
if (el.closest('#approvalCard') || el.closest('.step-actions')) return;
el.removeAttribute('readonly');
el.removeAttribute('disabled');
el.style.background = '';
el.style.cursor = '';
el.style.color = '';
});
document.getElementById('btnFinanceSave').style.display = 'inline-flex';
}
}
if (!isDraft && !isDone && isAdmin) {
document.getElementById('btnReject').style.display = 'inline-flex';
}
// show saved comments
const cdEl = document.getElementById('commentsDisplay');
if (d.comments) {
cdEl.textContent = d.comments;
cdEl.style.display = 'block';
}
// show inline reject panel for admins on active projects
if (!isDone && isAdmin) {
document.getElementById('rejectSection').style.display = 'block';
}
}
// ── approval workflow ──────────────────────────────────────────────────────
// ── approval workflow ──────────────────────────────────────────────────────
function renderApprovalSteps(d){
const div=document.getElementById('approvalSteps');
div.innerHTML=STEPS.map((step,i)=>{
const toIdx=STATUS_ORDER.indexOf(step.to);
const si=STATUS_ORDER.indexOf(d.status);
const dtVal=d[step.dtField];
const nameVal=step.nameField?d[step.nameField]:step.person;
let state;
if(si>=toIdx){state=dtVal?'done':'skipped';}
else if(d.status===step.from){state='current';}
else{state='pending';}
if(state==='done') return `<div class="step-row step-done">
<div class="step-num step-num-done">&#10003;</div>
<div class="step-info"><div class="step-label">${esc(step.label)}</div><div class="step-person">${esc(nameVal||step.person)}</div></div>
<div class="step-date">&#10003; ${fmtDate(dtVal)}</div>
</div>`;
if(state==='skipped') return `<div class="step-row step-skipped">
<div class="step-num step-num-skip">N/A</div>
<div class="step-info"><div class="step-label">${esc(step.label)}</div><div class="step-person" style="color:#7c3aed;font-style:italic">Not Applicable &#8212; Skipped</div></div>
</div>`;
if(state==='current') return `<div class="step-row step-current">
<div class="step-num step-num-curr">${i+1}</div>
<div class="step-info">
<div class="step-label">${esc(step.label)} <span style="font-size:11px;background:#fef3c7;color:#92400e;padding:2px 8px;border-radius:6px;margin-left:6px">Awaiting</span></div>
<div class="step-person">${step.nameField?`<input id="step_name_inp" type="text" value="${esc(nameVal||'CV Singh')}" placeholder="Plant Manager name" style="border:1.5px solid #fbbf24;border-radius:7px;padding:5px 10px;font-size:12px;font-family:inherit;width:220px;outline:none">`:esc(step.person)}</div>
${canApproveStep(step.key)?`<div class="step-actions">
<input type="date" id="step_dt_inp">
<button class="btn btn-success btn-sm" onclick="doApprove('${step.key}',true)">&#10003; Approve</button>
<button class="btn btn-outline btn-sm" style="border-color:#7c3aed;color:#7c3aed" onclick="doApprove('${step.key}',false)">Skip / N/A</button>
</div>`:'<div style="margin-top:6px;font-size:11px;color:#92400e;background:#fef3c7;padding:3px 10px;border-radius:6px;display:inline-block">Pending approval from this department</div>'}
</div>
</div>`;
return `<div class="step-row step-pending">
<div class="step-num step-num-pend">${i+1}</div>
<div class="step-info"><div class="step-label">${esc(step.label)}</div><div class="step-person">${esc(step.person||'&#8212;')}</div></div>
<div style="font-size:11px;color:#9ca3af">Pending</div>
</div>`;
}).join('');
}
async function doApprove(stepKey,withDate){
const dt=withDate?document.getElementById('step_dt_inp')?.value:null;
if(withDate&&!dt){showToast('Please enter the approval date','error');return;}
const name=document.getElementById('step_name_inp')?.value||null;
const r=await fetch('/api/projects/'+params.get('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 as Not Applicable','success');setTimeout(()=>location.reload(),800);}
else showToast(j.message,'error');
}
window.doApprove=doApprove;
// inline reject (from approval card)
async function doRejectInline(){
const reason = document.getElementById('rejectReason')?.value?.trim();
if (!reason) { showToast('Please enter a rejection reason', 'error'); return; }
if (!confirm('Reject this project? This cannot be undone.')) return;
const r = await fetch('/api/projects/'+params.get('id'), {
method:'PATCH', headers:hdrs,
body: JSON.stringify({ status:'REJECTED', comments: reason })
});
const j = await r.json();
if (j.success) { showToast('Project rejected','info'); setTimeout(()=>location.reload(), 800); }
else showToast(j.message,'error');
}
window.doRejectInline = doRejectInline;
// keep old doReject for the header button (still used)
async function doReject(){
const reason = prompt('Reason for rejection:');
if (reason === null) return;
const r = await fetch('/api/projects/'+params.get('id'), {
method:'PATCH', headers:hdrs,
body: JSON.stringify({ status:'REJECTED', comments: reason })
});
const j = await r.json();
if (j.success) { showToast('Project rejected','info'); setTimeout(()=>location.reload(), 800); }
else showToast(j.message,'error');
}
window.doReject = doReject;
async function saveComments(){
const c = document.getElementById('commentNew')?.value?.trim() || null;
if (!c) { showToast('Comment cannot be empty','error'); return; }
// append to existing comments
const existing = document.getElementById('commentsDisplay')?.textContent || '';
const combined = existing ? existing + '\n---\n' + c : c;
const r = await fetch('/api/projects/'+params.get('id'), {
method:'PATCH', headers:hdrs,
body: JSON.stringify({ comments: combined })
});
const j = await r.json();
if (j.success) {
showToast('Comment saved','success');
setTimeout(()=>location.reload(), 800);
} else showToast(j.message,'error');
}
window.saveComments = saveComments;
// ── team ──────────────────────────────────────────────────────────────────────
function renderPredefinedTeam(existing=[]){
const tbody=document.getElementById('teamBody');
tbody.innerHTML='';
const byFunc={};const customRows=[];
existing.forEach(m=>{
if(PREDEFINED_TEAM.find(p=>p.func===m.function))byFunc[m.function]=m;
else customRows.push(m);
});
PREDEFINED_TEAM.forEach(pt=>{
const ex=byFunc[pt.func];
let applicable=ex?(ex.applicable!==false&&ex.name!=='NA'):true;
const name=ex?ex.name:(applicable?pt.defaultName:'NA');
const resp=ex?(ex.responsibilities||pt.resp):pt.resp;
const tr=document.createElement('tr');
tr.dataset.predefined='true';tr.dataset.func=pt.func;
tr.innerHTML=`<td><select class="team-applicable ${applicable?'ok':'na'}" onchange="toggleTeamRow(this)">
<option value="Y"${applicable?' selected':''}>Applicable</option>
<option value="N"${!applicable?' selected':''}>Not Applicable</option>
</select></td>
<td><input class="team-name-inp" type="text" value="${esc(applicable?name:'NA')}"${!applicable?' disabled':''} placeholder="Name&#8230;"></td>
<td class="team-func-cell">${esc(pt.func)}</td>
<td><textarea class="team-resp-ta" rows="2" readonly>${esc(resp||'')}</textarea></td>
<td class="del-cell no-print"></td>`;
tbody.appendChild(tr);
});
customRows.forEach(m=>addCustomTeamRow(m));
}
function toggleTeamRow(sel){
const tr=sel.closest('tr');
const nameEl=tr.querySelector('.team-name-inp');
const applicable=sel.value==='Y';
sel.className='team-applicable '+(applicable?'ok':'na');
if(applicable){nameEl.disabled=false;if(nameEl.value==='NA'){const pt=PREDEFINED_TEAM.find(p=>p.func===tr.dataset.func);nameEl.value=pt?.defaultName||'';}}
else{nameEl.value='NA';nameEl.disabled=true;}
}
window.toggleTeamRow=toggleTeamRow;
function addCustomTeamRow(data={}){
const applicable=data.applicable!==false;
const tr=document.createElement('tr');
tr.innerHTML=`<td><select class="team-applicable ${applicable?'ok':'na'}" onchange="toggleTeamRow(this)">
<option value="Y"${applicable?' selected':''}>Applicable</option>
<option value="N"${!applicable?' selected':''}>Not Applicable</option>
</select></td>
<td><input class="team-name-inp" type="text" value="${esc(applicable?(data.name||''):'NA')}"${!applicable?' disabled':''} placeholder="Name&#8230;"></td>
<td><input type="text" style="width:100%;border:1.5px solid #e2e8f0;border-radius:6px;padding:6px 10px;font-size:12px;font-family:inherit;outline:none" value="${esc(data.function||'')}" placeholder="Function&#8230;"></td>
<td><textarea class="team-resp-ta" rows="2" placeholder="Responsibilities&#8230;" readonly>${esc(data.responsibilities||'')}</textarea></td>
<td class="del-cell no-print"><button class="btn btn-danger btn-sm" onclick="this.closest('tr').remove()" style="padding:0 8px">&#x2715;</button></td>`;
document.getElementById('teamBody').appendChild(tr);
}
window.addCustomTeamRow=addCustomTeamRow;
function getTeam(){
return[...document.querySelectorAll('#teamBody tr')].map(tr=>{
const sel=tr.querySelector('.team-applicable');
const applicable=!sel||sel.value==='Y';
const nameEl=tr.querySelector('.team-name-inp');
const funcInput=tr.querySelector('td:nth-child(3) input');
const funcCell=tr.querySelector('.team-func-cell');
const respEl=tr.querySelector('.team-resp-ta');
return{applicable,name:applicable?(nameEl?.value||''):'NA',function:funcInput?.value||funcCell?.textContent?.trim()||'',responsibilities:respEl?.value||''};
}).filter(m=>m.function);
}
// ── attachments ───────────────────────────────────────────────────────────────
function renderAttachments(files){
const list=document.getElementById('attList');
if(!files||!files.length){list.innerHTML='<div style="font-size:12px;color:#9ca3af;padding:6px 0">No attachments yet.</div>';return;}
list.innerHTML=files.map(f=>`<div class="att-item">
<a href="${esc(f.url)}" target="_blank">${esc(f.originalName||f.name)}</a>
<span class="att-size">${fmtBytes(f.size)}</span>
${params.get('id')?`<button class="btn btn-danger btn-sm no-print" onclick="deleteAttachment('${esc(f.name)}')" style="padding:0 8px">&#x2715;</button>`:''}
</div>`).join('');
}
async function uploadAttachments(){
const id=params.get('id');
if(!id){showToast('Save the form first (it will auto-upload)','info');return;}
const input=document.getElementById('attFileInput');
if(!input.files.length){showToast('Select at least one file','error');return;}
const fd=new FormData();
for(const f of input.files)fd.append('files',f);
const r=await fetch('/api/projects/'+id+'/upload',{method:'POST',headers:{'Authorization':'Bearer '+tok},body:fd});
const j=await r.json();
if(j.success){
input.value='';
const pr=await fetch('/api/projects/'+id,{headers:hdrs});
const pj=await pr.json();
if(pj.success)renderAttachments(pj.data.attachments||[]);
showToast('Uploaded!','success');
}else showToast(j.message,'error');
}
window.uploadAttachments=uploadAttachments;
async function deleteAttachment(filename){
if(!confirm('Remove this attachment?'))return;
const id=params.get('id');
const r=await fetch('/api/projects/'+id+'/attachments/'+encodeURIComponent(filename),{method:'DELETE',headers:hdrs});
const j=await r.json();
if(j.success){
const pr=await fetch('/api/projects/'+id,{headers:hdrs});
const pj=await pr.json();
if(pj.success)renderAttachments(pj.data.attachments||[]);
showToast('Removed','info');
}else showToast(j.message,'error');
}
window.deleteAttachment=deleteAttachment;
// ── category & cost ───────────────────────────────────────────────────────────
function selectCat(c){
document.getElementById('catA').classList.toggle('selected',c==='A');
document.getElementById('catB').classList.toggle('selected',c==='B');
document.getElementById('project_category').value=c;
}
window.selectCat=selectCat;
function calcTotal(){
const total=['labour_cost','material_cost','consultancy_fees','promotional_fees','other_cost']
.reduce((s,id)=>s+(parseFloat(document.getElementById(id)?.value)||0),0);
document.getElementById('totalDisplay').innerHTML='&#8377; '+fmtNum(total);
document.getElementById('total_est_cost').value=total.toFixed(2);
}
window.calcTotal=calcTotal;
// ── submit confirmation ───────────────────────────────────────────────────────
function confirmSubmit(){
// Validate mandatory fields before showing modal
const errs=[];
if(!document.getElementById('project_name').value.trim()) errs.push('Project Name is required');
if(!document.getElementById('purpose').value.trim()) errs.push('Purpose is required');
if(!document.getElementById('initiation_dt').value) errs.push('Initiation Date is required');
if(!document.getElementById('est_start_dt').value) errs.push('Est. Start Date is required');
if(!document.getElementById('est_finish_dt').value) errs.push('Est. Finish Date is required');
// Users row must have a name filled
const usersRow=[...document.querySelectorAll('#teamBody tr')].find(tr=>tr.dataset.func==='Users');
if(usersRow&&usersRow.querySelector('.team-applicable')?.value==='Y'&&!usersRow.querySelector('.team-name-inp')?.value?.trim())
errs.push('Please enter your name in the Project Team (Users row)');
// Must have at least one attachment (already uploaded or selected)
const hasUploaded=!!document.getElementById('attList').querySelector('.att-item');
const hasSelected=document.getElementById('attFileInput').files.length>0;
if(!hasUploaded&&!hasSelected) errs.push('At least one attachment is required');
if(errs.length){showToast(errs[0],'error');return;}
// Show modal with approval chain
const stepsHtml=STEPS.map((s,i)=>`<div class="modal-step">
<div class="modal-step-num">${i+1}</div>
<div><span class="modal-step-label">${esc(s.label)}</span><span class="modal-step-person">&#8212; ${esc(s.person)}</span></div>
</div>`).join('');
document.getElementById('submitModalSteps').innerHTML=stepsHtml;
document.getElementById('submitModal').style.display='flex';
}
window.confirmSubmit=confirmSubmit;
async function doSubmit(){
document.getElementById('submitModal').style.display='none';
await saveProject('PENDING');
}
window.doSubmit=doSubmit;
// ── save ──────────────────────────────────────────────────────────────────────
function buildApplicableFlags(){
// Map team member N/A status to approval applicable flags so stepApprove can auto-skip
const byFunc={};
getTeam().forEach(m=>{byFunc[m.function]=m;});
const isApplicable=(func)=>{
const m=byFunc[func];
return m?m.applicable!==false:true;
};
return{
qa_applicable: isApplicable('QA')?1:0,
qc_applicable: isApplicable('QC')?1:0,
legal_applicable: isApplicable('Legal')?1:0,
finance_applicable: isApplicable('Finance')?1:0,
owner_applicable: isApplicable('Project Leader/Project Owner')?1:0,
};
}
async function saveProject(status){
const name=document.getElementById('project_name').value.trim();
if(!name){showToast('Project name is required','error');return;}
const body={
project_name:name,
project_category:document.getElementById('project_category').value,
total_days:parseInt(document.getElementById('total_days').value)||null,
initiation_dt:document.getElementById('initiation_dt').value||null,
est_start_dt:document.getElementById('est_start_dt').value||null,
est_finish_dt:document.getElementById('est_finish_dt').value||null,
purpose:document.getElementById('purpose').value||null,
roi:document.getElementById('roi').value||null,
labour_cost:parseFloat(document.getElementById('labour_cost').value)||0,
material_cost:parseFloat(document.getElementById('material_cost').value)||0,
consultancy_fees:parseFloat(document.getElementById('consultancy_fees').value)||0,
promotional_fees:parseFloat(document.getElementById('promotional_fees').value)||0,
other_cost:parseFloat(document.getElementById('other_cost').value)||0,
total_est_cost:parseFloat(document.getElementById('total_est_cost').value)||0,
department_name:document.getElementById('department_name').value||null,
prepared_by_name:document.getElementById('prepared_by_name').value||null,
prepared_by_desig:document.getElementById('prepared_by_desig').value||null,
accountable_person:document.getElementById('accountable_person').value||null,
project_type:document.querySelector('input[name="project_type"]:checked')?.value||'NEW',
req_new_project:document.getElementById('req_new_project').value||null,
req_upgradation:document.getElementById('req_upgradation').value||null,
time_cost_recovery:document.getElementById('time_cost_recovery').value||null,
remarks_p2:document.getElementById('remarks_p2').value||null,
team_members:getTeam(),status,
// sync team N/A flags so stepApprove can auto-skip
...buildApplicableFlags(),
};
const id=params.get('id');
const resp=await fetch(id?'/api/projects/'+id:'/api/projects',{method:id?'PATCH':'POST',headers:hdrs,body:JSON.stringify(body)});
const json=await resp.json();
if(!json.success){showToast(json.message||'Error saving','error');return;}
const pid=id||String(json.id);
// Auto-upload any files selected in the attachment input
const fileInput=document.getElementById('attFileInput');
if(fileInput&&fileInput.files.length&&pid){
const fd=new FormData();
for(const f of fileInput.files)fd.append('files',f);
const ur=await fetch('/api/projects/'+pid+'/upload',{method:'POST',headers:{'Authorization':'Bearer '+tok},body:fd});
const uj=await ur.json();
if(!uj.success)showToast('Saved but file upload failed: '+uj.message,'error');
fileInput.value='';
}
showToast(id?'Project updated!':'Project created!','success');
setTimeout(()=>{window.location.href='/project-form?id='+pid;},700);
}
window.saveProject=saveProject;
async function saveFinanceEdit(){
await saveProject(currentProjectStatus||'PLANT');
}
window.saveFinanceEdit=saveFinanceEdit;
function goList(){window.location.href='/project-form?list=1';}
window.goList=goList;
// ── print (approval sheet only) ───────────────────────────────────────────────
function populatePrint(d){
const s=(id,v)=>{const el=document.getElementById(id);if(el)el.textContent=v??'';};
const f=v=>fmtNum(v||0);
// Page 1
s('p1_code', d.project_code);
s('p1_days', d.total_days||'');
s('p1_init', fmtDate(d.initiation_dt));
s('p1_start', fmtDate(d.est_start_dt));
s('p1_end', fmtDate(d.est_finish_dt));
s('p1_name', d.project_name);
s('p1_cat', d.project_category==='A'?'A: Heavy Investment Project Greater than 2.5 Lakh':'B: Regular Routine Low Cost Project Less than 2.5 Lakh');
s('p1_purpose', d.purpose||'—');
s('p1_roi', d.roi||'—');
s('p1_labour', '₹ '+f(d.labour_cost));
s('p1_material','₹ '+f(d.material_cost));
s('p1_mat_lab', '₹ '+fmtNum((parseFloat(d.labour_cost)||0)+(parseFloat(d.material_cost)||0)));
const other=(parseFloat(d.consultancy_fees)||0)+(parseFloat(d.promotional_fees)||0)+(parseFloat(d.other_cost)||0);
s('p1_othercost','₹ '+fmtNum(other));
s('p1_total', '₹ '+f(d.total_est_cost));
s('p1_acct', d.accountable_person||'—');
const pt=d.project_type||'NEW';
s('p1_ptype', pt==='UPGRADATION'?'☑ Upgradation ☐ New Project':'☐ Upgradation ☑ New Project');
s('p1_req_new', d.req_new_project||'NA');
s('p1_req_upg', d.req_upgradation||'NA');
// Team rows
// Team rows — merge saved data with PREDEFINED_TEAM to ensure all rows appear
const savedTeam = Array.isArray(d.team_members) ? d.team_members : [];
const savedByFunc = {};
savedTeam.forEach(m => { savedByFunc[m.function] = m; });
// Build full team list: predefined order first, then any custom rows
const fullTeam = PREDEFINED_TEAM.map(pt => {
const saved = savedByFunc[pt.func];
if (saved) return saved;
// not saved yet — use predefined defaults
return { function: pt.func, name: pt.defaultName, responsibilities: pt.resp, applicable: true };
});
// append any custom rows not in predefined
savedTeam.forEach(m => {
if (!PREDEFINED_TEAM.find(pt => pt.func === m.function)) fullTeam.push(m);
});
document.getElementById('p1_team_rows').innerHTML = fullTeam.map(m => {
const isNA = m.applicable === false || m.name === 'NA';
return `<tr>
<td style="border:1px solid #000;padding:4px 6px;vertical-align:top">${esc(isNA ? 'NA' : (m.name||''))}${isNA ? '<br><em style="font-size:8pt;color:#aaa">N/A</em>' : ''}</td>
<td style="border:1px solid #000;padding:4px 6px;vertical-align:top;white-space:nowrap">${esc(m.function||'')}</td>
<td style="border:1px solid #000;padding:4px 6px;font-size:8pt;vertical-align:top">${esc(m.responsibilities||'')}</td>
</tr>`;
}).join('');
// Page 2
s('p2_prepname', d.prepared_by_name||'Mr. Piyush Sharma');
s('p2_prepdesig', d.prepared_by_desig||'Sr. Engineer');
s('p2_dept', d.department_name||'MAINTENANCE');
s('p2_dept2', d.department_name||'MAINTENANCE');
s('p2_fullname', d.prepared_by_name||'Mr. Piyush Sharma');
s('p2_desig', d.prepared_by_desig||'Sr. Engineer');
s('p2_req_new', d.req_new_project||'NA');
s('p2_req_upg', d.req_upgradation||'NA');
s('p2_tcr', d.time_cost_recovery||'NA');
s('p2_remarks', d.remarks_p2||'NA');
s('p2_auth_by', d.authorised_by||'—');
s('p2_auth_dt', d.authorised_dt?fmtDate(d.authorised_dt):'—');
s('p2_comments', d.comments||'—');
// get names from saved team members if available
const teamArr=Array.isArray(d.team_members)?d.team_members:[];
const teamName=(func)=>{const m=teamArr.find(t=>t.function===func);return(m&&m.name&&m.name!=='NA')?m.name:null;};
s('p2_sig_finance', teamName('Finance')||'Mrs. Babita / Mrs. Rittu');
s('p2_sig_legal', teamName('Legal')||'Mr. Jubin Ahuja');
s('p2_sig_owner', teamName('Project Leader/Project Owner')||d.accountable_person||'Mr. Piyush');
s('p2_sig_plant', d.authorised_by||'CV Singh');
s('p2_finance_name', teamName('Finance')||'Mrs. Babita / Mrs. Rittu');
s('p2_legal_name', teamName('Legal')||'Mr. Jubin Ahuja');
// Finance/Legal dates from approval steps
s('p2_fin_dt', d.finance_verified_dt?fmtDate(d.finance_verified_dt):'dd-mm-yyyy');
s('p2_legal_dt', d.legal_verified_dt?fmtDate(d.legal_verified_dt):'dd-mm-yyyy');
// Approval rows
const si=STATUS_ORDER.indexOf(d.status);
const STEPS_LOCAL=[
{key:'purchase',label:'Purchase',person:'Mr. GS Negi / Mr. Rahul',dtField:'purchase_verified_dt'},
{key:'engineering',label:'Engineering',person:'Mr. Piyush / Ms. Piya',dtField:'engineering_verified_dt'},
{key:'qa',label:'QA',person:'Satish Shukla',dtField:'qa_verified_dt'},
{key:'qc',label:'QC',person:'Manoj Srivastav / Divya',dtField:'qc_verified_dt'},
{key:'legal',label:'Legal',person:'Mr. Jubin Ahuja',dtField:'legal_verified_dt'},
{key:'owner',label:'Project Owner',person:'Mr. Piyush',dtField:'owner_approved_dt'},
{key:'plant',label:'Plant Manager',person:'CV Singh',dtField:'authorised_dt'},
{key:'finance',label:'Finance & Accounts',person:'Mrs. Babita / Mrs. Rittu',dtField:'finance_verified_dt'},
];
document.getElementById('p2_approval_rows').innerHTML=STEPS_LOCAL.map(step=>{
const toIdx=STATUS_ORDER.indexOf(STEPS.find(s=>s.key===step.key)?.to||'');
const dtVal=d[step.dtField];
const isDone=si>=toIdx&&toIdx>=0;
const isSkipped=isDone&&!dtVal;
const statusTxt=isSkipped?'Not Applicable':isDone?'Approved ✓':'Pending';
const nameVal=(step.key==='plant'&&d.authorised_by)?d.authorised_by:step.person;
return`<tr>
<td style="border:1px solid #000;padding:4px 6px;font-weight:700" colspan="2">${esc(step.label)}</td>
<td style="border:1px solid #000;padding:4px 6px" colspan="2">${esc(nameVal)}</td>
<td style="border:1px solid #000;padding:4px 6px;color:${isSkipped?'#7c3aed':isDone?'#059669':'#999'};font-weight:700">${statusTxt}</td>
<td style="border:1px solid #000;padding:4px 6px">${isDone&&dtVal?fmtDate(dtVal):'—'}</td>
<td style="border:1px solid #000;padding:20px 6px 4px" colspan="2"></td>
</tr>`;
}).join('');
}
function printForm(){
const id=params.get('id');
if(!id){showToast('Save the project first','error');return;}
fetch('/api/projects/'+id,{headers:hdrs}).then(r=>r.json()).then(j=>{if(j.success){populatePrint(j.data);setTimeout(()=>window.print(),300);}});
}
window.printForm=printForm;
// ── helpers ───────────────────────────────────────────────────────────────────
function statusBadge(s){
const m={DRAFT:'s-draft',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',APPROVED:'s-approved',REJECTED:'s-rejected'};
const lbl={DRAFT:'DRAFT',PENDING:'Pending — Purchase',PURCHASE:'Purchase ✓ — Engineering',ENGINEERING:'Engg ✓ — QA',QA_DONE:'QA ✓ — QC',QC_DONE:'QC ✓ — Legal',LEGAL:'Legal ✓ — Owner',OWNER:'Owner ✓ — Plant Mgr',PLANT:'Plant ✓ — Finance',APPROVED:'APPROVED',REJECTED:'REJECTED'};
return`<span class="status-badge ${m[s]||'s-draft'}">${lbl[s]||s||'DRAFT'}</span>`;
}
function fmtDate(d){if(!d)return'—';try{return new Date(d).toLocaleDateString('en-IN',{day:'2-digit',month:'short',year:'numeric'});}catch{return String(d);}}
function fmtInput(d){if(!d)return'';try{const dt=new Date(d);if(isNaN(dt))return'';return dt.toISOString().slice(0,10);}catch{return'';}}
function fmtNum(v,dec=2){return Number(v||0).toLocaleString('en-IN',{minimumFractionDigits:dec,maximumFractionDigits:dec});}
function fmtBytes(b){if(!b)return'';if(b<1024)return b+'B';if(b<1048576)return(b/1024).toFixed(1)+'KB';return(b/1048576).toFixed(1)+'MB';}
function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
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>