Browse Source

added spring security correctly and payment tracker, toner managemnt UI changes

master
yoganandhank 3 months ago
parent
commit
8fc28ff9af
  1. 4
      pom.xml
  2. 66
      src/main/java/com/gcc_manpower/config/AppUserDetailsService.java
  3. 123
      src/main/java/com/gcc_manpower/config/SecurityConfig.java
  4. 36
      src/main/java/com/gcc_manpower/controller/HomeController.java
  5. 49
      src/main/java/com/gcc_manpower/gccservicedocuments/controller/GccServiceDocumentsController.java
  6. 75
      src/main/java/com/gcc_manpower/gccservicepaymenttracker/controller/GccServicePaymentTrackerController.java
  7. 67
      src/main/java/com/gcc_manpower/gcctonermanagement/controller/TonerController.java
  8. 40
      src/main/java/com/gcc_manpower/gcctonermanagement/service/TonerService.java
  9. 152
      src/main/resources/templates/gccservicepaymenttracker/dashboard.html
  10. 2239
      src/main/resources/templates/gccservicepaymenttracker/details.html
  11. 697
      src/main/resources/templates/gcctonermanagement/upload.html

4
pom.xml

@ -43,8 +43,8 @@
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>

66
src/main/java/com/gcc_manpower/config/AppUserDetailsService.java

@ -0,0 +1,66 @@
package com.gcc_manpower.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.util.List;
import java.util.Map;
@Service
public class AppUserDetailsService implements UserDetailsService {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(@Qualifier("mysqlManpowerDataSource") DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String sql = "SELECT usr_name, usr_pass, usr_role, is_active FROM app_user WHERE usr_name = ?";
List<Map<String, Object>> users;
try {
users = jdbcTemplate.queryForList(sql, username);
} catch (Exception e) {
throw new UsernameNotFoundException("Database error during user retrieval", e);
}
if (users == null || users.isEmpty()) {
throw new UsernameNotFoundException("Invalid username or password!");
}
Map<String, Object> user = users.get(0);
// Check if user is active
int isActive = 1;
Object activeObj = user.get("is_active");
if (activeObj instanceof Number) {
isActive = ((Number) activeObj).intValue();
} else if (activeObj instanceof Boolean) {
isActive = (Boolean) activeObj ? 1 : 0;
}
if (isActive == 0) {
throw new UsernameNotFoundException("User account is inactive!");
}
String password = (String) user.get("usr_pass");
String role = (String) user.get("usr_role");
if (role == null) {
role = "USER";
}
return User.withUsername(username)
.password(password != null ? password : "")
.roles(role.toUpperCase()) // Will be loaded as authority ROLE_ROLE
.build();
}
}

123
src/main/java/com/gcc_manpower/config/SecurityConfig.java

@ -0,0 +1,123 @@
package com.gcc_manpower.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
private final BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
@Override
public String encode(CharSequence rawPassword) {
return bcrypt.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String dbPassword) {
if (dbPassword != null && dbPassword.startsWith("$2a$")) {
return bcrypt.matches(rawPassword, dbPassword);
}
return rawPassword.toString().equals(dbPassword);
}
};
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Configure headers for clickjacking and XSS protection
.headers(headers -> headers
.frameOptions(frame -> frame.sameOrigin())
.xssProtection(
xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)))
// Configure URL authorization
.authorizeHttpRequests(auth -> auth
// Allow static resources
.requestMatchers(
"/assets/**",
"/css/**",
"/js/**",
"/images/**",
"/favicon.ico",
"/error")
.permitAll()
// Allow login pages and login API endpoints
.requestMatchers(
"/login",
"/api/val-app-usr-login",
"/gcctonermanagement/login",
"/gcctonermanagement/api/login",
"/gccservicepaymenttracker/login",
"/gccservicepaymenttracker/api/login",
"/gccservicedocuments/login",
"/gccservicedocuments/api/login")
.permitAll()
// Allow logout endpoints
.requestMatchers(
"/usr-logout",
"/gcctonermanagement/logout",
"/gccservicepaymenttracker/logout",
"/gccservicedocuments/logout")
.permitAll()
// Require authentication for all other requests
.anyRequest().authenticated())
// Configure CSRF protection (ignore CSRF on the login endpoints only)
.csrf(csrf -> csrf
.ignoringRequestMatchers(
"/api/val-app-usr-login",
"/gcctonermanagement/api/login",
"/gccservicepaymenttracker/api/login",
"/gccservicedocuments/api/login"))
// Session management configuration with module-aware invalid session
// redirection
.sessionManagement(session -> session
.invalidSessionStrategy((request, response) -> {
// Create a fresh session so requestedSessionIdValid won't cause endless loop
request.getSession(true);
String requestURI = request.getRequestURI();
if (requestURI.contains("/gccservicepaymenttracker")) {
response.sendRedirect(request.getContextPath() + "/gccservicepaymenttracker/login");
} else if (requestURI.contains("/gcctonermanagement")) {
response.sendRedirect(request.getContextPath() + "/gcctonermanagement/login");
} else if (requestURI.contains("/gccservicedocuments")) {
response.sendRedirect(request.getContextPath() + "/gccservicedocuments/login");
} else {
response.sendRedirect(request.getContextPath() + "/login");
}
}))
// Exception handling for unauthenticated requests with module-aware redirection
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, authException) -> {
String requestURI = request.getRequestURI();
if (requestURI.contains("/gccservicepaymenttracker")) {
response.sendRedirect(request.getContextPath() + "/gccservicepaymenttracker/login");
} else if (requestURI.contains("/gcctonermanagement")) {
response.sendRedirect(request.getContextPath() + "/gcctonermanagement/login");
} else if (requestURI.contains("/gccservicedocuments")) {
response.sendRedirect(request.getContextPath() + "/gccservicedocuments/login");
} else {
response.sendRedirect(request.getContextPath() + "/login");
}
}));
return http.build();
}
}

36
src/main/java/com/gcc_manpower/controller/HomeController.java

@ -19,6 +19,13 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@Controller
@RequestMapping("/")
public class HomeController {
@ -26,6 +33,9 @@ public class HomeController {
@Autowired
private AppUserService appUserService;
@Autowired
private AuthenticationManager authenticationManager;
@Value("${upload.path:gcc_manpower_uploads}")
private String uploadPathConfig;
@ -93,18 +103,29 @@ public class HomeController {
@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("usrName", authResult.get("usrName"));
session.setAttribute("usrRole", authResult.get("usrRole"));
try {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(usrName, usrPass);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
String role = authentication.getAuthorities().stream()
.map(auth -> auth.getAuthority().replace("ROLE_", ""))
.findFirst()
.orElse("USER");
session.setAttribute("usrName", usrName);
session.setAttribute("usrRole", role);
session.setMaxInactiveInterval(600); // 10 minutes session timeout
response.put("result", true);
response.put("usrName", authResult.get("usrName"));
} else {
response.put("usrName", usrName);
} catch (AuthenticationException e) {
response.put("result", false);
response.put("error", authResult.get("error"));
response.put("error", e.getMessage() != null ? e.getMessage() : "Invalid username or password!");
}
return response;
@ -634,6 +655,7 @@ public class HomeController {
@PostMapping("/usr-logout")
public String logout(HttpSession session) {
SecurityContextHolder.clearContext();
session.invalidate();
return "redirect:/login";
}

49
src/main/java/com/gcc_manpower/gccservicedocuments/controller/GccServiceDocumentsController.java

@ -10,11 +10,19 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@Controller
@RequestMapping("/gccservicedocuments")
public class GccServiceDocumentsController {
@ -28,6 +36,9 @@ public class GccServiceDocumentsController {
@Autowired
private AppUserService appUserService;
@Autowired
private AuthenticationManager authenticationManager;
@Value("${document.upload.path:gccservicedocuments}")
private String documentUploadPathConfig;
@ -81,27 +92,45 @@ public class GccServiceDocumentsController {
@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("doc_usrName", authResult.get("usrName"));
session.setAttribute("doc_usrRole", authResult.get("usrRole"));
try {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(usrName,
usrPass);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
String role = authentication.getAuthorities().stream()
.map(auth -> auth.getAuthority().replace("ROLE_", ""))
.findFirst()
.orElse("USER");
session.setAttribute("doc_usrName", usrName);
session.setAttribute("doc_docRole", role); // keep backward compatibility
session.setAttribute("doc_usrRole", role);
session.setMaxInactiveInterval(1800); // 30 minutes session timeout
response.put("result", true);
response.put("usrName", authResult.get("usrName"));
} else {
response.put("usrName", usrName);
} catch (AuthenticationException e) {
response.put("result", false);
response.put("error", authResult.get("error"));
response.put("error", e.getMessage() != null ? e.getMessage() : "Invalid username or password!");
}
return response;
}
@GetMapping("/logout")
public String logout(HttpSession session) {
session.removeAttribute("doc_usrName");
session.removeAttribute("doc_usrRole");
public String logout(HttpSession session, HttpServletRequest request) {
SecurityContextHolder.clearContext();
if (session != null) {
session.removeAttribute("doc_usrName");
session.removeAttribute("doc_usrRole");
session.invalidate();
}
request.getSession(true);
return "redirect:/gccservicedocuments/login";
}

75
src/main/java/com/gcc_manpower/gccservicepaymenttracker/controller/GccServicePaymentTrackerController.java

@ -9,6 +9,7 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import java.io.File;
@ -16,6 +17,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@Controller
@RequestMapping("/gccservicepaymenttracker")
public class GccServicePaymentTrackerController {
@ -26,6 +34,9 @@ public class GccServicePaymentTrackerController {
@Autowired
private AppUserService appUserService;
@Autowired
private AuthenticationManager authenticationManager;
@Value("${payment.tracker.upload.path:gccpaymenttrackerfiles}")
private String paymentTrackerUploadPathConfig;
@ -50,6 +61,23 @@ public class GccServicePaymentTrackerController {
return "gccservicepaymenttracker/dashboard";
}
@GetMapping("/details")
public String viewDetails(HttpSession session, Model model) {
String username = (String) session.getAttribute("pay_usrName");
if (username == null) {
return "redirect:/gccservicepaymenttracker/login";
}
String usrRole = (String) session.getAttribute("pay_usrRole");
boolean isAdmin = "ADMIN".equalsIgnoreCase(usrRole);
model.addAttribute("isLoggedIn", true);
model.addAttribute("isAdmin", isAdmin);
model.addAttribute("usrName", username);
model.addAttribute("curPage", "gccservicepaymenttracker");
return "gccservicepaymenttracker/details";
}
@GetMapping("/login")
public String login(HttpSession session) {
return "gccservicepaymenttracker/login";
@ -62,34 +90,55 @@ public class GccServicePaymentTrackerController {
@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("pay_usrName", authResult.get("usrName"));
session.setAttribute("pay_usrRole", authResult.get("usrRole"));
try {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(usrName, usrPass);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
String role = authentication.getAuthorities().stream()
.map(auth -> auth.getAuthority().replace("ROLE_", ""))
.findFirst()
.orElse("USER");
session.setAttribute("pay_usrName", usrName);
session.setAttribute("pay_usrRole", role);
session.setMaxInactiveInterval(1800); // 30 minutes session timeout
response.put("result", true);
response.put("usrName", authResult.get("usrName"));
} else {
response.put("usrName", usrName);
} catch (AuthenticationException e) {
response.put("result", false);
response.put("error", authResult.get("error"));
response.put("error", e.getMessage() != null ? e.getMessage() : "Invalid username or password!");
}
return response;
}
@PostMapping("/logout")
public String logout(HttpSession session) {
session.removeAttribute("pay_usrName");
session.removeAttribute("pay_usrRole");
public String logout(HttpSession session, HttpServletRequest request) {
SecurityContextHolder.clearContext();
if (session != null) {
session.removeAttribute("pay_usrName");
session.removeAttribute("pay_usrRole");
session.invalidate();
}
request.getSession(true);
return "redirect:/gccservicepaymenttracker/login";
}
@GetMapping("/logout")
public String logoutGet(HttpSession session) {
session.removeAttribute("pay_usrName");
session.removeAttribute("pay_usrRole");
public String logoutGet(HttpSession session, HttpServletRequest request) {
SecurityContextHolder.clearContext();
if (session != null) {
session.removeAttribute("pay_usrName");
session.removeAttribute("pay_usrRole");
session.invalidate();
}
request.getSession(true);
return "redirect:/gccservicepaymenttracker/login";
}

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

@ -18,8 +18,15 @@ 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;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@Controller
@RequestMapping("/gcctonermanagement")
@ -31,6 +38,9 @@ public class TonerController {
@Autowired
private TonerService tonerService;
@Autowired
private AuthenticationManager authenticationManager;
@Value("${toner.upload.path:gcctonermanagementfiles}")
private String tonerUploadPathConfig;
@ -68,27 +78,44 @@ public class TonerController {
@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"));
try {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(usrName,
usrPass);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
String role = authentication.getAuthorities().stream()
.map(auth -> auth.getAuthority().replace("ROLE_", ""))
.findFirst()
.orElse("USER");
session.setAttribute("toner_usrName", usrName);
session.setAttribute("toner_usrRole", role);
session.setMaxInactiveInterval(1800); // 30 minutes session timeout
response.put("result", true);
response.put("usrName", authResult.get("usrName"));
} else {
response.put("usrName", usrName);
} catch (AuthenticationException e) {
response.put("result", false);
response.put("error", authResult.get("error"));
response.put("error", e.getMessage() != null ? e.getMessage() : "Invalid username or password!");
}
return response;
}
@GetMapping("/logout")
public String logoutGet(HttpSession session) {
session.removeAttribute("toner_usrName");
session.removeAttribute("toner_usrRole");
public String logoutGet(HttpSession session, HttpServletRequest request) {
SecurityContextHolder.clearContext();
if (session != null) {
session.removeAttribute("toner_usrName");
session.removeAttribute("toner_usrRole");
session.invalidate();
}
request.getSession(true);
return "redirect:/gcctonermanagement/login";
}
@ -286,6 +313,24 @@ public class TonerController {
return tonerService.getPendingRequests();
}
@GetMapping("/api/inward/list")
@ResponseBody
public List<Map<String, Object>> getInwardListData(HttpSession session) {
if (session.getAttribute("toner_usrName") == null) {
return java.util.Collections.emptyList();
}
return tonerService.getInwardHistory();
}
@GetMapping("/api/requests/list")
@ResponseBody
public List<Map<String, Object>> getUserRequestsListData(HttpSession session) {
if (session.getAttribute("toner_usrName") == null) {
return java.util.Collections.emptyList();
}
return tonerService.getAllUserRequests();
}
@GetMapping("/api/outward/list")
@ResponseBody
public List<Map<String, Object>> getOutwardListData(HttpSession session) {

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

@ -224,12 +224,45 @@ public class TonerService {
}
}
public List<Map<String, Object>> getInwardHistory() {
String sql = "SELECT s.id, s.company_name, s.qty_registered, s.dc_upload, s.cdate, s.order_by, " +
"p.printer_name, t.model_name " +
"FROM stock_register_details s " +
"LEFT JOIN printer_model_list p ON CAST(s.pid AS SIGNED) = p.id " +
"LEFT JOIN toner_model_list t ON CAST(s.tid AS SIGNED) = t.id " +
"WHERE s.is_active = 1 AND s.is_delete = 0 " +
"ORDER BY s.id DESC";
try {
return jdbcTemplate.queryForList(sql);
} catch (Exception e) {
System.err.println("Error fetching inward history: " + e.getMessage());
return new ArrayList<>();
}
}
public List<Map<String, Object>> getAllUserRequests() {
String sql = "SELECT r.id, r.req_no, r.itcell_ref_no, r.dept_id, r.qty_requested, r.qty_delivered, " +
"r.user_request_upload, r.cdate, r.updated_date, r.updated_by, " +
"p.printer_name, t.model_name " +
"FROM department_requested_details r " +
"LEFT JOIN printer_model_list p ON CAST(r.pid AS SIGNED) = p.id " +
"LEFT JOIN toner_model_list t ON CAST(r.tid AS SIGNED) = t.id " +
"WHERE r.is_active = 1 AND r.is_delete = 0 " +
"ORDER BY r.id DESC";
try {
return jdbcTemplate.queryForList(sql);
} catch (Exception e) {
System.err.println("Error fetching user requests: " + e.getMessage());
return new ArrayList<>();
}
}
public List<Map<String, Object>> getOutwardHistory() {
String sql = "SELECT r.id, r.req_no, r.itcell_ref_no, r.dept_id, r.qty_requested, r.qty_delivered, r.final_delivered_upload, r.updated_date, " +
String sql = "SELECT r.id, r.req_no, r.itcell_ref_no, r.dept_id, r.qty_requested, r.qty_delivered, r.final_delivered_upload, r.user_request_upload, r.cdate, r.updated_date, " +
"p.printer_name, t.model_name " +
"FROM department_requested_details r " +
"LEFT JOIN printer_model_list p ON r.pid = p.id " +
"LEFT JOIN toner_model_list t ON r.tid = t.id " +
"LEFT JOIN printer_model_list p ON CAST(r.pid AS SIGNED) = p.id " +
"LEFT JOIN toner_model_list t ON CAST(r.tid AS SIGNED) = t.id " +
"WHERE r.is_active = 1 AND r.is_delete = 0 " +
"AND r.qty_delivered IS NOT NULL AND CAST(r.qty_delivered AS SIGNED) > 0 " +
"ORDER BY r.updated_date DESC";
@ -241,3 +274,4 @@ public class TonerService {
}
}
}

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

@ -1632,11 +1632,7 @@
<button type="button" class="btn btn-sm btn-danger px-3" onclick="submitDeleteRecord()">Delete
Record</button>
</div>
</div>
</div>
</div>
<!-- Count Details Modal -->
<!-- OLD IN-PAGE MODAL (COMMENTED OUT FOR REFERENCE - ALL COUNT METRICS NOW OPEN IN A NEW TAB)
<div class="modal fade" id="countDetailsModal" tabindex="-1" aria-labelledby="countDetailsModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
@ -1649,100 +1645,81 @@
aria-label="Close"></button>
</div>
<div class="modal-body" style="padding: 1.5rem;">
<!-- Filters Row Inside Modal -->
<div class="row g-3 mb-4 p-3 bg-light rounded border align-items-end">
<!-- Quick Type Filters Under Type Filter Label -->
<div class="col-12 mb-2">
<label class="form-label fw-bold text-secondary text-uppercase"
style="font-size: 0.75rem; letter-spacing: 0.5px; display: block; margin-bottom: 8px;">Type
Filter</label>
<div id="modal-type-badges" class="d-flex flex-wrap gap-2">
<!-- Populated dynamically via JS -->
</div>
<!-- Hidden select element to preserve compatibility with existing JS -->
<select id="modal-filter-type" class="d-none">
<div class="mb-3">
<div class="d-flex align-items-center mb-2">
<span class="text-uppercase fw-bold text-muted me-2" style="font-size: 0.75rem; letter-spacing: 0.5px;">Type Filter</span>
</div>
<div id="modal-type-badges" class="d-inline-flex flex-wrap gap-2 align-middle">
</div>
</div>
<div class="mb-4">
<div class="d-flex align-items-center mb-2">
<span class="text-uppercase fw-bold text-muted me-2" style="font-size: 0.75rem; letter-spacing: 0.5px;">Status Filter</span>
</div>
<div id="modal-status-badges" class="d-inline-flex flex-wrap gap-2 align-middle">
</div>
</div>
<div class="row g-3 align-items-end mb-4 p-3 rounded-3" style="background-color: #f8fafc; border: 1px solid #e2e8f0;">
<div class="col-md-3">
<label class="form-label fw-bold text-secondary" style="font-size: 0.82rem;">Type of Bill</label>
<select id="modal-filter-type" class="filter-input" onchange="applyModalFilters()">
<option value="all">- All Types -</option>
</select>
</div>
<!-- Quick Status Filters Under Status Filter Label -->
<div class="col-12 mb-2">
<label class="form-label fw-bold text-secondary text-uppercase"
style="font-size: 0.75rem; letter-spacing: 0.5px; display: block; margin-bottom: 8px;">Status
Filter</label>
<div id="modal-status-badges" class="d-flex flex-wrap gap-2">
<!-- Populated dynamically via JS -->
</div>
<!-- Hidden select element to preserve compatibility with existing JS -->
<select id="modal-filter-status" class="d-none">
<div class="col-md-3">
<label class="form-label fw-bold text-secondary" style="font-size: 0.82rem;">Status</label>
<select id="modal-filter-status" class="filter-input" onchange="applyModalFilters()">
<option value="all">- All Statuses -</option>
</select>
</div>
<div class="col-md-4">
<label for="modal-filter-vendor" class="form-label fw-bold text-secondary text-uppercase"
style="font-size: 0.75rem; letter-spacing: 0.5px;">Vendor Filter</label>
<select id="modal-filter-vendor" class="form-select form-select-sm"
onchange="applyModalFilters()">
<div class="col-md-3">
<label class="form-label fw-bold text-secondary" style="font-size: 0.82rem;">Vendor</label>
<select id="modal-filter-vendor" class="filter-input" onchange="applyModalFilters()">
<option value="all">- All Vendors -</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label fw-bold text-secondary text-uppercase"
style="font-size: 0.75rem; letter-spacing: 0.5px;">Search:</label>
<input type="text" id="modal-table-search" class="form-control form-control-sm"
placeholder="Search in this table..." oninput="applyModalFilters()">
<div class="col-md-3">
<label class="form-label fw-bold text-secondary" style="font-size: 0.82rem;">Search Table</label>
<input type="text" id="modal-table-search" class="filter-input" placeholder="Search bill, vendor, dept..." oninput="applyModalFilters()">
</div>
<div class="col-md-4">
<button
class="btn btn-sm btn-success fw-bold px-3 d-inline-flex align-items-center gap-2 w-100 justify-content-center"
style="border-radius: 8px; height: 31px;" onclick="exportModalTableToExcel()">
<i class="fa-solid fa-file-excel"></i> Export to Excel
</div>
<div class="d-flex justify-content-between align-items-center mb-3 px-1">
<div class="d-flex align-items-center gap-2">
<span class="badge bg-secondary px-3 py-2" id="modal-records-count" style="font-size: 0.85rem; font-weight: 600;">0 Records</span>
</div>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-sm btn-success fw-bold px-3 d-flex align-items-center gap-1" style="border-radius: 6px; font-size: 0.82rem;" onclick="exportModalTableToExcel()">
<i class="fa-solid fa-file-excel"></i> Export Excel
</button>
</div>
</div>
<!-- Responsive Table Inside Modal -->
<div class="table-responsive"
style="max-height: 400px; overflow-y: auto; border: 1px solid #eef2f6;">
<table class="table grid-table table-striped table-hover align-middle mb-0"
id="modal-tracking-table">
<div class="table-responsive" style="max-height: 450px; overflow-y: auto; border: 1px solid #eef2f6;">
<table class="table grid-table table-striped table-hover align-middle mb-0" id="modal-tracking-table">
<thead class="table-light">
<tr>
<th class="text-center" style="width: 50px;">Sr. No.</th>
<th onclick="sortModalTable('received_date')"
style="cursor: pointer; user-select: none;">Received Date <span
id="modal-sort-received_date-icon"><i class="fa-solid fa-sort ms-1"
style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('invoice_date')"
style="cursor: pointer; user-select: none;">Invoice Date <span
id="modal-sort-invoice_date-icon"><i class="fa-solid fa-sort ms-1"
style="opacity: 0.4;"></i></span></th>
<th>Physical File No.</th>
<th>eFile No./ Receipt No.</th>
<th>Subject</th>
<th>Type of File</th>
<th>Vendor Name</th>
<th>Amount</th>
<th>Age of Bill (Days)</th>
<th>Status</th>
<th onclick="sortModalTable('received_date')" style="cursor: pointer; user-select: none;">Received Date <span id="modal-sort-received_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('bill_date')" style="cursor: pointer; user-select: none;">Invoice Date <span id="modal-sort-bill_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('department_name')" style="cursor: pointer; user-select: none;">Department <span id="modal-sort-department_name-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('file_no')" style="cursor: pointer; user-select: none;">Physical File No. <span id="modal-sort-file_no-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('bill_no')" style="cursor: pointer; user-select: none;">eFile No./ Receipt No. <span id="modal-sort-bill_no-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('subject')" style="cursor: pointer; user-select: none;">Subject <span id="modal-sort-subject-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('resolved_type')" style="cursor: pointer; user-select: none;">Type of File <span id="modal-sort-resolved_type-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('vendor_name')" style="cursor: pointer; user-select: none;">Vendor Name <span id="modal-sort-vendor_name-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('amount')" style="cursor: pointer; user-select: none;">Amount <span id="modal-sort-amount-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('age')" style="cursor: pointer; user-select: none;">Age of Bill (Days) <span id="modal-sort-age-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('resolved_status')" style="cursor: pointer; user-select: none;">Status <span id="modal-sort-resolved_status-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th>Remarks</th>
<th onclick="sortModalTable('updated_date')"
style="cursor: pointer; user-select: none;">Last Updated Date <span
id="modal-sort-updated_date-icon"><i class="fa-solid fa-sort ms-1"
style="opacity: 0.4;"></i></span></th>
<th onclick="sortModalTable('updated_date')" style="cursor: pointer; user-select: none;">Last Updated Date <span id="modal-sort-updated_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
<th>Last Updated By</th>
<th class="text-center" style="width: 120px;">Actions</th>
</tr>
</thead>
<tbody id="modal-tracking-tbody">
<!-- Populated dynamically via JS -->
</tbody>
</table>
<div id="modal-no-records-found" class="empty-state text-center py-5" style="display: none;">
<i class="fa-solid fa-folder-open empty-icon"
style="font-size: 2.5rem; color: #cbd5e1; margin-bottom: 10px;"></i>
<i class="fa-solid fa-folder-open empty-icon" style="font-size: 2.5rem; color: #cbd5e1; margin-bottom: 10px;"></i>
<h5>No Records Found</h5>
<p>No payment tracking records match the active criteria.</p>
</div>
@ -1751,6 +1728,10 @@
</div>
</div>
</div>
-->
</div>
</div>
</div>
<!-- Deleted Records Modal -->
<div class="modal fade" id="deletedRecordsModal" tabindex="-1" aria-labelledby="deletedRecordsModalLabel"
@ -3432,6 +3413,8 @@
let activeModalQuickStatus = 'all';
function openModalForCount(clickType, clickValue, displayName, vendorName = null, col = null) {
/*
// ================= OLD IN-PAGE MODAL CODE (COMMENTED OUT FOR REFERENCE) =================
modalClickFilter = { clickType, clickValue, displayName, vendorName, col };
if (clickType === 'category' && clickValue) {
@ -3480,7 +3463,6 @@
// If click was vendor-based
if (vendorName) {
// Find matching option or set value
for (let opt of modalVendorSelect.options) {
if (opt.textContent.trim().toLowerCase() === vendorName.trim().toLowerCase() || opt.value === vendorName) {
modalVendorSelect.value = opt.value;
@ -3497,6 +3479,18 @@
countDetailsModalObj = new bootstrap.Modal(document.getElementById('countDetailsModal'));
}
countDetailsModalObj.show();
// =======================================================================================
*/
// NEW: Open Details View in New Browser Tab
const params = new URLSearchParams();
if (clickType) params.append('clickType', clickType);
if (clickValue) params.append('clickValue', Array.isArray(clickValue) ? clickValue.join(',') : clickValue);
if (displayName) params.append('title', displayName);
if (vendorName) params.append('vendor', vendorName);
if (col) params.append('col', col);
window.open(contextPath + 'gccservicepaymenttracker/details?' + params.toString(), '_blank');
}
function populateModalDropdowns() {
@ -3551,16 +3545,13 @@
'sd': 0,
'emd': 0,
'asd': 0,
'agreement': 0,
'status_not_updated': 0
'agreement': 0
};
clickFilteredRecords.forEach(row => {
const cat = getCategoryCode(row.resolved_type || row.type);
if (cat in counts) {
counts[cat]++;
} else {
counts['status_not_updated']++;
}
});
@ -3570,8 +3561,7 @@
{ key: 'sd', label: 'Security Deposit', color: '#1e40af', textLight: '#dbeafe', bgLight: '#eff6ff' },
{ key: 'emd', label: 'EMD', color: '#6b21a8', textLight: '#f3e8ff', bgLight: '#faf5ff' },
{ key: 'asd', label: 'ASD', color: '#9a3412', textLight: '#ffedd5', bgLight: '#fff7ed' },
{ key: 'agreement', label: 'Agreement', color: '#9d174d', textLight: '#fce7f3', bgLight: '#fdf2f8' },
{ key: 'status_not_updated', label: 'Type Not Updated', color: '#374151', textLight: '#f3f4f6', bgLight: '#f9fafb' }
{ key: 'agreement', label: 'Agreement', color: '#9d174d', textLight: '#fce7f3', bgLight: '#fdf2f8' }
];
let html = '';

2239
src/main/resources/templates/gccservicepaymenttracker/details.html
File diff suppressed because it is too large
View File

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

@ -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();

Loading…
Cancel
Save