|
|
|
@ -34,6 +34,9 @@ |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script> |
|
|
|
|
|
|
|
<!-- SheetJS (Excel Export) --> |
|
|
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script> |
|
|
|
|
|
|
|
<!-- Styles --> |
|
|
|
<style> |
|
|
|
body { |
|
|
|
@ -552,7 +555,7 @@ |
|
|
|
<div class="section-header"> |
|
|
|
<h1 class="section-title"> |
|
|
|
<i class="fa-solid fa-chart-pie"></i> |
|
|
|
Toner Inventory Dashboard |
|
|
|
Toner Inventory Dashboard & Reports |
|
|
|
</h1> |
|
|
|
</div> |
|
|
|
|
|
|
|
@ -581,26 +584,96 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Inventory Balance Table --> |
|
|
|
<div class="card-custom"> |
|
|
|
<h5 class="fw-bold mb-4" style="color: #0f5c78;"> |
|
|
|
<i class="fa-solid fa-warehouse me-2"></i>Current Stock Summary |
|
|
|
<!-- Interactive Report Filter Controls --> |
|
|
|
<div class="card-custom mb-4"> |
|
|
|
<h5 class="fw-bold mb-3" style="color: #0f5c78;"> |
|
|
|
<i class="fa-solid fa-filter me-2"></i>Report Filters & Options |
|
|
|
</h5> |
|
|
|
<div class="row g-3 align-items-end"> |
|
|
|
<!-- Select Report Type --> |
|
|
|
<div class="col-md-3"> |
|
|
|
<label class="form-label" for="report-type-select"> |
|
|
|
<i class="fa-solid fa-table-list text-primary me-1"></i> Select Report Type |
|
|
|
</label> |
|
|
|
<select id="report-type-select" class="form-control-custom" onchange="onReportTypeChange()"> |
|
|
|
<option value="user_requests">User Request Details Report</option> |
|
|
|
<option value="inward_stock">Inward Register Stock Report</option> |
|
|
|
<option value="stock_summary">Current Stock Summary</option> |
|
|
|
<option value="outward_history">Outward Distribution History</option> |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Printer Model Filter --> |
|
|
|
<div class="col-md-3"> |
|
|
|
<label class="form-label" for="report-printer-filter"> |
|
|
|
<i class="fa-solid fa-print text-primary me-1"></i> Printer Model |
|
|
|
</label> |
|
|
|
<select id="report-printer-filter" class="form-control-custom" |
|
|
|
onchange="onReportPrinterFilterChange()"> |
|
|
|
<option value="">-- All Printer Models --</option> |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Toner Model Filter --> |
|
|
|
<div class="col-md-2"> |
|
|
|
<label class="form-label" for="report-toner-filter"> |
|
|
|
<i class="fa-solid fa-droplet text-primary me-1"></i> Toner Model |
|
|
|
</label> |
|
|
|
<select id="report-toner-filter" class="form-control-custom" onchange="applyReportFilters()" |
|
|
|
disabled> |
|
|
|
<option value="">-- All Toners --</option> |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Status Filter --> |
|
|
|
<div class="col-md-2" id="report-status-filter-wrapper"> |
|
|
|
<label class="form-label" for="report-status-filter"> |
|
|
|
<i class="fa-solid fa-clock-rotate-left text-primary me-1"></i> Status |
|
|
|
</label> |
|
|
|
<select id="report-status-filter" class="form-control-custom" |
|
|
|
onchange="applyReportFilters()"> |
|
|
|
<option value="">-- All Statuses --</option> |
|
|
|
<option value="Pending">Pending</option> |
|
|
|
<option value="Delivered">Delivered</option> |
|
|
|
<option value="Partial">Partial</option> |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Search Filter --> |
|
|
|
<div class="col-md-2"> |
|
|
|
<label class="form-label" for="report-search-input"> |
|
|
|
<i class="fa-solid fa-magnifying-glass text-primary me-1"></i> Search |
|
|
|
</label> |
|
|
|
<input type="text" id="report-search-input" class="form-control-custom" |
|
|
|
placeholder="Search..." oninput="applyReportFilters()"> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Single Dynamic Datatable Below --> |
|
|
|
<div class="card-custom" id="dynamic-report-card"> |
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2"> |
|
|
|
<h5 class="fw-bold m-0" style="color: #0f5c78;" id="report-dynamic-title"> |
|
|
|
<i class="fa-solid fa-file-invoice me-2"></i>User Request Details Report |
|
|
|
</h5> |
|
|
|
<div class="d-flex align-items-center gap-2"> |
|
|
|
<span class="badge bg-primary p-2" id="report-count-badge" style="font-size: 0.85rem;">0 |
|
|
|
Records</span> |
|
|
|
<button type="button" |
|
|
|
class="btn btn-sm btn-success fw-bold px-3 py-2 d-inline-flex align-items-center gap-1 shadow-sm" |
|
|
|
onclick="exportReportToExcel()"> |
|
|
|
<i class="fa-solid fa-file-excel"></i> Export Excel |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="table-responsive"> |
|
|
|
<table class="table grid-table align-middle"> |
|
|
|
<thead> |
|
|
|
<tr> |
|
|
|
<th>Sr. No.</th> |
|
|
|
<th>Printer Model</th> |
|
|
|
<th>Toner Model</th> |
|
|
|
<th class="text-center">Total Inward</th> |
|
|
|
<th class="text-center">Total Outward</th> |
|
|
|
<th class="text-center">Available Stock</th> |
|
|
|
</tr> |
|
|
|
<thead id="report-dynamic-thead"> |
|
|
|
<!-- Dynamic headers --> |
|
|
|
</thead> |
|
|
|
<tbody id="report-table-body"> |
|
|
|
<tbody id="report-dynamic-tbody"> |
|
|
|
<tr> |
|
|
|
<td colspan="6" class="text-center text-muted py-4">Loading report details...</td> |
|
|
|
<td colspan="11" class="text-center text-muted py-4">Loading report data...</td> |
|
|
|
</tr> |
|
|
|
</tbody> |
|
|
|
</table> |
|
|
|
@ -608,6 +681,7 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<!-- ================= INWARD REGISTER PANEL ================= --> |
|
|
|
<div id="panel-inward" class="view-panel"> |
|
|
|
<div class="section-header"> |
|
|
|
@ -687,8 +761,37 @@ |
|
|
|
</button> |
|
|
|
</form> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Inward Register Details Table --> |
|
|
|
<div class="card-custom"> |
|
|
|
<h5 class="fw-bold mb-4" style="color: #0f5c78;"> |
|
|
|
<i class="fa-solid fa-boxes-packing me-2"></i>Inward Stock Register Details |
|
|
|
</h5> |
|
|
|
<div class="table-responsive"> |
|
|
|
<table class="table grid-table align-middle"> |
|
|
|
<thead> |
|
|
|
<tr> |
|
|
|
<th>Sr. No.</th> |
|
|
|
<th>Company Name</th> |
|
|
|
<th>Printer Model</th> |
|
|
|
<th>Toner Model</th> |
|
|
|
<th class="text-center">Qty Inward</th> |
|
|
|
<th>Created Date</th> |
|
|
|
<th>Entered By</th> |
|
|
|
<th class="text-center">DC Document</th> |
|
|
|
</tr> |
|
|
|
</thead> |
|
|
|
<tbody id="inward-history-table-body"> |
|
|
|
<tr> |
|
|
|
<td colspan="8" class="text-center text-muted py-4">No inward records found.</td> |
|
|
|
</tr> |
|
|
|
</tbody> |
|
|
|
</table> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<!-- ================= USER REQUEST PANEL ================= --> |
|
|
|
<div id="panel-request" class="view-panel"> |
|
|
|
<div class="section-header"> |
|
|
|
@ -778,6 +881,37 @@ |
|
|
|
</button> |
|
|
|
</form> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- User Request Details History Table --> |
|
|
|
<div class="card-custom"> |
|
|
|
<h5 class="fw-bold mb-4" style="color: #0f5c78;"> |
|
|
|
<i class="fa-solid fa-list-check me-2"></i>User Request Details & History |
|
|
|
</h5> |
|
|
|
<div class="table-responsive"> |
|
|
|
<table class="table grid-table align-middle"> |
|
|
|
<thead> |
|
|
|
<tr> |
|
|
|
<th>S.No</th> |
|
|
|
<th>Req. No</th> |
|
|
|
<th>IT Cell Ref No</th> |
|
|
|
<th>Department</th> |
|
|
|
<th>Printer Model</th> |
|
|
|
<th>Toner Model</th> |
|
|
|
<th class="text-center">Qty Req</th> |
|
|
|
<th class="text-center">Qty Delivered</th> |
|
|
|
<th>Created Date</th> |
|
|
|
<th class="text-center">Status</th> |
|
|
|
<th class="text-center">Request Doc</th> |
|
|
|
</tr> |
|
|
|
</thead> |
|
|
|
<tbody id="user-requests-table-body"> |
|
|
|
<tr> |
|
|
|
<td colspan="11" class="text-center text-muted py-4">No request records found.</td> |
|
|
|
</tr> |
|
|
|
</tbody> |
|
|
|
</table> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ================= OUTWARD REGISTER PANEL ================= --> |
|
|
|
@ -872,22 +1006,25 @@ |
|
|
|
<table class="table grid-table align-middle"> |
|
|
|
<thead> |
|
|
|
<tr> |
|
|
|
<th>S.No</th> |
|
|
|
<th>Req. No</th> |
|
|
|
<th>Department</th> |
|
|
|
<th>Printer Model</th> |
|
|
|
<th>Toner Model</th> |
|
|
|
<th class="text-center">Qty Req</th> |
|
|
|
<th class="text-center">Qty Issued</th> |
|
|
|
<th>Requested Date</th> |
|
|
|
<th>Issued Date</th> |
|
|
|
<th class="text-center">Action</th> |
|
|
|
</tr> |
|
|
|
</thead> |
|
|
|
<tbody id="outward-history-table-body"> |
|
|
|
<tr> |
|
|
|
<td colspan="8" class="text-center text-muted py-4">No outward records found.</td> |
|
|
|
<td colspan="10" class="text-center text-muted py-4">No outward records found.</td> |
|
|
|
</tr> |
|
|
|
</tbody> |
|
|
|
</table> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -902,10 +1039,13 @@ |
|
|
|
<script th:inline="javascript"> |
|
|
|
const contextPath = /*[[@{/}]]*/ ''; |
|
|
|
|
|
|
|
// Cache printers list |
|
|
|
// Caching datasets |
|
|
|
let printersCached = []; |
|
|
|
// Cache pending requests in Outward Register |
|
|
|
let pendingRequestsCached = []; |
|
|
|
let userRequestsCached = []; |
|
|
|
let inwardHistoryCached = []; |
|
|
|
let outwardHistoryCached = []; |
|
|
|
let stockSummaryCached = []; |
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
// Load base list of printers on startup |
|
|
|
@ -931,9 +1071,11 @@ |
|
|
|
} else if (panelName === 'inward') { |
|
|
|
resetForm('inward'); |
|
|
|
populatePrintersDropdown('inward'); |
|
|
|
fetchInwardHistory(); |
|
|
|
} else if (panelName === 'request') { |
|
|
|
resetForm('request'); |
|
|
|
populatePrintersDropdown('request'); |
|
|
|
fetchUserRequests(); |
|
|
|
} else if (panelName === 'outward') { |
|
|
|
resetForm('outward'); |
|
|
|
fetchPendingRequests(); |
|
|
|
@ -947,6 +1089,7 @@ |
|
|
|
.then(res => res.json()) |
|
|
|
.then(data => { |
|
|
|
printersCached = data; |
|
|
|
populateReportPrinterFilters(); |
|
|
|
}) |
|
|
|
.catch(err => console.error("Error fetching printers: ", err)); |
|
|
|
} |
|
|
|
@ -963,6 +1106,18 @@ |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function populateReportPrinterFilters() { |
|
|
|
const select = document.getElementById('report-printer-filter'); |
|
|
|
if (!select) return; |
|
|
|
select.innerHTML = '<option value="">-- All Printer Models --</option>'; |
|
|
|
printersCached.forEach(p => { |
|
|
|
const opt = document.createElement('option'); |
|
|
|
opt.value = p.id; |
|
|
|
opt.textContent = p.printer_name; |
|
|
|
select.appendChild(opt); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function loadToners(prefix) { |
|
|
|
const pid = document.getElementById(`${prefix}-printer-select`).value; |
|
|
|
const tonerSelect = document.getElementById(`${prefix}-toner-select`); |
|
|
|
@ -1071,7 +1226,7 @@ |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ================= REPORT VIEW API ================= |
|
|
|
// ================= REPORT VIEW API & FILTER LOGIC ================= |
|
|
|
function loadReportPage() { |
|
|
|
fetch(contextPath + 'gcctonermanagement/api/report') |
|
|
|
.then(res => res.json()) |
|
|
|
@ -1081,35 +1236,484 @@ |
|
|
|
document.getElementById('stats-raised').textContent = data.metrics.totalRaised || 0; |
|
|
|
document.getElementById('stats-distributed').textContent = data.metrics.totalDistributed || 0; |
|
|
|
document.getElementById('stats-stock').textContent = data.metrics.availableStock || 0; |
|
|
|
stockSummaryCached = data.summary || []; |
|
|
|
applyReportFilters(); |
|
|
|
} |
|
|
|
}) |
|
|
|
.catch(err => console.error("Error loading report page:", err)); |
|
|
|
|
|
|
|
// Populate inventory summary table |
|
|
|
const tbody = document.getElementById('report-table-body'); |
|
|
|
tbody.innerHTML = ''; |
|
|
|
if (data.summary.length === 0) { |
|
|
|
tbody.innerHTML = `<tr><td colspan="6" class="text-center text-muted py-4">No toner records found.</td></tr>`; |
|
|
|
return; |
|
|
|
} |
|
|
|
// Fetch datasets for dynamic filtering |
|
|
|
fetch(contextPath + 'gcctonermanagement/api/requests/list') |
|
|
|
.then(res => res.json()) |
|
|
|
.then(data => { |
|
|
|
userRequestsCached = data || []; |
|
|
|
renderUserRequestsTable('user-requests-table-body', userRequestsCached, true); |
|
|
|
applyReportFilters(); |
|
|
|
}) |
|
|
|
.catch(err => console.error("Error fetching user requests for report:", err)); |
|
|
|
|
|
|
|
fetch(contextPath + 'gcctonermanagement/api/inward/list') |
|
|
|
.then(res => res.json()) |
|
|
|
.then(data => { |
|
|
|
inwardHistoryCached = data || []; |
|
|
|
renderInwardTable('inward-history-table-body', inwardHistoryCached, true); |
|
|
|
applyReportFilters(); |
|
|
|
}) |
|
|
|
.catch(err => console.error("Error fetching inward history for report:", err)); |
|
|
|
|
|
|
|
fetch(contextPath + 'gcctonermanagement/api/outward/list') |
|
|
|
.then(res => res.json()) |
|
|
|
.then(data => { |
|
|
|
outwardHistoryCached = data || []; |
|
|
|
applyReportFilters(); |
|
|
|
}) |
|
|
|
.catch(err => console.error("Error fetching outward history for report:", err)); |
|
|
|
} |
|
|
|
|
|
|
|
data.summary.forEach((item, index) => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
tr.innerHTML = ` |
|
|
|
<td class="fw-semibold text-secondary">${index + 1}</td> |
|
|
|
<td class="fw-semibold text-dark">${escapeHtml(item.printer_name)}</td> |
|
|
|
<td><span class="badge bg-secondary p-2 font-monospace">${escapeHtml(item.model_name)}</span></td> |
|
|
|
<td class="text-center text-primary fw-bold">${item.inward_qty}</td> |
|
|
|
<td class="text-center text-success fw-bold">${item.outward_qty}</td> |
|
|
|
<td class="text-center fw-bold ${item.available_qty <= 2 ? 'text-danger animate-pulse' : 'text-dark'}">${item.available_qty}</td> |
|
|
|
`; |
|
|
|
tbody.appendChild(tr); |
|
|
|
function onReportTypeChange() { |
|
|
|
const reportType = document.getElementById('report-type-select').value; |
|
|
|
const statusWrapper = document.getElementById('report-status-filter-wrapper'); |
|
|
|
if (statusWrapper) { |
|
|
|
statusWrapper.style.display = (reportType === 'user_requests') ? 'block' : 'none'; |
|
|
|
} |
|
|
|
applyReportFilters(); |
|
|
|
} |
|
|
|
|
|
|
|
function onReportPrinterFilterChange() { |
|
|
|
const pid = document.getElementById('report-printer-filter').value; |
|
|
|
const tonerSelect = document.getElementById('report-toner-filter'); |
|
|
|
tonerSelect.innerHTML = '<option value="">-- All Toners --</option>'; |
|
|
|
tonerSelect.disabled = true; |
|
|
|
|
|
|
|
if (pid) { |
|
|
|
fetch(contextPath + 'gcctonermanagement/api/toners?pid=' + pid) |
|
|
|
.then(res => res.json()) |
|
|
|
.then(toners => { |
|
|
|
toners.forEach(t => { |
|
|
|
const opt = document.createElement('option'); |
|
|
|
opt.value = t.id; |
|
|
|
opt.textContent = t.model_name; |
|
|
|
tonerSelect.appendChild(opt); |
|
|
|
}); |
|
|
|
if (toners.length > 0) { |
|
|
|
tonerSelect.disabled = false; |
|
|
|
} |
|
|
|
applyReportFilters(); |
|
|
|
}) |
|
|
|
.catch(err => { |
|
|
|
console.error("Error loading toners filter:", err); |
|
|
|
applyReportFilters(); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
applyReportFilters(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function applyReportFilters() { |
|
|
|
const reportType = document.getElementById('report-type-select').value; |
|
|
|
const selectedPrinter = document.getElementById('report-printer-filter').value; |
|
|
|
const selectedToner = document.getElementById('report-toner-filter').value; |
|
|
|
const selectedStatus = document.getElementById('report-status-filter').value; |
|
|
|
const searchQuery = document.getElementById('report-search-input').value.trim().toLowerCase(); |
|
|
|
|
|
|
|
const titleEl = document.getElementById('report-dynamic-title'); |
|
|
|
const badgeEl = document.getElementById('report-count-badge'); |
|
|
|
const thead = document.getElementById('report-dynamic-thead'); |
|
|
|
const tbody = document.getElementById('report-dynamic-tbody'); |
|
|
|
|
|
|
|
if (!titleEl || !thead || !tbody) return; |
|
|
|
|
|
|
|
// 1. User Request Details Report |
|
|
|
if (reportType === 'user_requests') { |
|
|
|
titleEl.innerHTML = `<i class="fa-solid fa-file-invoice me-2"></i>User Request Details Report`; |
|
|
|
thead.innerHTML = ` |
|
|
|
<tr> |
|
|
|
<th>S.No</th> |
|
|
|
<th>Req. No</th> |
|
|
|
<th>IT Cell Ref No</th> |
|
|
|
<th>Department</th> |
|
|
|
<th>Printer Model</th> |
|
|
|
<th>Toner Model</th> |
|
|
|
<th class="text-center">Qty Req</th> |
|
|
|
<th class="text-center">Qty Delivered</th> |
|
|
|
<th>Created Date</th> |
|
|
|
<th class="text-center">Status</th> |
|
|
|
<th class="text-center">Request Doc</th> |
|
|
|
</tr>`; |
|
|
|
|
|
|
|
let filtered = userRequestsCached.filter(item => { |
|
|
|
if (selectedPrinter && String(item.pid) !== String(selectedPrinter)) return false; |
|
|
|
if (selectedToner && String(item.tid) !== String(selectedToner)) return false; |
|
|
|
|
|
|
|
const reqQty = parseInt(item.qty_requested || 0); |
|
|
|
const delQty = parseInt(item.qty_delivered || 0); |
|
|
|
let status = 'Pending'; |
|
|
|
if (delQty >= reqQty && reqQty > 0) status = 'Delivered'; |
|
|
|
else if (delQty > 0) status = 'Partial'; |
|
|
|
|
|
|
|
if (selectedStatus && status !== selectedStatus) return false; |
|
|
|
|
|
|
|
if (searchQuery) { |
|
|
|
const searchStr = `${item.req_no || ''} ${item.itcell_ref_no || ''} ${item.dept_id || ''} ${item.printer_name || ''} ${item.model_name || ''}`.toLowerCase(); |
|
|
|
if (!searchStr.includes(searchQuery)) return false; |
|
|
|
} |
|
|
|
return true; |
|
|
|
}); |
|
|
|
|
|
|
|
if (badgeEl) badgeEl.textContent = `${filtered.length} Records`; |
|
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
|
|
if (filtered.length === 0) { |
|
|
|
tbody.innerHTML = `<tr><td colspan="11" class="text-center text-muted py-4">No matching user request records found.</td></tr>`; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
filtered.forEach((item, idx) => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
const createdDate = formatTimestamp(item.cdate); |
|
|
|
const reqQty = parseInt(item.qty_requested || 0); |
|
|
|
const delQty = parseInt(item.qty_delivered || 0); |
|
|
|
|
|
|
|
let statusBadge = '<span class="badge bg-warning text-dark"><i class="fa-solid fa-clock me-1"></i>Pending</span>'; |
|
|
|
if (delQty >= reqQty && reqQty > 0) { |
|
|
|
statusBadge = '<span class="badge bg-success"><i class="fa-solid fa-circle-check me-1"></i>Delivered</span>'; |
|
|
|
} else if (delQty > 0) { |
|
|
|
statusBadge = '<span class="badge bg-info text-dark"><i class="fa-solid fa-boxes-packing me-1"></i>Partial</span>'; |
|
|
|
} |
|
|
|
|
|
|
|
let docBtn = '-'; |
|
|
|
if (item.user_request_upload) { |
|
|
|
docBtn = `<a href="${contextPath + item.user_request_upload.substring(1)}" target="_blank" class="btn-action btn-view-doc"><i class="fa-solid fa-file-pdf"></i> View Letter</a>`; |
|
|
|
} |
|
|
|
|
|
|
|
tr.innerHTML = ` |
|
|
|
<td class="fw-semibold text-secondary">${idx + 1}</td> |
|
|
|
<td class="fw-bold text-dark">${escapeHtml(item.req_no)}</td> |
|
|
|
<td class="font-monospace text-secondary">${escapeHtml(item.itcell_ref_no || '-')}</td> |
|
|
|
<td>${escapeHtml(item.dept_id)}</td> |
|
|
|
<td>${escapeHtml(item.printer_name || '-')}</td> |
|
|
|
<td><span class="badge bg-secondary p-1">${escapeHtml(item.model_name || '-')}</span></td> |
|
|
|
<td class="text-center font-monospace">${item.qty_requested}</td> |
|
|
|
<td class="text-center text-success fw-bold font-monospace">${item.qty_delivered || 0}</td> |
|
|
|
<td><small class="text-dark fw-semibold">${createdDate}</small></td> |
|
|
|
<td class="text-center">${statusBadge}</td> |
|
|
|
<td class="text-center">${docBtn}</td> |
|
|
|
`; |
|
|
|
tbody.appendChild(tr); |
|
|
|
}); |
|
|
|
} |
|
|
|
// 2. Inward Register Stock Report |
|
|
|
else if (reportType === 'inward_stock') { |
|
|
|
titleEl.innerHTML = `<i class="fa-solid fa-boxes-packing me-2"></i>Inward Register Stock Report`; |
|
|
|
thead.innerHTML = ` |
|
|
|
<tr> |
|
|
|
<th>S.No</th> |
|
|
|
<th>Company Name</th> |
|
|
|
<th>Printer Model</th> |
|
|
|
<th>Toner Model</th> |
|
|
|
<th class="text-center">Qty Inward</th> |
|
|
|
<th>Created Date</th> |
|
|
|
<th>Entered By</th> |
|
|
|
<th class="text-center">DC Document</th> |
|
|
|
</tr>`; |
|
|
|
|
|
|
|
let filtered = inwardHistoryCached.filter(item => { |
|
|
|
if (selectedPrinter && String(item.pid) !== String(selectedPrinter)) return false; |
|
|
|
if (selectedToner && String(item.tid) !== String(selectedToner)) return false; |
|
|
|
if (searchQuery) { |
|
|
|
const searchStr = `${item.company_name || ''} ${item.printer_name || ''} ${item.model_name || ''} ${item.order_by || ''}`.toLowerCase(); |
|
|
|
if (!searchStr.includes(searchQuery)) return false; |
|
|
|
} |
|
|
|
return true; |
|
|
|
}); |
|
|
|
|
|
|
|
if (badgeEl) badgeEl.textContent = `${filtered.length} Records`; |
|
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
|
|
if (filtered.length === 0) { |
|
|
|
tbody.innerHTML = `<tr><td colspan="8" class="text-center text-muted py-4">No matching inward stock records found.</td></tr>`; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
filtered.forEach((item, idx) => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
const createdDate = formatTimestamp(item.cdate); |
|
|
|
let docBtn = '-'; |
|
|
|
if (item.dc_upload) { |
|
|
|
docBtn = `<a href="${contextPath + item.dc_upload.substring(1)}" target="_blank" class="btn-action btn-view-doc"><i class="fa-solid fa-file-pdf"></i> View DC</a>`; |
|
|
|
} |
|
|
|
|
|
|
|
tr.innerHTML = ` |
|
|
|
<td class="fw-semibold text-secondary">${idx + 1}</td> |
|
|
|
<td class="fw-bold text-dark">${escapeHtml(item.company_name)}</td> |
|
|
|
<td>${escapeHtml(item.printer_name || '-')}</td> |
|
|
|
<td><span class="badge bg-secondary p-1">${escapeHtml(item.model_name || '-')}</span></td> |
|
|
|
<td class="text-center text-primary fw-bold font-monospace">${item.qty_registered}</td> |
|
|
|
<td><small class="text-dark fw-semibold">${createdDate}</small></td> |
|
|
|
<td><small class="text-muted">${escapeHtml(item.order_by || '-')}</small></td> |
|
|
|
<td class="text-center">${docBtn}</td> |
|
|
|
`; |
|
|
|
tbody.appendChild(tr); |
|
|
|
}); |
|
|
|
} |
|
|
|
// 3. Current Stock Summary |
|
|
|
else if (reportType === 'stock_summary') { |
|
|
|
titleEl.innerHTML = `<i class="fa-solid fa-warehouse me-2"></i>Current Stock Summary`; |
|
|
|
thead.innerHTML = ` |
|
|
|
<tr> |
|
|
|
<th>S.No</th> |
|
|
|
<th>Printer Model</th> |
|
|
|
<th>Toner Model</th> |
|
|
|
<th class="text-center">Total Inward</th> |
|
|
|
<th class="text-center">Total Outward</th> |
|
|
|
<th class="text-center">Available Stock</th> |
|
|
|
</tr>`; |
|
|
|
|
|
|
|
let filtered = stockSummaryCached.filter(item => { |
|
|
|
if (searchQuery) { |
|
|
|
const searchStr = `${item.printer_name || ''} ${item.model_name || ''}`.toLowerCase(); |
|
|
|
if (!searchStr.includes(searchQuery)) return false; |
|
|
|
} |
|
|
|
return true; |
|
|
|
}); |
|
|
|
|
|
|
|
if (badgeEl) badgeEl.textContent = `${filtered.length} Records`; |
|
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
|
|
if (filtered.length === 0) { |
|
|
|
tbody.innerHTML = `<tr><td colspan="6" class="text-center text-muted py-4">No matching stock summary records found.</td></tr>`; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
filtered.forEach((item, idx) => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
tr.innerHTML = ` |
|
|
|
<td class="fw-semibold text-secondary">${idx + 1}</td> |
|
|
|
<td class="fw-semibold text-dark">${escapeHtml(item.printer_name)}</td> |
|
|
|
<td><span class="badge bg-secondary p-2 font-monospace">${escapeHtml(item.model_name)}</span></td> |
|
|
|
<td class="text-center text-primary fw-bold">${item.inward_qty}</td> |
|
|
|
<td class="text-center text-success fw-bold">${item.outward_qty}</td> |
|
|
|
<td class="text-center fw-bold ${item.available_qty <= 2 ? 'text-danger animate-pulse' : 'text-dark'}">${item.available_qty}</td> |
|
|
|
`; |
|
|
|
tbody.appendChild(tr); |
|
|
|
}); |
|
|
|
} |
|
|
|
// 4. Outward Distribution History |
|
|
|
else if (reportType === 'outward_history') { |
|
|
|
titleEl.innerHTML = `<i class="fa-solid fa-truck-moving me-2"></i>Outward Distribution History`; |
|
|
|
thead.innerHTML = ` |
|
|
|
<tr> |
|
|
|
<th>S.No</th> |
|
|
|
<th>Req. No</th> |
|
|
|
<th>Department</th> |
|
|
|
<th>Printer Model</th> |
|
|
|
<th>Toner Model</th> |
|
|
|
<th class="text-center">Qty Req</th> |
|
|
|
<th class="text-center">Qty Issued</th> |
|
|
|
<th>Requested Date</th> |
|
|
|
<th>Issued Date</th> |
|
|
|
<th class="text-center">Action</th> |
|
|
|
</tr>`; |
|
|
|
|
|
|
|
let filtered = outwardHistoryCached.filter(item => { |
|
|
|
if (selectedPrinter && String(item.pid) !== String(selectedPrinter)) return false; |
|
|
|
if (selectedToner && String(item.tid) !== String(selectedToner)) return false; |
|
|
|
if (searchQuery) { |
|
|
|
const searchStr = `${item.req_no || ''} ${item.dept_id || ''} ${item.printer_name || ''} ${item.model_name || ''}`.toLowerCase(); |
|
|
|
if (!searchStr.includes(searchQuery)) return false; |
|
|
|
} |
|
|
|
return true; |
|
|
|
}); |
|
|
|
|
|
|
|
if (badgeEl) badgeEl.textContent = `${filtered.length} Records`; |
|
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
|
|
if (filtered.length === 0) { |
|
|
|
tbody.innerHTML = `<tr><td colspan="10" class="text-center text-muted py-4">No matching outward records found.</td></tr>`; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
filtered.forEach((item, idx) => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
const reqDateFormatted = formatTimestamp(item.cdate); |
|
|
|
const dateFormatted = formatTimestamp(item.updated_date); |
|
|
|
let docBtn = ''; |
|
|
|
if (item.final_delivered_upload) { |
|
|
|
docBtn = `<a href="${contextPath + item.final_delivered_upload.substring(1)}" target="_blank" class="btn-action btn-view-doc me-1"><i class="fa-solid fa-file-pdf"></i> View Doc</a>`; |
|
|
|
} |
|
|
|
const pdfBtn = `<button type="button" class="btn-action btn-pdf" onclick='downloadPdfAcknowledgement(${JSON.stringify(item)})'><i class="fa-solid fa-file-arrow-down"></i> PDF Ack</button>`; |
|
|
|
|
|
|
|
tr.innerHTML = ` |
|
|
|
<td class="fw-semibold text-secondary">${idx + 1}</td> |
|
|
|
<td class="fw-bold text-dark">${escapeHtml(item.req_no)}</td> |
|
|
|
<td>${escapeHtml(item.dept_id)}</td> |
|
|
|
<td>${escapeHtml(item.printer_name)}</td> |
|
|
|
<td><span class="badge bg-secondary p-1">${escapeHtml(item.model_name)}</span></td> |
|
|
|
<td class="text-center font-monospace">${item.qty_requested}</td> |
|
|
|
<td class="text-center text-success fw-bold font-monospace">${item.qty_delivered}</td> |
|
|
|
<td><small class="text-dark fw-semibold">${reqDateFormatted}</small></td> |
|
|
|
<td><small class="text-muted">${dateFormatted}</small></td> |
|
|
|
<td class="text-center">${docBtn}${pdfBtn}</td> |
|
|
|
`; |
|
|
|
tbody.appendChild(tr); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// ================= EXPORT TO EXCEL LOGIC ================= |
|
|
|
function exportReportToExcel() { |
|
|
|
const reportTitleEl = document.getElementById('report-dynamic-title'); |
|
|
|
let rawTitle = reportTitleEl ? reportTitleEl.innerText.trim() : "Toner_Report"; |
|
|
|
let cleanFileName = rawTitle.replace(/[^a-zA-Z0-9]/g, "_") + "_" + new Date().toISOString().slice(0, 10) + ".xlsx"; |
|
|
|
|
|
|
|
const table = document.querySelector("#dynamic-report-card table"); |
|
|
|
if (!table) { |
|
|
|
showNotification('warning', 'Export Warning', 'No report table found to export.'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Clone table to strip out doc/action column from Excel output |
|
|
|
const clone = table.cloneNode(true); |
|
|
|
clone.querySelectorAll("tr").forEach(tr => { |
|
|
|
const lastTh = tr.querySelector("th:last-child"); |
|
|
|
if (lastTh && (lastTh.innerText.toLowerCase().includes("doc") || lastTh.innerText.toLowerCase().includes("action"))) { |
|
|
|
lastTh.remove(); |
|
|
|
} |
|
|
|
const lastTd = tr.querySelector("td:last-child"); |
|
|
|
if (lastTd && (lastTd.querySelector(".btn-action") || lastTd.innerText.toLowerCase().includes("view") || lastTd.innerText.toLowerCase().includes("ack"))) { |
|
|
|
lastTd.remove(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
try { |
|
|
|
const wb = XLSX.utils.table_to_book(clone, { sheet: "Toner Report" }); |
|
|
|
XLSX.writeFile(wb, cleanFileName); |
|
|
|
} catch (err) { |
|
|
|
console.error("Error exporting Excel:", err); |
|
|
|
showNotification('error', 'Export Error', 'Failed to generate Excel file.'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// ================= INWARD HISTORY FETCH & RENDER ================= |
|
|
|
function fetchInwardHistory() { |
|
|
|
fetch(contextPath + 'gcctonermanagement/api/inward/list') |
|
|
|
.then(res => res.json()) |
|
|
|
.then(data => { |
|
|
|
inwardHistoryCached = data || []; |
|
|
|
renderInwardTable('inward-history-table-body', inwardHistoryCached, true); |
|
|
|
applyReportFilters(); |
|
|
|
}) |
|
|
|
.catch(err => { |
|
|
|
console.error("Error loading inward history:", err); |
|
|
|
const errHtml = `<tr><td colspan="8" class="text-center text-danger py-4">Error loading inward details.</td></tr>`; |
|
|
|
if (document.getElementById('inward-history-table-body')) { |
|
|
|
document.getElementById('inward-history-table-body').innerHTML = errHtml; |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function renderInwardTable(tbodyId, data, showDoc) { |
|
|
|
const tbody = document.getElementById(tbodyId); |
|
|
|
if (!tbody) return; |
|
|
|
tbody.innerHTML = ''; |
|
|
|
const colSpan = showDoc ? 8 : 7; |
|
|
|
if (!data || data.length === 0) { |
|
|
|
tbody.innerHTML = `<tr><td colspan="${colSpan}" class="text-center text-muted py-4">No inward records found.</td></tr>`; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
data.forEach((item, index) => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
const createdDate = formatTimestamp(item.cdate); |
|
|
|
let docBtn = '-'; |
|
|
|
if (showDoc && item.dc_upload) { |
|
|
|
docBtn = `<a href="${contextPath + item.dc_upload.substring(1)}" target="_blank" class="btn-action btn-view-doc"><i class="fa-solid fa-file-pdf"></i> View DC</a>`; |
|
|
|
} |
|
|
|
|
|
|
|
let rowHtml = ` |
|
|
|
<td class="fw-semibold text-secondary">${index + 1}</td> |
|
|
|
<td class="fw-bold text-dark">${escapeHtml(item.company_name)}</td> |
|
|
|
<td>${escapeHtml(item.printer_name || '-')}</td> |
|
|
|
<td><span class="badge bg-secondary p-1">${escapeHtml(item.model_name || '-')}</span></td> |
|
|
|
<td class="text-center text-primary fw-bold font-monospace">${item.qty_registered}</td> |
|
|
|
<td><small class="text-dark fw-semibold">${createdDate}</small></td> |
|
|
|
<td><small class="text-muted">${escapeHtml(item.order_by || '-')}</small></td> |
|
|
|
`; |
|
|
|
if (showDoc) { |
|
|
|
rowHtml += `<td class="text-center">${docBtn}</td>`; |
|
|
|
} |
|
|
|
tr.innerHTML = rowHtml; |
|
|
|
tbody.appendChild(tr); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// ================= USER REQUESTS FETCH & RENDER ================= |
|
|
|
function fetchUserRequests() { |
|
|
|
fetch(contextPath + 'gcctonermanagement/api/requests/list') |
|
|
|
.then(res => res.json()) |
|
|
|
.then(data => { |
|
|
|
userRequestsCached = data || []; |
|
|
|
renderUserRequestsTable('user-requests-table-body', userRequestsCached, true); |
|
|
|
applyReportFilters(); |
|
|
|
}) |
|
|
|
.catch(err => { |
|
|
|
console.error("Error loading report page:", err); |
|
|
|
document.getElementById('report-table-body').innerHTML = `<tr><td colspan="6" class="text-center text-danger py-4">Error loading report data.</td></tr>`; |
|
|
|
console.error("Error loading user requests:", err); |
|
|
|
const errHtml = `<tr><td colspan="11" class="text-center text-danger py-4">Error loading user request details.</td></tr>`; |
|
|
|
if (document.getElementById('user-requests-table-body')) { |
|
|
|
document.getElementById('user-requests-table-body').innerHTML = errHtml; |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function renderUserRequestsTable(tbodyId, data, showDoc) { |
|
|
|
const tbody = document.getElementById(tbodyId); |
|
|
|
if (!tbody) return; |
|
|
|
tbody.innerHTML = ''; |
|
|
|
const colSpan = showDoc ? 11 : 10; |
|
|
|
if (!data || data.length === 0) { |
|
|
|
tbody.innerHTML = `<tr><td colspan="${colSpan}" class="text-center text-muted py-4">No user request records found.</td></tr>`; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
data.forEach((item, index) => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
const createdDate = formatTimestamp(item.cdate); |
|
|
|
const reqQty = parseInt(item.qty_requested || 0); |
|
|
|
const delQty = parseInt(item.qty_delivered || 0); |
|
|
|
|
|
|
|
let statusBadge = '<span class="badge bg-warning text-dark"><i class="fa-solid fa-clock me-1"></i>Pending</span>'; |
|
|
|
if (delQty >= reqQty && reqQty > 0) { |
|
|
|
statusBadge = '<span class="badge bg-success"><i class="fa-solid fa-circle-check me-1"></i>Delivered</span>'; |
|
|
|
} else if (delQty > 0) { |
|
|
|
statusBadge = '<span class="badge bg-info text-dark"><i class="fa-solid fa-boxes-packing me-1"></i>Partial</span>'; |
|
|
|
} |
|
|
|
|
|
|
|
let docBtn = '-'; |
|
|
|
if (showDoc && item.user_request_upload) { |
|
|
|
docBtn = `<a href="${contextPath + item.user_request_upload.substring(1)}" target="_blank" class="btn-action btn-view-doc"><i class="fa-solid fa-file-pdf"></i> View Letter</a>`; |
|
|
|
} |
|
|
|
|
|
|
|
let rowHtml = ` |
|
|
|
<td class="fw-semibold text-secondary">${index + 1}</td> |
|
|
|
<td class="fw-bold text-dark">${escapeHtml(item.req_no)}</td> |
|
|
|
<td class="font-monospace text-secondary">${escapeHtml(item.itcell_ref_no || '-')}</td> |
|
|
|
<td>${escapeHtml(item.dept_id)}</td> |
|
|
|
<td>${escapeHtml(item.printer_name || '-')}</td> |
|
|
|
<td><span class="badge bg-secondary p-1">${escapeHtml(item.model_name || '-')}</span></td> |
|
|
|
<td class="text-center font-monospace">${item.qty_requested}</td> |
|
|
|
<td class="text-center text-success fw-bold font-monospace">${item.qty_delivered || 0}</td> |
|
|
|
<td><small class="text-dark fw-semibold">${createdDate}</small></td> |
|
|
|
<td class="text-center">${statusBadge}</td> |
|
|
|
`; |
|
|
|
if (showDoc) { |
|
|
|
rowHtml += `<td class="text-center">${docBtn}</td>`; |
|
|
|
} |
|
|
|
tr.innerHTML = rowHtml; |
|
|
|
tbody.appendChild(tr); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ================= INWARD UPLOAD API ================= |
|
|
|
function submitInward(event) { |
|
|
|
@ -1168,7 +1772,8 @@ |
|
|
|
text: res.message || 'Inward stock added successfully!', |
|
|
|
confirmButtonColor: '#0f5c78' |
|
|
|
}).then(() => { |
|
|
|
switchView('report'); |
|
|
|
fetchInwardHistory(); |
|
|
|
switchView('inward'); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
showNotification('error', 'Error', res.error || 'Failed to save inward stock details.'); |
|
|
|
@ -1250,7 +1855,8 @@ |
|
|
|
html: `Department request saved successfully!<br><br><strong>Request No:</strong> <span style="font-size: 1.25rem; color: #0f5c78; font-family: monospace;">${res.reqNo}</span>`, |
|
|
|
confirmButtonColor: '#0f5c78' |
|
|
|
}).then(() => { |
|
|
|
switchView('report'); |
|
|
|
fetchUserRequests(); |
|
|
|
switchView('request'); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
showNotification('error', 'Error', res.error || 'Failed to save department request.'); |
|
|
|
@ -1325,12 +1931,13 @@ |
|
|
|
const tbody = document.getElementById('outward-history-table-body'); |
|
|
|
tbody.innerHTML = ''; |
|
|
|
if (data.length === 0) { |
|
|
|
tbody.innerHTML = `<tr><td colspan="8" class="text-center text-muted py-4">No outward records found.</td></tr>`; |
|
|
|
tbody.innerHTML = `<tr><td colspan="9" class="text-center text-muted py-4">No outward records found.</td></tr>`; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
data.forEach(item => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
const reqDateFormatted = formatTimestamp(item.cdate); |
|
|
|
const dateFormatted = formatTimestamp(item.updated_date); |
|
|
|
let docBtn = ''; |
|
|
|
if (item.final_delivered_upload) { |
|
|
|
@ -1345,6 +1952,7 @@ |
|
|
|
<td><span class="badge bg-secondary p-1">${escapeHtml(item.model_name)}</span></td> |
|
|
|
<td class="text-center font-monospace">${item.qty_requested}</td> |
|
|
|
<td class="text-center text-success fw-bold font-monospace">${item.qty_delivered}</td> |
|
|
|
<td><small class="text-dark fw-semibold">${reqDateFormatted}</small></td> |
|
|
|
<td><small class="text-muted">${dateFormatted}</small></td> |
|
|
|
<td class="text-center">${docBtn}${pdfBtn}</td> |
|
|
|
`; |
|
|
|
@ -1353,10 +1961,11 @@ |
|
|
|
}) |
|
|
|
.catch(err => { |
|
|
|
console.error("Error loading outward history:", err); |
|
|
|
document.getElementById('outward-history-table-body').innerHTML = `<tr><td colspan="8" class="text-center text-danger py-4">Error loading outward deliveries.</td></tr>`; |
|
|
|
document.getElementById('outward-history-table-body').innerHTML = `<tr><td colspan="9" class="text-center text-danger py-4">Error loading outward deliveries.</td></tr>`; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function submitOutward(event) { |
|
|
|
event.preventDefault(); |
|
|
|
|
|
|
|
|