Browse Source

newly added toner management project and changes in manpower , payment tracker ui and api's

master
yoganandhank 3 months ago
parent
commit
fe2aee5f4c
  1. 13
      src/main/java/com/gcc_manpower/config/DataSourceConfig.java
  2. 9
      src/main/java/com/gcc_manpower/config/WebConfig.java
  3. 3
      src/main/java/com/gcc_manpower/gccservicedocuments/service/GccServiceDocumentsService.java
  4. 226
      src/main/java/com/gcc_manpower/gcctonermanagement/controller/TonerController.java
  5. 97
      src/main/java/com/gcc_manpower/gcctonermanagement/service/TonerService.java
  6. 1
      src/main/java/com/gcc_manpower/service/AppUserService.java
  7. 6
      src/main/resources/templates/fragments/navbar.html
  8. 22
      src/main/resources/templates/fragments/tonerheader.html
  9. 292
      src/main/resources/templates/gccservicepaymenttracker/dashboard.html
  10. 116
      src/main/resources/templates/gcctonermanagement/login.html
  11. 670
      src/main/resources/templates/gcctonermanagement/upload.html
  12. 957
      src/main/resources/templates/home/home.html
  13. 2
      src/main/resources/templates/home/view-login-details.html

13
src/main/java/com/gcc_manpower/config/DataSourceConfig.java

@ -14,7 +14,8 @@ public class DataSourceConfig {
private static String dbpassword = "root";
// AWS
// private static String host = "gcc-facial-db-instance-1.cf48eqcciziq.ap-south-1.rds.amazonaws.com:3306";
// private static String host =
// "gcc-facial-db-instance-1.cf48eqcciziq.ap-south-1.rds.amazonaws.com:3306";
// private static String dbpassword = "gcc-facial-password";
@Bean(name = "mysqlManpowerDataSource")
@ -46,4 +47,14 @@ public class DataSourceConfig {
dataSource.setPassword(dbpassword);
return dataSource;
}
@Bean(name = "mysqlTonerManagementDataSource")
public DataSource mysqlTonerManagementDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://" + host + "/gcc_toner_management");
dataSource.setUsername("root");
dataSource.setPassword(dbpassword);
return dataSource;
}
}

9
src/main/java/com/gcc_manpower/config/WebConfig.java

@ -18,6 +18,9 @@ public class WebConfig implements WebMvcConfigurer {
@Value("${payment.tracker.upload.path:gccpaymenttrackerfiles}")
private String paymentTrackerUploadPathConfig;
@Value("${toner.upload.path:gcctonermanagementfiles}")
private String tonerUploadPathConfig;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Resolve absolute path (supports both relative and absolute paths)
@ -39,5 +42,11 @@ public class WebConfig implements WebMvcConfigurer {
.toString();
registry.addResourceHandler("/gccpaymenttrackerfiles/**")
.addResourceLocations(trackerUploadPath);
// Map URL path /gcctonermanagementfiles/** to the configured toner folder
String tonerUploadPath = Paths.get(tonerUploadPathConfig).toAbsolutePath().toUri()
.toString();
registry.addResourceHandler("/gcctonermanagementfiles/**")
.addResourceLocations(tonerUploadPath);
}
}

3
src/main/java/com/gcc_manpower/gccservicedocuments/service/GccServiceDocumentsService.java

@ -226,7 +226,8 @@ public class GccServiceDocumentsService {
return null;
}
public boolean updateDocument(Long id, String title, String mainCategory, String subCategory, String fileType, String filePath,
public boolean updateDocument(Long id, String title, String mainCategory, String subCategory, String fileType,
String filePath,
String originalFilename) {
// Insert old record into history before update
try {

226
src/main/java/com/gcc_manpower/gcctonermanagement/controller/TonerController.java

@ -0,0 +1,226 @@
package com.gcc_manpower.gcctonermanagement.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import com.gcc_manpower.gcctonermanagement.service.TonerService;
import com.gcc_manpower.service.AppUserService;
import jakarta.servlet.http.HttpSession;
@Controller
@RequestMapping("/gcctonermanagement")
public class TonerController {
@Autowired
private AppUserService appUserService;
@Autowired
private TonerService tonerService;
@Value("${toner.upload.path:gcctonermanagementfiles}")
private String tonerUploadPathConfig;
@GetMapping({ "", "/", "/upload" })
public String viewDashboard(Model model, HttpSession session) {
String username = (String) session.getAttribute("toner_usrName");
if (username == null) {
return "redirect:/gcctonermanagement/login";
}
String usrRole = (String) session.getAttribute("toner_usrRole");
boolean isAdmin = "ADMIN".equalsIgnoreCase(usrRole);
// List<Map<String, Object>> vendorDetails =
// appUserService.getVendorDetailsWithCounts();
model.addAttribute("isLoggedIn", true);
model.addAttribute("isAdmin", isAdmin);
model.addAttribute("usrName", username);
model.addAttribute("curPage", "gcctonermanagement");
// model.addAttribute("vendorDetails", vendorDetails);
return "gcctonermanagement/upload";
}
@GetMapping("/login")
public String login(HttpSession session) {
return "gcctonermanagement/login";
}
@PostMapping("/api/login")
@ResponseBody
public Map<String, Object> validateLogin(
@RequestParam("usrName") String usrName,
@RequestParam("usrPass") String usrPass,
HttpSession session) {
Map<String, Object> authResult = appUserService.authenticate(usrName, usrPass);
Map<String, Object> response = new HashMap<>();
if (Boolean.TRUE.equals(authResult.get("success"))) {
session.setAttribute("toner_usrName", authResult.get("usrName"));
session.setAttribute("toner_usrRole", authResult.get("usrRole"));
session.setMaxInactiveInterval(1800); // 30 minutes session timeout
response.put("result", true);
response.put("usrName", authResult.get("usrName"));
} else {
response.put("result", false);
response.put("error", authResult.get("error"));
}
return response;
}
@GetMapping("/logout")
public String logoutGet(HttpSession session) {
session.removeAttribute("toner_usrName");
session.removeAttribute("toner_usrRole");
return "redirect:/gcctonermanagement/login";
}
@GetMapping("/api/printers")
@ResponseBody
public List<Map<String, Object>> getPrinters(HttpSession session) {
if (session.getAttribute("toner_usrName") == null) {
return java.util.Collections.emptyList();
}
return tonerService.getActivePrinters();
}
@GetMapping("/api/toners")
@ResponseBody
public List<Map<String, Object>> getToners(@RequestParam("pid") int pid, HttpSession session) {
if (session.getAttribute("toner_usrName") == null) {
return java.util.Collections.emptyList();
}
return tonerService.getActiveTonersByPrinter(pid);
}
@PostMapping("/api/upload")
@ResponseBody
public Map<String, Object> uploadToner(
@RequestParam("companyName") String companyName,
@RequestParam("pid") int pid,
@RequestParam("tid") int tid,
@RequestParam("qty") String qty,
@RequestParam(value = "dcFile", required = false) MultipartFile dcFile,
@RequestParam(value = "userRequestFile", required = false) MultipartFile userRequestFile,
@RequestParam(value = "finalDeliveredFile", required = false) MultipartFile finalDeliveredFile,
HttpSession session) {
Map<String, Object> response = new HashMap<>();
String username = (String) session.getAttribute("toner_usrName");
if (username == null) {
response.put("success", false);
response.put("error", "Unauthorized access. Please log in.");
return response;
}
// Validate PDF extension on non-empty files
if (dcFile != null && !dcFile.isEmpty() && !isPdfFile(dcFile)) {
response.put("success", false);
response.put("error", "Delivery Challan file must be a PDF.");
return response;
}
if (userRequestFile != null && !userRequestFile.isEmpty() && !isPdfFile(userRequestFile)) {
response.put("success", false);
response.put("error", "User Request file must be a PDF.");
return response;
}
if (finalDeliveredFile != null && !finalDeliveredFile.isEmpty() && !isPdfFile(finalDeliveredFile)) {
response.put("success", false);
response.put("error", "Final Delivered file must be a PDF.");
return response;
}
try {
File uploadDir = new File(tonerUploadPathConfig).getAbsoluteFile();
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
// Save files
String dcPath = saveUploadedFile(dcFile, "dc", uploadDir);
String userRequestPath = saveUploadedFile(userRequestFile, "user_request", uploadDir);
String finalDeliveredPath = saveUploadedFile(finalDeliveredFile, "final_delivered", uploadDir);
boolean saved = tonerService.saveInventoryDetails(
companyName.trim(),
pid,
tid,
qty.trim(),
dcPath,
userRequestPath,
finalDeliveredPath,
username);
if (saved) {
response.put("success", true);
response.put("message", "Toner details saved successfully.");
} else {
// Delete newly saved files on DB insert failure
deleteFileIfExists(dcPath, uploadDir);
deleteFileIfExists(userRequestPath, uploadDir);
deleteFileIfExists(finalDeliveredPath, uploadDir);
response.put("success", false);
response.put("error", "Failed to save registration details in the database.");
}
} catch (Exception e) {
response.put("success", false);
response.put("error", "An error occurred during saving: " + e.getMessage());
e.printStackTrace();
}
return response;
}
private boolean isPdfFile(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
if (originalFilename == null)
return false;
return originalFilename.toLowerCase().endsWith(".pdf");
}
private String saveUploadedFile(MultipartFile file, String prefix, File uploadDir) throws Exception {
if (file == null || file.isEmpty()) {
return null;
}
String originalFilename = file.getOriginalFilename();
String ext = ".pdf"; // Since we only allow PDFs
// Clean filename, make it unique
String savedFilename = prefix + "_" + System.currentTimeMillis() + ext;
File destFile = new File(uploadDir, savedFilename).getAbsoluteFile();
file.transferTo(destFile);
return "/gcctonermanagementfiles/" + savedFilename;
}
private void deleteFileIfExists(String webPath, File uploadDir) {
if (webPath == null || webPath.isEmpty())
return;
try {
String filename = webPath.substring(webPath.lastIndexOf('/') + 1);
File file = new File(uploadDir, filename).getAbsoluteFile();
if (file.exists()) {
file.delete();
}
} catch (Exception e) {
System.err.println("Error deleting file: " + e.getMessage());
}
}
}

97
src/main/java/com/gcc_manpower/gcctonermanagement/service/TonerService.java

@ -0,0 +1,97 @@
package com.gcc_manpower.gcctonermanagement.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class TonerService {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(@Qualifier("mysqlTonerManagementDataSource") DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<Map<String, Object>> getActivePrinters() {
String sql = "SELECT id, printer_name FROM printer_model_list where is_active = 1 AND is_delete = 0 ";
try {
return jdbcTemplate.queryForList(sql);
} catch (Exception e) {
System.err.println("Error fetching printers list: " + e.getMessage());
return new ArrayList<>();
}
}
public List<Map<String, Object>> getActiveTonersByPrinter(int pid) {
String sql = "SELECT id, model_name FROM toner_model_list WHERE pid = ? AND is_active = 1 AND is_delete = 0 ";
try {
return jdbcTemplate.queryForList(sql, pid);
} catch (Exception e) {
System.err.println("Error fetching toners list: " + e.getMessage());
return new ArrayList<>();
}
}
public String getTonerModelName(int tid) {
String sql = "SELECT model_name FROM toner_model_list WHERE id = ?";
try {
return jdbcTemplate.queryForObject(sql, String.class, tid);
} catch (Exception e) {
System.err.println("Error fetching toner model name for id " + tid + ": " + e.getMessage());
return "UNKNOWN";
}
}
public int getMaxInventoryId() {
String sql = "SELECT COALESCE(MAX(id), 0) FROM inventory_registered_details";
try {
return jdbcTemplate.queryForObject(sql, Integer.class);
} catch (Exception e) {
System.err.println("Error fetching max id from inventory_registered_details: " + e.getMessage());
return 0;
}
}
public boolean saveInventoryDetails(String companyName, int pid, int tid, String qty, String dcUpload,
String userRequestUpload, String finalDeliveredUpload, String username) {
// 1. Get toner model name
String tonerModel = getTonerModelName(tid);
String cleanModel = tonerModel.trim().replaceAll("\\s+", "-").replaceAll("[^a-zA-Z0-9-]", "");
// 2. Get next sequential number
int maxId = getMaxInventoryId();
int seqNum = 1000 + maxId + 1;
// 3. Get current year
int year = java.util.Calendar.getInstance().get(java.util.Calendar.YEAR);
// 4. Construct request number
String reqNo = String.format("%s-%d-%04d", cleanModel, year, seqNum);
// 5. Insert row
String sql = "INSERT INTO department_requested_details " +
"(req_no, company_name, pid, tid, qty_registered, dc_upload, user_request_upload, final_delivered_upload, cdate, updated_date, updated_by, is_active, is_delete) "
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)";
java.sql.Timestamp now = new java.sql.Timestamp(System.currentTimeMillis());
try {
int rows = jdbcTemplate.update(sql, reqNo, companyName, String.valueOf(pid), String.valueOf(tid), qty,
dcUpload, userRequestUpload, finalDeliveredUpload, now, now, username);
return rows > 0;
} catch (Exception e) {
System.err.println("Error saving inventory details: " + e.getMessage());
e.printStackTrace();
return false;
}
}
}

1
src/main/java/com/gcc_manpower/service/AppUserService.java

@ -851,6 +851,7 @@ public class AppUserService {
" e.joined_date, " +
" e.cdate, " +
" e.updated_at, " +
" e.designation AS raw_designation, " +
" COALESCE(u.usr_name, e.updated_by) AS updated_by_name, " +
" COALESCE(v.vendor_name, e.vendor_id) AS vendor_name, " +
" COALESCE(ut.usertype, e.user_type) AS user_type, " +

6
src/main/resources/templates/fragments/navbar.html

@ -52,6 +52,12 @@
<i class="fa-solid fa-file-circle-xmark"></i> Deleted Work Orders
</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/?action=positionHistory}"
onclick="if (document.getElementById('positionHistoryModal')) { bootstrap.Modal.getOrCreateInstance(document.getElementById('positionHistoryModal')).show(); return false; }">
<i class="fa-solid fa-clock-rotate-left"></i> Position History
</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/?action=addVendorWorkOrder}"
onclick="if (document.getElementById('addVendorWorkOrderModal')) { bootstrap.Modal.getOrCreateInstance(document.getElementById('addVendorWorkOrderModal')).show(); return false; }">

22
src/main/resources/templates/fragments/tonerheader.html

@ -0,0 +1,22 @@
<th:block th:fragment="tonerheader">
<!-- Awesome Font -->
<link rel="stylesheet" th:href="@{/assets/font-awesome-6.5.2/css/all.min.css}" type="text/css">
<!-- Bootstrap CSS -->
<link rel="stylesheet" th:href="@{/assets/bootstrap-5.3.2/css/bootstrap.min.css}" type="text/css">
<!-- CSS for Header -->
<link rel="stylesheet" th:href="@{/assets/css/header-styles.css}" type="text/css">
<div class="header">
<div class="logo-left">
<img th:src="@{/assets/header-images/gcc1.png}" alt="Greater Chennai Corporation Logo">
<div class="logo-text">
<h2>GREATER</h2>
<h2>CHENNAI</h2>
<h2>CORPORATION</h2>
<p>பெருநகர சென்னை மாநகராட்சி</p>
</div>
</div>
<div class="title">
<h2>GCC Toner Material Management Details</h2>
</div>
</div>
</th:block>

292
src/main/resources/templates/gccservicepaymenttracker/dashboard.html

@ -37,6 +37,16 @@
<!-- Custom Style Sheet mapping to theme -->
<style>
.text-info-dark {
color: #075985 !important;
font-weight: 600;
}
.text-warning-dark {
color: #92400e !important;
font-weight: 600;
}
body {
background-color: #f4f7f6;
font-family: 'Poppins', sans-serif;
@ -1109,12 +1119,8 @@
<thead class="table-light text-secondary fw-bold" style="font-size: 0.75rem;">
<tr id="summary-thead-tr">
<th style="background-color: #f8fafc; color: #475569;">Total No of files</th>
<!-- Commented out status headers to tally with days-wise count
<th style="background-color: #f0fdf4; color: #166534;">Completed</th>
<th style="background-color: #fffbeb; color: #92400e;">Awaiting Details</th>
<th style="background-color: #f0f9ff; color: #075985;">Pending with Dept</th>
<th style="background-color: #fef2f2; color: #991b1b;">Pending</th>
-->
<th style="background-color: #f0fdf4; color: #166534;">Completed Files</th>
<th style="background-color: #fef2f2; color: #991b1b;">Pending Files</th>
<th style="background-color: #fafaf9; color: #44403c;">1 to 15 Days</th>
<th style="background-color: #fafaf9; color: #44403c;">16 to 30 Days</th>
<th style="background-color: #fafaf9; color: #44403c;">31 to 60 Days</th>
@ -1128,18 +1134,12 @@
<td id="summary-total" class="cursor-pointer text-primary"
style="background-color: #f8fafc;"
onclick="openModalForCount('total', null, 'Total No of files')">0</td>
<!-- Commented out status cells to tally with days-wise count
<td id="summary-completed" class="cursor-pointer text-success"
onclick="setClickFilter('status', 'Completed', 'Status: Completed')">0</td>
<td id="summary-awaiting" class="cursor-pointer text-warning"
onclick="setClickFilter('status', 'Awaiting Details', 'Status: Awaiting Details')">
0</td>
<td id="summary-dept" class="cursor-pointer text-info"
onclick="setClickFilter('status', 'Pending with Dept', 'Status: Pending with Dept')">
0</td>
onclick="openModalForCount('completed_custom', null, 'Completed Files')">0
</td>
<td id="summary-pending" class="cursor-pointer text-danger"
onclick="setClickFilter('status', 'Pending', 'Status: Pending')">0</td>
-->
style="color: #991b1b; font-weight: bold;"
onclick="openModalForCount('pending_custom', null, 'Pending Files')">0</td>
<td id="summary-1-15" class="cursor-pointer"
onclick="openModalForCount('age', [1, 15], 'Age: 1 to 15 Days')">0</td>
<td id="summary-16-30" class="cursor-pointer"
@ -1918,8 +1918,8 @@
const statusColors = {
'completed': { bg: '#f0fdf4', text: '#166534', badgeClass: 'text-success' },
'awaiting details': { bg: '#fffbeb', text: '#92400e', badgeClass: 'text-warning' },
'pending with dept': { bg: '#f0f9ff', text: '#075985', badgeClass: 'text-info' },
'awaiting details': { bg: '#fffbeb', text: '#92400e', badgeClass: 'text-warning-dark' },
'pending with dept': { bg: '#f0f9ff', text: '#075985', badgeClass: 'text-info-dark' },
'pending': { bg: '#fef2f2', text: '#991b1b', badgeClass: 'text-danger' },
'no action': { bg: '#fafaf9', text: '#44403c', badgeClass: 'text-secondary' },
'processed': { bg: '#dbeafe', text: '#1e40af', badgeClass: 'text-primary' },
@ -2008,13 +2008,11 @@
// Dynamically populate summary table headers
const summaryTheadTr = document.getElementById('summary-thead-tr');
if (summaryTheadTr) {
let summaryHeadHtml = `<th style="background-color: #f8fafc; color: #475569;">Total No of files</th>`;
/* Commented out status headers to tally with days-wise count
activeStatus.forEach(s => {
const style = getStatusStyle(s.status_name);
summaryHeadHtml += `<th style="background-color: ${style.bg}; color: ${style.text};">${escapeHtml(s.status_name)}</th>`;
});
*/
let summaryHeadHtml = `
<th style="background-color: #f8fafc; color: #475569;">Total No of files</th>
<th style="background-color: #f0fdf4; color: #166534;">Completed Files</th>
<th style="background-color: #fef2f2; color: #991b1b;">Pending Files</th>
`;
summaryHeadHtml += `
<th style="background-color: #fafaf9; color: #44403c;">1 to 15 Days</th>
<th style="background-color: #fafaf9; color: #44403c;">16 to 30 Days</th>
@ -2359,7 +2357,7 @@
const getTypeBadge = typeVal => {
const getTypeBadge = (typeVal, filePath) => {
const cleanType = String(typeVal || '').trim().toLowerCase();
let label = cleanType;
let badgeClass = 'type-sd';
@ -2389,12 +2387,26 @@
// Fallback: check if cleanType is a numeric ID from activeTypes
const typeObj = activeTypes.find(t => String(t.id) === cleanType);
if (typeObj) {
return getTypeBadge(typeObj.type_name);
return getTypeBadge(typeObj.type_name, filePath);
}
label = typeVal || '-';
badgeClass = 'status-default';
}
return `<span class="type-badge ${badgeClass}">${label}</span>`;
let onClickAttr = '';
if (filePath) {
let pdfUrl = filePath;
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
pdfUrl = contextPath + pdfUrl.substring(1);
} else {
pdfUrl = contextPath + pdfUrl;
}
onClickAttr = `onclick="window.open('${escapeHtml(pdfUrl)}', '_blank')"`;
} else {
onClickAttr = `onclick="alert('pdf file not uploaded')"`;
}
return `<span class="type-badge ${badgeClass} cursor-pointer" ${onClickAttr} style="cursor: pointer;" title="${filePath ? 'Click to view PDF' : 'PDF not uploaded'}">${label}</span>`;
};
const dispStatus = String(row.resolved_status || getStatusName(row.status) || '').trim();
@ -2405,7 +2417,7 @@
<td>${text(row.physical_file_no)}</td>
<td>${text(row.receiptno)}</td>
<td>${text(row.subject)}</td>
<td>${getTypeBadge(row.resolved_type || row.type)}</td>
<td>${getTypeBadge(row.resolved_type || row.type, row.file_path)}</td>
<td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td>
<td class="fw-semibold text-dark">&#8377; ${text(row.amount)}</td>
<td>${calculateAgeOfBill(row.received_date)}</td>
@ -2976,6 +2988,7 @@
s.status_name.toLowerCase().trim() === statusName.toLowerCase().trim()
);
const statusId = matchedStatus ? matchedStatus.id : null;
const isCompleted = matchedStatus && getStatusKey(matchedStatus.status_name) === 'completed';
const age = parseInt(calculateAgeOfBill(row.received_date), 10);
const vendorName = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
@ -2984,12 +2997,14 @@
statusCounts[statusId] = (statusCounts[statusId] || 0) + 1;
}
if (age >= 1 && age <= 15) age_1_15++;
else if (age >= 16 && age <= 30) age_16_30++;
else if (age >= 31 && age <= 60) age_31_60++;
else if (age >= 61 && age <= 90) age_61_90++;
else if (age >= 91 && age <= 120) age_91_120++;
else if (age > 120) age_over_120++;
if (!isCompleted) {
if (age >= 1 && age <= 15) age_1_15++;
else if (age >= 16 && age <= 30) age_16_30++;
else if (age >= 31 && age <= 60) age_31_60++;
else if (age >= 61 && age <= 90) age_61_90++;
else if (age >= 91 && age <= 120) age_91_120++;
else if (age > 120) age_over_120++;
}
if (!vendorMap[vendorName]) {
vendorMap[vendorName] = {
@ -3001,7 +3016,13 @@
age_31_60: 0,
age_61_90: 0,
age_91_120: 0,
age_over_120: 0
age_over_120: 0,
comp_age_1_15: 0,
comp_age_16_30: 0,
comp_age_31_60: 0,
comp_age_61_90: 0,
comp_age_91_120: 0,
comp_age_over_120: 0
};
activeStatus.forEach(s => {
vendorMap[vendorName].statusCounts[s.id] = 0;
@ -3014,26 +3035,35 @@
vData.statusCounts[statusId] = (vData.statusCounts[statusId] || 0) + 1;
}
if (age >= 1 && age <= 15) vData.age_1_15++;
else if (age >= 16 && age <= 30) vData.age_16_30++;
else if (age >= 31 && age <= 60) vData.age_31_60++;
else if (age >= 61 && age <= 90) vData.age_61_90++;
else if (age >= 91 && age <= 120) vData.age_91_120++;
else if (age > 120) vData.age_over_120++;
if (isCompleted) {
if (age >= 1 && age <= 15) vData.comp_age_1_15++;
else if (age >= 16 && age <= 30) vData.comp_age_16_30++;
else if (age >= 31 && age <= 60) vData.comp_age_31_60++;
else if (age >= 61 && age <= 90) vData.comp_age_61_90++;
else if (age >= 91 && age <= 120) vData.comp_age_91_120++;
else if (age > 120) vData.comp_age_over_120++;
} else {
if (age >= 1 && age <= 15) vData.age_1_15++;
else if (age >= 16 && age <= 30) vData.age_16_30++;
else if (age >= 31 && age <= 60) vData.age_31_60++;
else if (age >= 61 && age <= 90) vData.age_61_90++;
else if (age >= 91 && age <= 120) vData.age_91_120++;
else if (age > 120) vData.age_over_120++;
}
});
let completedTotal = 0;
const completedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed');
completedStatuses.forEach(s => {
completedTotal += (statusCounts[s.id] || 0);
});
// Populate total
document.getElementById('summary-total').textContent = total;
const pendingTotal = total - completedTotal;
// Dynamically populate body values for dynamic statuses
let summaryBodyHtml = `<td id="summary-total" class="cursor-pointer text-primary" style="background-color: #f8fafc;" onclick="openModalForCount('total', null, 'Total No of Files')">${total}</td>`;
/* Commented out status columns to tally with days-wise count
activeStatus.forEach(s => {
const count = statusCounts[s.id] || 0;
const style = getStatusStyle(s.status_name);
summaryBodyHtml += `<td id="summary-status-${s.id}" class="cursor-pointer ${style.badgeClass}" onclick="setClickFilter('status', '${s.id}', 'Status: ' + this.getAttribute('data-status-name'))" data-status-name="${escapeHtml(s.status_name)}">${count}</td>`;
});
*/
let summaryBodyHtml = `
<td id="summary-total" class="cursor-pointer text-primary" style="background-color: #f8fafc;" onclick="openModalForCount('total', null, 'Total No of files')">${total}</td>
<td id="summary-completed" class="cursor-pointer text-success" onclick="openModalForCount('completed_custom', null, 'Completed Files')">${completedTotal}</td>
<td id="summary-pending" class="cursor-pointer text-danger" style="color: #991b1b; font-weight: bold;" onclick="openModalForCount('pending_custom', null, 'Pending Files')">${pendingTotal}</td>
`;
summaryBodyHtml += `
<td id="summary-1-15" class="cursor-pointer" onclick="openModalForCount('age', [1, 15], 'Age: 1 to 15 Days')">${age_1_15}</td>
<td id="summary-16-30" class="cursor-pointer" onclick="openModalForCount('age', [16, 30], 'Age: 16 to 30 Days')">${age_16_30}</td>
@ -3385,6 +3415,41 @@
}
if (clickType === 'age') {
// Exclude completed files for pending age column clicks
const rowStatusStr = String(row.status || '').trim();
const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
rowStatusStr === String(s.id) ||
rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
);
if (isCompleted) return false;
const [min, max] = clickValue;
if (max !== null) {
if (age < min || age > max) return false;
} else {
if (age < min) return false;
}
}
if (clickType === 'completed_custom') {
// Filter for completed status
const rowStatusStr = String(row.status || '').trim();
const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
rowStatusStr === String(s.id) ||
rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
);
if (!isCompleted) return false;
}
if (clickType === 'completed_age') {
// Filter for completed status
const rowStatusStr = String(row.status || '').trim();
const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
rowStatusStr === String(s.id) ||
rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
);
if (!isCompleted) return false;
const [min, max] = clickValue;
if (max !== null) {
if (age < min || age > max) return false;
@ -3446,6 +3511,34 @@
const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
if (rowVendor !== vendorName) return false;
// Exclude completed files for pending vendor age clicks
const rowStatusStr = String(row.status || '').trim();
const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
rowStatusStr === String(s.id) ||
rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
);
if (isCompleted) return false;
const [min, max] = clickValue;
if (max !== null) {
if (age < min || age > max) return false;
} else {
if (age < min) return false;
}
}
if (clickType === 'vendor_completed_age') {
const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
if (rowVendor !== vendorName) return false;
// Filter for completed status
const rowStatusStr = String(row.status || '').trim();
const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
rowStatusStr === String(s.id) ||
rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
);
if (!isCompleted) return false;
const [min, max] = clickValue;
if (max !== null) {
if (age < min || age > max) return false;
@ -3546,7 +3639,7 @@
}
};
const getTypeBadge = typeVal => {
const getTypeBadge = (typeVal, filePath) => {
const cleanType = String(typeVal || '').trim().toLowerCase();
let label = cleanType;
let badgeClass = 'type-sd';
@ -3575,12 +3668,26 @@
} else {
const typeObj = activeTypes.find(t => String(t.id) === cleanType);
if (typeObj) {
return getTypeBadge(typeObj.type_name);
return getTypeBadge(typeObj.type_name, filePath);
}
label = typeVal || '-';
badgeClass = 'status-default';
}
return `<span class="type-badge ${badgeClass}">${label}</span>`;
let onClickAttr = '';
if (filePath) {
let pdfUrl = filePath;
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
pdfUrl = contextPath + pdfUrl.substring(1);
} else {
pdfUrl = contextPath + pdfUrl;
}
onClickAttr = `onclick="window.open('${escapeHtml(pdfUrl)}', '_blank')"`;
} else {
onClickAttr = `onclick="alert('pdf file not uploaded')"`;
}
return `<span class="type-badge ${badgeClass} cursor-pointer" ${onClickAttr} style="cursor: pointer;" title="${filePath ? 'Click to view PDF' : 'PDF not uploaded'}">${label}</span>`;
};
data.forEach((row, index) => {
@ -3599,7 +3706,7 @@
<td>${text(row.physical_file_no)}</td>
<td>${text(row.receiptno)}</td>
<td>${text(row.subject)}</td>
<td>${getTypeBadge(row.resolved_type || row.type)}</td>
<td>${getTypeBadge(row.resolved_type || row.type, row.file_path)}</td>
<td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td>
<td class="fw-semibold text-dark">&#8377; ${text(row.amount)}</td>
<td>${calculateAgeOfBill(row.received_date)}</td>
@ -3700,6 +3807,53 @@
});
}
// Find Type of File column index to append PDF Status
let typeColIndex = -1;
clonedTable.querySelectorAll('thead th').forEach((th, index) => {
if (th.textContent.trim().toLowerCase() === 'type of file') {
typeColIndex = index;
}
});
if (typeColIndex !== -1) {
// Insert header "PDF Status" right after "Type of File"
const th = document.createElement('th');
th.textContent = 'PDF Status';
th.style.backgroundColor = '#f0fdf4';
th.style.color = '#166534';
const theadTr = clonedTable.querySelector('thead tr');
const nextTh = theadTr.children[typeColIndex + 1];
if (nextTh) {
theadTr.insertBefore(th, nextTh);
} else {
theadTr.appendChild(th);
}
// Insert td for each row
clonedTable.querySelectorAll('tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
const typeCell = cells[typeColIndex];
let pdfStatusText = 'pdf file not uploaded';
if (typeCell) {
const span = typeCell.querySelector('span');
if (span && span.getAttribute('title') && span.getAttribute('title').toLowerCase().includes('click')) {
pdfStatusText = 'PDF Uploaded';
}
}
const td = document.createElement('td');
td.textContent = pdfStatusText;
const nextTd = row.children[typeColIndex + 1];
if (nextTd) {
row.insertBefore(td, nextTd);
} else {
row.appendChild(td);
}
});
}
// Convert to Excel workbook and download
const wb = XLSX.utils.table_to_book(clonedTable, { raw: true, sheet: "Sheet1" });
XLSX.writeFile(wb, filename);
@ -3729,6 +3883,7 @@
<th>eFile No./ Receipt No.</th>
<th>Subject</th>
<th>Type of File</th>
<th>PDF Status</th>
<th>Vendor Name</th>
<th>Amount</th>
<th>Age of Bill (Days)</th>
@ -3813,6 +3968,7 @@
<td>${text(row.receiptno)}</td>
<td>${text(row.subject)}</td>
<td>${getTypeLabel(row.resolved_type || row.type)}</td>
<td>${row.file_path ? 'PDF Uploaded' : 'pdf file not uploaded'}</td>
<td>${text(row.resolved_vendor || row.vendor_name)}</td>
<td>${text(row.amount)}</td>
<td>${calculateAgeOfBill(row.received_date)}</td>
@ -4158,7 +4314,7 @@
}
};
const getTypeBadge = typeVal => {
const getTypeBadge = (typeVal, filePath) => {
const cleanType = String(typeVal || '').trim().toLowerCase();
let label = cleanType;
let badgeClass = 'type-sd';
@ -4187,12 +4343,26 @@
} else {
const typeObj = activeTypes.find(t => String(t.id) === cleanType);
if (typeObj) {
return getTypeBadge(typeObj.type_name);
return getTypeBadge(typeObj.type_name, filePath);
}
label = typeVal || '-';
badgeClass = 'status-default';
}
return `<span class="type-badge ${badgeClass}">${label}</span>`;
let onClickAttr = '';
if (filePath) {
let pdfUrl = filePath;
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
pdfUrl = contextPath + pdfUrl.substring(1);
} else {
pdfUrl = contextPath + pdfUrl;
}
onClickAttr = `onclick="window.open('${escapeHtml(pdfUrl)}', '_blank')"`;
} else {
onClickAttr = `onclick="alert('pdf file not uploaded')"`;
}
return `<span class="type-badge ${badgeClass} cursor-pointer" ${onClickAttr} style="cursor: pointer;" title="${filePath ? 'Click to view PDF' : 'PDF not uploaded'}">${label}</span>`;
};
data.forEach((row, index) => {
@ -4211,7 +4381,7 @@
<td>${text(row.physical_file_no)}</td>
<td>${text(row.receiptno)}</td>
<td>${text(row.subject)}</td>
<td>${getTypeBadge(row.resolved_type || row.type)}</td>
<td>${getTypeBadge(row.resolved_type || row.type, row.file_path)}</td>
<td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td>
<td class="fw-semibold text-dark">&#8377; ${text(row.amount)}</td>
<td>${calculateAgeOfBill(row.received_date)}</td>

116
src/main/resources/templates/gcctonermanagement/login.html

@ -0,0 +1,116 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GCC IT Cell Toner Management - Login</title>
<!-- CSRF Tokens -->
<meta th:if="${_csrf != null}" name="_csrf" th:content="${_csrf.token}" />
<meta th:if="${_csrf != null}" name="_csrf_header" th:content="${_csrf.headerName}" />
<!-- Include CSS -->
<link rel="stylesheet" type="text/css" th:href="@{/assets/css/login-form-styles.css}">
<!-- Awesome Font -->
<link th:href="@{/assets/font-awesome-6.5.2/css/all.min.css}" rel="stylesheet" type="text/css">
</head>
<body>
<div class="container">
<img th:src="@{/assets/header-images/gcc1.png}" alt="Greater Chennai Corporation" class="small-logo">
<h3>GREATER CHENNAI CORPORATION</h3>
<h4>பெருநகர சென்னை மாநகராட்சி</h4>
<br>
<h3>GCC IT Cell Toner Management</h3>
<br>
<h2>Login</h2>
<div id="error-box"
style="color: #c0392b; font-weight: 500; font-size: 0.85rem; margin-bottom: 1.5rem; text-align: center; display: none;">
<i class="fa-solid fa-circle-exclamation"></i> <span id="error-text">Invalid username or password!</span>
</div>
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="input-field">
<input type="text" name="usrName" id="usrName" placeholder="Enter Username" required
autocomplete="username">
<div class="underline"></div>
</div>
<div class="input-field">
<input type="password" name="usrPass" id="usrPass" placeholder="Enter Password" required
autocomplete="current-password">
<div class="underline"></div>
</div>
<input type="submit" value="Login" id="submit-btn">
</form>
</div>
<!-- Footer Fragment -->
<th:block th:replace="~{fragments/footer :: footer}"></th:block>
<!-- Custom JS -->
<script th:inline="javascript">
const contextPath = /*[[@{/}]]*/ '';
</script>
<script>
function handleLogin(event) {
event.preventDefault();
const errorBox = document.getElementById("error-box");
const errorText = document.getElementById("error-text");
const submitBtn = document.getElementById("submit-btn");
errorBox.style.display = "none";
submitBtn.disabled = true;
submitBtn.value = 'Authenticating...';
const username = document.getElementById("usrName").value;
const password = document.getElementById("usrPass").value;
const urlParams = new URLSearchParams();
urlParams.append('usrName', username);
urlParams.append('usrPass', password);
// Fetch CSRF tokens
const csrfToken = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
const csrfHeader = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
const headers = {
'Content-Type': 'application/x-www-form-urlencoded'
};
if (csrfToken && csrfHeader) {
headers[csrfHeader] = csrfToken;
}
fetch(contextPath + 'gcctonermanagement/api/login', {
method: 'POST',
headers: headers,
body: urlParams
})
.then(response => response.json())
.then(data => {
if (data.result) {
sessionStorage.setItem("toner_usrName", data.usrName);
window.location.href = contextPath + 'gcctonermanagement/upload';
} else {
submitBtn.disabled = false;
submitBtn.value = 'Login';
errorText.textContent = data.error || "Authentication failed.";
errorBox.style.display = "block";
}
})
.catch(err => {
console.error("Login Error:", err);
submitBtn.disabled = false;
submitBtn.value = 'Login';
errorText.textContent = "Network error. Please try again later.";
errorBox.style.display = "block";
});
}
</script>
</body>
</html>

670
src/main/resources/templates/gcctonermanagement/upload.html

@ -0,0 +1,670 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<script th:inline="javascript">
(function () {
const currentUser = /*[[${session.toner_usrName}]]*/ null;
if (currentUser && sessionStorage.getItem("toner_usrName") !== currentUser) {
window.location.replace(/*[[@{/gcctonermanagement/login}]]*/ '/gcctonermanagement/login');
}
})();
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GCC Toner Upload Portal</title>
<!-- CSRF Tokens -->
<meta th:if="${_csrf != null}" name="_csrf" th:content="${_csrf.token}" />
<meta th:if="${_csrf != null}" name="_csrf_header" th:content="${_csrf.headerName}" />
<!-- Header Fragment -->
<th:block th:replace="~{fragments/tonerheader :: tonerheader}"></th:block>
<!-- Google Fonts -->
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Poppins:wght@300;400;500;600;700&display=swap"
rel="stylesheet">
<!-- Styles -->
<style>
body {
background-color: #f4f7f6;
font-family: 'Poppins', sans-serif;
color: #333;
}
.main-container {
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.page-title {
font-family: 'Outfit', sans-serif;
font-size: 2rem;
font-weight: 700;
color: #0f5c78;
margin: 0;
display: flex;
align-items: center;
gap: 12px;
}
.btn-back {
background-color: #ffffff;
color: #475569;
font-weight: 600;
border: 1.5px solid #cbd5e1;
padding: 10px 20px;
border-radius: 30px;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-back:hover {
background-color: #f1f5f9;
color: #0f5c78;
border-color: #0f5c78;
}
.upload-card {
background: #ffffff;
border-radius: 20px;
border: 1px solid rgba(15, 92, 120, 0.08);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.04);
padding: 40px;
}
.form-label {
font-size: 0.9rem;
font-weight: 600;
color: #475569;
margin-bottom: 8px;
}
.form-control-custom {
height: 48px;
border-radius: 10px;
border: 1.5px solid #e2e8f0;
padding: 10px 16px;
font-size: 0.95rem;
font-weight: 500;
color: #334155;
background-color: #f8fafc;
transition: all 0.3s ease;
width: 100%;
}
.form-control-custom:focus {
outline: none;
border-color: #0f5c78;
background-color: #ffffff;
box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15);
}
.form-group {
margin-bottom: 24px;
}
.category-select-group {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Drag & Drop Zone */
.dropzone-container {
border: 2px dashed #cbd5e1;
border-radius: 16px;
padding: 40px 20px;
text-align: center;
background-color: #f8fafc;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
margin-bottom: 24px;
}
.dropzone-container:hover,
.dropzone-container.dragover {
border-color: #0f5c78;
background-color: rgba(15, 92, 120, 0.03);
}
.dropzone-icon {
font-size: 3rem;
color: #94a3b8;
margin-bottom: 16px;
transition: color 0.3s ease;
}
.dropzone-container:hover .dropzone-icon {
color: #0f5c78;
}
.dropzone-text {
font-size: 1rem;
color: #475569;
font-weight: 500;
margin-bottom: 8px;
}
.dropzone-hint {
font-size: 0.8rem;
color: #94a3b8;
}
.file-input-hidden {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.selected-file-banner {
background-color: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 10px;
padding: 12px 18px;
margin-bottom: 24px;
display: none;
align-items: center;
justify-content: space-between;
}
.selected-file-details {
display: flex;
align-items: center;
gap: 12px;
color: #166534;
font-size: 0.9rem;
font-weight: 500;
}
.btn-remove-file {
background: none;
border: none;
color: #b91c1c;
font-size: 1.1rem;
cursor: pointer;
}
/* Progress Bar */
.progress-container {
display: none;
margin-bottom: 24px;
}
.progress-bar-wrapper {
background-color: #e2e8f0;
height: 8px;
border-radius: 10px;
overflow: hidden;
}
.progress-bar-fill {
background: linear-gradient(90deg, #0f5c78, #1781a6);
height: 100%;
width: 0%;
transition: width 0.1s ease;
}
.progress-text {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
font-weight: 600;
color: #64748b;
margin-bottom: 6px;
}
.btn-submit-upload {
width: 100%;
height: 50px;
border-radius: 12px;
background: linear-gradient(135deg, #0f5c78, #1781a6);
color: #ffffff;
font-weight: 600;
font-size: 1rem;
border: none;
box-shadow: 0 4px 15px rgba(15, 92, 120, 0.2);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-submit-upload:hover {
background: linear-gradient(135deg, #1781a6, #0f5c78);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(15, 92, 120, 0.3);
}
.btn-submit-upload:disabled {
background: #cbd5e1;
color: #94a3b8;
box-shadow: none;
cursor: not-allowed;
transform: none;
}
.file-upload-card {
border: 2px dashed #cbd5e1;
background-color: #f8fafc;
border-radius: 12px;
padding: 20px 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
height: 120px;
}
.file-upload-card:hover {
border-color: #0f5c78;
background-color: rgba(15, 92, 120, 0.03);
transform: translateY(-2px);
}
.file-upload-card.file-selected {
border-color: #166534;
background-color: #f0fdf4;
}
.upload-card-icon {
font-size: 1.8rem;
color: #94a3b8;
transition: color 0.3s ease;
}
.file-upload-card:hover .upload-card-icon {
color: #0f5c78;
}
.file-upload-card.file-selected .upload-card-icon {
color: #166534;
}
.upload-card-text {
font-size: 0.85rem;
font-weight: 500;
color: #64748b;
word-break: break-all;
max-width: 100%;
padding: 0 4px;
}
.file-upload-card.file-selected .upload-card-text {
color: #166534;
font-weight: 600;
}
</style>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body>
<div class="main-container">
<!-- Header -->
<div class="page-header">
<h1 class="page-title">
<i class="fa-solid fa-cloud-arrow-up text-primary"></i>
Inward Register
</h1>
<div class="header-actions" style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
<!-- User welcome badge -->
<div th:if="${isLoggedIn}" class="user-badge"
style="background-color: #e2e8f0; padding: 8px 16px; border-radius: 20px; font-size: 0.9rem; font-weight: 600; color: #475569; display: flex; align-items: center; gap: 8px;">
<i class="fa-solid fa-circle-user" style="color: #0f5c78; font-size: 1.1rem;"></i>
<span th:text="${usrName}">User</span>
</div>
<a th:href="@{/gcctonermanagement/view}" class="btn-back">
<i class="fa-solid fa-arrow-left"></i>
Back to View Report
</a>
<!-- Logout link -->
<a th:if="${isLoggedIn}" th:href="@{/gcctonermanagement/logout}" class="btn-logout-link"
style="color: #ef4444; font-weight: 600; font-size: 0.95rem; text-decoration: none; display: flex; align-items: center; gap: 6px; transition: color 0.2s ease;"
onmouseover="this.style.color='#b91c1c'" onmouseout="this.style.color='#ef4444'">
<i class="fa-solid fa-right-from-bracket"></i>
Logout
</a>
</div>
</div>
<!-- Upload Card -->
<div class="upload-card">
<form id="upload-form" onsubmit="handleUpload(event)">
<!-- Company Name -->
<div class="form-group mb-4">
<label class="form-label" for="company-name-input">
<i class="fa-solid fa-building me-1 text-primary"></i> Company Name *
</label>
<input type="text" id="company-name-input" required class="form-control-custom"
placeholder="Enter company name" maxlength="30">
</div>
<!-- Printer Model Dropdown -->
<div class="form-group mb-4">
<label class="form-label" for="printer-select">
<i class="fa-solid fa-print me-1 text-primary"></i> Printer Model *
</label>
<select id="printer-select" required class="form-control-custom" onchange="onPrinterChange()">
<option value="">-- Select Printer Model --</option>
</select>
</div>
<!-- Toner Model Dropdown -->
<div class="form-group mb-4">
<label class="form-label" for="toner-select">
<i class="fa-solid fa-droplet me-1 text-primary"></i> Toner Model *
</label>
<select id="toner-select" required class="form-control-custom" disabled>
<option value="">-- Select Toner Model --</option>
</select>
</div>
<!-- Quantity -->
<div class="form-group mb-4">
<label class="form-label" for="qty-input">
<i class="fa-solid fa-list-ol me-1 text-primary"></i> Quantity *
</label>
<input type="number" id="qty-input" required class="form-control-custom"
placeholder="Enter quantity" min="1" step="1">
</div>
<!-- File Upload Options (DC, User Request, Final Delivered) -->
<div class="row">
<!-- DC Upload -->
<div class="col-md-4 form-group mb-4">
<label class="form-label">
<i class="fa-solid fa-file-pdf me-1 text-danger"></i> Delivery Challan (DC)
</label>
<div class="file-upload-card" onclick="document.getElementById('dc-file').click()">
<i class="fa-solid fa-cloud-arrow-up upload-card-icon"></i>
<span class="upload-card-text" id="dc-file-label">Choose PDF</span>
<input type="file" id="dc-file" accept=".pdf" class="d-none"
onchange="handleFileSelect(this, 'dc-file-label')">
</div>
</div>
<!-- User Request Upload -->
<!-- <div class="col-md-4 form-group mb-4">
<label class="form-label">
<i class="fa-solid fa-file-pdf me-1 text-danger"></i> User Request
</label>
<div class="file-upload-card" onclick="document.getElementById('ur-file').click()">
<i class="fa-solid fa-cloud-arrow-up upload-card-icon"></i>
<span class="upload-card-text" id="ur-file-label">Choose PDF</span>
<input type="file" id="ur-file" accept=".pdf" class="d-none"
onchange="handleFileSelect(this, 'ur-file-label')">
</div>
</div> -->
<!-- Final Delivered Upload -->
<!-- <div class="col-md-4 form-group mb-4">
<label class="form-label">
<i class="fa-solid fa-file-pdf me-1 text-danger"></i> Final Delivered Document
</label>
<div class="file-upload-card" onclick="document.getElementById('fd-file').click()">
<i class="fa-solid fa-cloud-arrow-up upload-card-icon"></i>
<span class="upload-card-text" id="fd-file-label">Choose PDF</span>
<input type="file" id="fd-file" accept=".pdf" class="d-none"
onchange="handleFileSelect(this, 'fd-file-label')">
</div>
</div> -->
</div>
<!-- Progress Bar -->
<div class="progress-container mb-4" id="progress-container">
<div class="progress-text">
<span id="upload-status">Saving details...</span>
<span id="progress-percent">0%</span>
</div>
<div class="progress-bar-wrapper">
<div class="progress-bar-fill" id="progress-fill"></div>
</div>
</div>
<!-- Submit Button -->
<button type="submit" class="btn-submit-upload mt-3" id="submit-btn">
<i class="fa-solid fa-paper-plane"></i> Save & Upload Details
</button>
</form>
</div>
</div>
<!-- Footer Fragment -->
<th:block th:replace="~{fragments/footer :: footer}"></th:block>
<script th:inline="javascript">
const contextPath = /*[[@{/}]]*/ '';
document.addEventListener('DOMContentLoaded', () => {
loadPrinters();
});
function loadPrinters() {
fetch(contextPath + 'gcctonermanagement/api/printers')
.then(res => res.json())
.then(printers => {
const select = document.getElementById('printer-select');
select.innerHTML = '<option value="">-- Select Printer Model --</option>';
printers.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.printer_name;
select.appendChild(opt);
});
})
.catch(err => {
console.error("Error loading printers:", err);
showNotification('error', 'Error', 'Failed to load printers.');
});
}
function onPrinterChange() {
const pid = document.getElementById('printer-select').value;
const tonerSelect = document.getElementById('toner-select');
tonerSelect.innerHTML = '<option value="">-- Select Toner Model --</option>';
tonerSelect.disabled = true;
if (!pid) return;
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;
} else {
const opt = document.createElement('option');
opt.value = "";
opt.textContent = "-- No Toners Available --";
tonerSelect.appendChild(opt);
}
})
.catch(err => {
console.error("Error loading toners:", err);
showNotification('error', 'Error', 'Failed to load toner models.');
});
}
function handleFileSelect(input, labelId) {
const label = document.getElementById(labelId);
const card = input.parentElement;
if (input.files && input.files.length > 0) {
const file = input.files[0];
if (!file.name.toLowerCase().endsWith('.pdf')) {
showNotification('warning', 'Invalid File Type', 'Please select a PDF file.');
input.value = '';
card.classList.remove('file-selected');
label.textContent = 'Choose PDF';
return;
}
card.classList.add('file-selected');
label.textContent = file.name;
} else {
card.classList.remove('file-selected');
label.textContent = 'Choose PDF';
}
}
function showNotification(icon, title, text) {
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: icon,
title: title,
text: text,
confirmButtonColor: '#0f5c78'
});
} else {
alert(title + ": " + text);
}
}
function handleUpload(event) {
event.preventDefault();
const companyName = document.getElementById('company-name-input').value.trim();
const pid = document.getElementById('printer-select').value;
const tid = document.getElementById('toner-select').value;
const qty = document.getElementById('qty-input').value.trim();
if (!companyName || !pid || !tid || !qty) {
showNotification('warning', 'Incomplete Form', 'Please fill in all required fields.');
return;
}
const dcInput = document.getElementById('dc-file');
const urInput = document.getElementById('ur-file');
const fdInput = document.getElementById('fd-file');
const formData = new FormData();
formData.append('companyName', companyName);
formData.append('pid', pid);
formData.append('tid', tid);
formData.append('qty', qty);
if (dcInput.files.length > 0) formData.append('dcFile', dcInput.files[0]);
if (urInput.files.length > 0) formData.append('userRequestFile', urInput.files[0]);
if (fdInput.files.length > 0) formData.append('finalDeliveredFile', fdInput.files[0]);
// CSRF security token
const csrfMeta = document.querySelector('meta[name="_csrf"]');
const csrfHeaderMeta = document.querySelector('meta[name="_csrf_header"]');
const xhr = new XMLHttpRequest();
xhr.open('POST', contextPath + 'gcctonermanagement/api/upload', true);
if (csrfMeta && csrfHeaderMeta) {
xhr.setRequestHeader(csrfHeaderMeta.content, csrfMeta.content);
}
// Show progress elements
const progressContainer = document.getElementById('progress-container');
const progressFill = document.getElementById('progress-fill');
const progressPercent = document.getElementById('progress-percent');
const submitBtn = document.getElementById('submit-btn');
progressContainer.style.display = 'block';
submitBtn.disabled = true;
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressFill.style.width = percent + '%';
progressPercent.textContent = percent + '%';
}
});
xhr.onload = function () {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
if (typeof Swal !== 'undefined') {
Swal.fire({
title: 'Success!',
text: response.message || "Details saved successfully!",
icon: 'success',
confirmButtonColor: '#0f5c78'
}).then(() => {
document.getElementById('upload-form').reset();
resetFileCards();
});
} else {
alert("Success: " + (response.message || "Details saved successfully!"));
document.getElementById('upload-form').reset();
resetFileCards();
}
} else {
showNotification('error', 'Upload Failed', response.error || "Failed to record toner details.");
}
} catch (e) {
showNotification('error', 'Error', 'Failed to parse response: ' + e.message);
}
} else {
showNotification('error', 'Error', "An error occurred. HTTP Code: " + xhr.status);
}
progressContainer.style.display = 'none';
submitBtn.disabled = false;
};
xhr.onerror = function () {
showNotification('error', 'Error', "Connection error occurred during submission.");
progressContainer.style.display = 'none';
submitBtn.disabled = false;
};
xhr.send(formData);
}
function resetFileCards() {
const labels = ['dc-file-label', 'ur-file-label', 'fd-file-label'];
labels.forEach(lblId => {
document.getElementById(lblId).textContent = 'Choose PDF';
});
const cards = document.querySelectorAll('.file-upload-card');
cards.forEach(card => {
card.classList.remove('file-selected');
});
document.getElementById('toner-select').disabled = true;
}
</script>
</body>
</html>

957
src/main/resources/templates/home/home.html
File diff suppressed because it is too large
View File

2
src/main/resources/templates/home/view-login-details.html

@ -572,7 +572,7 @@
<!-- Employee Details Modal -->
<div class="modal fade" id="employeeDetailsModal" tabindex="-1" aria-labelledby="employeeDetailsModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-dialog modal-xl modal-dialog-centered" style="max-width: 95%; width: 95%;">
<div class="modal-content"
style="border-radius: 14px; border: none; box-shadow: 0 10px 40px rgba(0,0,0,0.2);">
<div class="modal-custom-header">

Loading…
Cancel
Save