Browse Source

added login page for service documents and department filter

master
yoga-agoy 4 months ago
parent
commit
31c2c69069
  1. 125
      src/main/java/com/gcc_manpower/gccservicedocuments/controller/GccServiceDocumentsController.java
  2. 114
      src/main/java/com/gcc_manpower/gccservicedocuments/service/GccServiceDocumentsService.java
  3. 4
      src/main/resources/application.properties
  4. 115
      src/main/resources/templates/gccservicedocuments/login.html
  5. 195
      src/main/resources/templates/gccservicedocuments/upload.html
  6. 105
      src/main/resources/templates/gccservicedocuments/view.html

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

@ -2,6 +2,7 @@ package com.gcc_manpower.gccservicedocuments.controller;
import com.gcc_manpower.gccservicedocuments.service.GccServiceDocumentsService;
import com.gcc_manpower.gccservicedocuments.service.VideoCompressionService;
import com.gcc_manpower.service.AppUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
@ -24,20 +25,24 @@ public class GccServiceDocumentsController {
@Autowired
private VideoCompressionService videoCompressionService;
@Autowired
private AppUserService appUserService;
@Value("${document.upload.path:gccservicedocuments}")
private String documentUploadPathConfig;
@GetMapping({ "", "/" })
public String viewPortal(Model model, HttpSession session) {
// String usrName = (String) session.getAttribute("usrName");
// String usrRole = (String) session.getAttribute("usrRole");
// boolean isLoggedIn = (usrName != null);
// boolean isAdmin = "ADMIN".equalsIgnoreCase(usrRole);
String usrName = (String) session.getAttribute("usrName");
if (usrName == null) {
return "redirect:/gccservicedocuments/login";
}
String usrRole = (String) session.getAttribute("usrRole");
boolean isAdmin = "ADMIN".equalsIgnoreCase(usrRole);
model.addAttribute("isLoggedIn", true);
model.addAttribute("isAdmin", true);
model.addAttribute("usrName", "Guest");
model.addAttribute("isAdmin", isAdmin);
model.addAttribute("usrName", usrName);
model.addAttribute("curPage", "gccservicedocuments");
return "gccservicedocuments/upload";
@ -45,28 +50,69 @@ public class GccServiceDocumentsController {
@GetMapping("/view")
public String viewReportPage(Model model, HttpSession session) {
// String usrName = (String) session.getAttribute("usrName");
// if (usrName == null) {
// return "redirect:/login";
// }
// String usrRole = (String) session.getAttribute("usrRole");
// boolean isAdmin = "ADMIN".equalsIgnoreCase(usrRole);
String usrName = (String) session.getAttribute("usrName");
if (usrName == null) {
return "redirect:/gccservicedocuments/login";
}
String usrRole = (String) session.getAttribute("usrRole");
boolean isAdmin = "ADMIN".equalsIgnoreCase(usrRole);
model.addAttribute("isLoggedIn", true);
model.addAttribute("isAdmin", true);
model.addAttribute("usrName", "Guest");
model.addAttribute("isAdmin", isAdmin);
model.addAttribute("usrName", usrName);
model.addAttribute("curPage", "gccservicedocuments_upload");
return "gccservicedocuments/view";
}
@GetMapping("/login")
public String login(HttpSession session) {
if (session.getAttribute("usrName") != null) {
return "redirect:/gccservicedocuments/view";
}
return "gccservicedocuments/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("usrName", authResult.get("usrName"));
session.setAttribute("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 logout(HttpSession session) {
session.removeAttribute("usrName");
session.removeAttribute("usrRole");
session.invalidate();
return "redirect:/gccservicedocuments/login";
}
@GetMapping("/api/list")
@ResponseBody
public List<Map<String, Object>> listDocuments(
@RequestParam(value = "category", required = false) String category,
@RequestParam(value = "mainCategory", required = false) String mainCategory,
@RequestParam(value = "subCategory", required = false) String subCategory,
@RequestParam(value = "fromDate", required = false) String fromDate,
@RequestParam(value = "toDate", required = false) String toDate) {
return documentService.searchDocuments(category, fromDate, toDate);
return documentService.searchDocuments(mainCategory, subCategory, fromDate, toDate);
}
@GetMapping("/api/categories")
@ -75,6 +121,18 @@ public class GccServiceDocumentsController {
return documentService.getAllCategories();
}
@GetMapping("/api/main-categories")
@ResponseBody
public List<Map<String, Object>> getMainCategories() {
return documentService.getAllMainCategories();
}
@GetMapping("/api/sub-categories")
@ResponseBody
public List<Map<String, Object>> getSubCategories() {
return documentService.getAllSubCategories();
}
@GetMapping("/api/file-types")
@ResponseBody
public List<Map<String, Object>> getFileTypes() {
@ -85,18 +143,19 @@ public class GccServiceDocumentsController {
@ResponseBody
public Map<String, Object> uploadFile(
@RequestParam("title") String title,
@RequestParam("category") String category,
@RequestParam("mainCategory") String mainCategory,
@RequestParam("subCategory") String subCategory,
@RequestParam("fileType") String fileType,
@RequestParam("file") MultipartFile file,
HttpSession session) {
Map<String, Object> response = new HashMap<>();
// String usrName = (String) session.getAttribute("usrName");
// if (usrName == null) {
// response.put("success", false);
// response.put("error", "Unauthorized access! Please log in.");
// return response;
// }
String usrName = (String) session.getAttribute("usrName");
if (usrName == null) {
response.put("success", false);
response.put("error", "Unauthorized access! Please log in.");
return response;
}
if (file.isEmpty()) {
response.put("success", false);
@ -107,7 +166,7 @@ public class GccServiceDocumentsController {
try {
// Clean category name for folder names (letters, numbers, underscores, dashes,
// spaces)
String cleanCategory = category.trim().replaceAll("[^a-zA-Z0-9-_ ]", "").trim();
String cleanCategory = subCategory.trim().replaceAll("[^a-zA-Z0-9-_ ]", "").trim();
if (cleanCategory.isEmpty()) {
cleanCategory = "General";
}
@ -142,7 +201,8 @@ public class GccServiceDocumentsController {
boolean saved = documentService.saveDocument(
title.trim(),
cleanCategory,
mainCategory,
subCategory,
fileType,
fileWebUrl,
originalFilename);
@ -172,12 +232,13 @@ public class GccServiceDocumentsController {
@ResponseBody
public Map<String, Object> deleteFile(@RequestParam("id") Long id, HttpSession session) {
Map<String, Object> response = new HashMap<>();
// String usrName = (String) session.getAttribute("usrName");
// if (usrName == null) {
// response.put("success", false);
// response.put("error", "Unauthorized access! Please log in.");
// return response;
// }
String usrName = (String) session.getAttribute("usrName");
String usrRole = (String) session.getAttribute("usrRole");
if (usrName == null || !"ADMIN".equalsIgnoreCase(usrRole)) {
response.put("success", false);
response.put("error", "Unauthorized access! Admin role required.");
return response;
}
boolean deleted = documentService.deleteDocument(id);
if (deleted) {

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

@ -20,11 +20,11 @@ public class GccServiceDocumentsService {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
private int getOrCreateCategory(String categoryName) {
private int getOrCreateMainCategory(String mainCategoryName) {
// Try parsing as ID first
try {
int id = Integer.parseInt(categoryName.trim());
String checkIdSql = "SELECT id FROM category_list WHERE id = ? AND is_delete = 0";
int id = Integer.parseInt(mainCategoryName.trim());
String checkIdSql = "SELECT id FROM main_category_list WHERE id = ? AND is_delete = 0";
List<Integer> ids = jdbcTemplate.queryForList(checkIdSql, Integer.class, id);
if (!ids.isEmpty()) {
return ids.get(0);
@ -33,27 +33,65 @@ public class GccServiceDocumentsService {
// Not a number, fallback to name matching
}
String selectSql = "SELECT id FROM category_list WHERE category_name = ? AND is_delete = 0";
String selectSql = "SELECT id FROM main_category_list WHERE main_category_name = ? AND is_delete = 0";
try {
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, categoryName.trim());
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, mainCategoryName.trim());
if (!ids.isEmpty()) {
return ids.get(0);
}
} catch (Exception e) {
System.err.println("Error checking category: " + e.getMessage());
System.err.println("Error checking main category: " + e.getMessage());
}
// Insert new category
String insertSql = "INSERT INTO category_list (category_name, is_active, is_delete) VALUES (?, 1, 0)";
// Insert new main category
String insertSql = "INSERT INTO main_category_list (main_category_name, is_active, is_delete) VALUES (?, 1, 0)";
try {
jdbcTemplate.update(insertSql, categoryName.trim());
jdbcTemplate.update(insertSql, mainCategoryName.trim());
// Fetch the generated ID
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, categoryName.trim());
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, mainCategoryName.trim());
if (!ids.isEmpty()) {
return ids.get(0);
}
} catch (Exception e) {
System.err.println("Error creating category: " + e.getMessage());
System.err.println("Error creating main category: " + e.getMessage());
}
return 0;
}
private int getOrCreateSubCategory(String subCategoryName, int mainCategoryId) {
// Try parsing as ID first
try {
int id = Integer.parseInt(subCategoryName.trim());
String checkIdSql = "SELECT id FROM sub_category_list WHERE id = ? AND mid = ? AND is_delete = 0";
List<Integer> ids = jdbcTemplate.queryForList(checkIdSql, Integer.class, id, String.valueOf(mainCategoryId));
if (!ids.isEmpty()) {
return ids.get(0);
}
} catch (NumberFormatException e) {
// Not a number, fallback to name matching
}
String selectSql = "SELECT id FROM sub_category_list WHERE category_name = ? AND mid = ? AND is_delete = 0";
try {
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, subCategoryName.trim(), String.valueOf(mainCategoryId));
if (!ids.isEmpty()) {
return ids.get(0);
}
} catch (Exception e) {
System.err.println("Error checking sub category: " + e.getMessage());
}
// Insert new sub category
String insertSql = "INSERT INTO sub_category_list (category_name, mid, is_active, is_delete) VALUES (?, ?, 1, 0)";
try {
jdbcTemplate.update(insertSql, subCategoryName.trim(), String.valueOf(mainCategoryId));
// Fetch the generated ID
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, subCategoryName.trim(), String.valueOf(mainCategoryId));
if (!ids.isEmpty()) {
return ids.get(0);
}
} catch (Exception e) {
System.err.println("Error creating sub category: " + e.getMessage());
}
return 0;
}
@ -96,16 +134,23 @@ public class GccServiceDocumentsService {
return 0;
}
public boolean saveDocument(String title, String category, String fileType, String filePath,
public boolean saveDocument(String title, String mainCategory, String subCategory, String fileType, String filePath,
String originalFilename) {
int categoryId = getOrCreateCategory(category);
int mainCategoryId = getOrCreateMainCategory(mainCategory);
if (mainCategoryId == 0) {
return false;
}
int subCategoryId = getOrCreateSubCategory(subCategory, mainCategoryId);
if (subCategoryId == 0) {
return false;
}
int fileTypeId = getOrCreateFileType(fileType);
String sql = "INSERT INTO documents_uploads (title, category, file_type, file_path, original_filename, is_active, is_delete) "
+
"VALUES (?, ?, ?, ?, ?, 1, 0)";
try {
int rows = jdbcTemplate.update(sql, title, String.valueOf(categoryId), String.valueOf(fileTypeId), filePath,
int rows = jdbcTemplate.update(sql, title, String.valueOf(subCategoryId), String.valueOf(fileTypeId), filePath,
originalFilename);
return rows > 0;
} catch (Exception e) {
@ -114,19 +159,24 @@ public class GccServiceDocumentsService {
}
}
public List<Map<String, Object>> searchDocuments(String category, String fromDate, String toDate) {
public List<Map<String, Object>> searchDocuments(String mainCategory, String subCategory, String fromDate, String toDate) {
StringBuilder sql = new StringBuilder(
"SELECT d.id, d.title, c.category_name AS category, f.file_type_name AS file_type, d.file_path, d.original_filename, d.upload_date "
"SELECT d.id, d.title, s.category_name AS category, m.main_category_name AS main_category, f.file_type_name AS file_type, d.file_path, d.original_filename, d.upload_date "
+
"FROM documents_uploads d " +
"LEFT JOIN category_list c ON d.category = c.id " +
"LEFT JOIN sub_category_list s ON d.category = s.id " +
"LEFT JOIN main_category_list m ON s.mid = m.id " +
"LEFT JOIN upload_file_types f ON d.file_type = f.id " +
"WHERE d.is_active = 1 AND d.is_delete = 0");
List<Object> params = new ArrayList<>();
if (category != null && !category.trim().isEmpty()) {
sql.append(" AND c.category_name = ?");
params.add(category.trim());
if (mainCategory != null && !mainCategory.trim().isEmpty()) {
sql.append(" AND m.main_category_name = ?");
params.add(mainCategory.trim());
}
if (subCategory != null && !subCategory.trim().isEmpty()) {
sql.append(" AND s.category_name = ?");
params.add(subCategory.trim());
}
if (fromDate != null && !fromDate.trim().isEmpty()) {
sql.append(" AND DATE(d.upload_date) >= ?");
@ -137,7 +187,7 @@ public class GccServiceDocumentsService {
params.add(toDate.trim());
}
sql.append(" ORDER BY d.category ");
sql.append(" ORDER BY m.main_category_name, s.category_name ");
try {
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
@ -159,7 +209,7 @@ public class GccServiceDocumentsService {
}
public List<String> getAllCategories() {
String sql = "SELECT category_name FROM category_list WHERE is_active = 1 AND is_delete = 0 ORDER BY category_name";
String sql = "SELECT category_name FROM sub_category_list WHERE is_active = 1 AND is_delete = 0 ORDER BY category_name";
try {
return jdbcTemplate.queryForList(sql, String.class);
} catch (Exception e) {
@ -168,6 +218,26 @@ public class GccServiceDocumentsService {
}
}
public List<Map<String, Object>> getAllMainCategories() {
String sql = "SELECT id, main_category_name FROM main_category_list WHERE is_active = 1 AND is_delete = 0 ORDER BY main_category_name";
try {
return jdbcTemplate.queryForList(sql);
} catch (Exception e) {
System.err.println("Error fetching main categories: " + e.getMessage());
return new ArrayList<>();
}
}
public List<Map<String, Object>> getAllSubCategories() {
String sql = "SELECT id, category_name, mid FROM sub_category_list WHERE is_active = 1 AND is_delete = 0 ORDER BY category_name";
try {
return jdbcTemplate.queryForList(sql);
} catch (Exception e) {
System.err.println("Error fetching sub categories: " + e.getMessage());
return new ArrayList<>();
}
}
public List<Map<String, Object>> getAllFileTypes() {
String sql = "SELECT id, file_type_name FROM upload_file_types WHERE is_active = 1 AND is_delete = 0 ORDER BY file_type_name";
try {

4
src/main/resources/application.properties

@ -1,5 +1,5 @@
spring.profiles.active=local
#spring.profiles.active=prod
#spring.profiles.active=local
spring.profiles.active=prod
spring.application.name=gcc-manpower
server.servlet.session.timeout=10m

115
src/main/resources/templates/gccservicedocuments/login.html

@ -0,0 +1,115 @@
<!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 Service Documents - 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 Training Services Documents</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 + 'gccservicedocuments/api/login', {
method: 'POST',
headers: headers,
body: urlParams
})
.then(response => response.json())
.then(data => {
if (data.result) {
window.location.href = contextPath + 'gccservicedocuments/view';
} 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>

195
src/main/resources/templates/gccservicedocuments/upload.html

@ -271,10 +271,24 @@
<i class="fa-solid fa-cloud-arrow-up text-primary"></i>
Upload Documents
</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="@{/gccservicedocuments/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="@{/gccservicedocuments/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 -->
@ -288,26 +302,35 @@
placeholder="Enter a descriptive title">
</div>
<!-- File Type & Category Row -->
<!-- File Type, Main Category & Subcategory Row -->
<div class="row">
<div class="col-md-6 form-group">
<div class="col-md-4 form-group">
<label class="form-label" for="type-select">File Type *</label>
<select id="type-select" required class="form-control-custom" onchange="adjustFileAcceptance()">
<option value="">- Select File Type -</option>
</select>
</div>
<div class="col-md-6 form-group category-select-group">
<label class="form-label" for="category-select">Category *</label>
<select id="category-select" required class="form-control-custom"
onchange="toggleCustomCategoryInput()">
<option value="">- Select Category -</option>
<option value="__NEW__">+ Add Custom Category...</option>
<div class="col-md-4 form-group category-select-group">
<label class="form-label" for="main-category-select">Main Category *</label>
<select id="main-category-select" required class="form-control-custom"
onchange="onMainCategoryChange()">
<option value="">- Select Main Category -</option>
<option value="__NEW__">+ Add Custom Main Category...</option>
</select>
<input type="text" id="custom-main-category-input" class="form-control-custom mt-2" style="display: none;"
placeholder="Enter custom main category name">
</div>
<!-- Dynamic Custom Category Write-In -->
<input type="text" id="custom-category-input" class="form-control-custom" style="display: none;"
placeholder="Enter custom category name">
<div class="col-md-4 form-group category-select-group">
<label class="form-label" for="subcategory-select">Subcategory *</label>
<select id="subcategory-select" required class="form-control-custom"
onchange="onSubCategoryChange()">
<option value="">- Select Subcategory -</option>
<option value="__NEW__">+ Add Custom Subcategory...</option>
</select>
<input type="text" id="custom-subcategory-input" class="form-control-custom mt-2" style="display: none;"
placeholder="Enter custom subcategory name">
</div>
</div>
@ -358,26 +381,109 @@
const contextPath = /*[[@{/}]]*/ '';
let selectedFile = null;
let allSubCategories = [];
document.addEventListener('DOMContentLoaded', () => {
loadExistingCategories();
loadCategoriesAndSubcategories();
loadFileTypes();
setupDragAndDrop();
});
function loadExistingCategories() {
fetch(contextPath + 'gccservicedocuments/api/categories')
function loadCategoriesAndSubcategories() {
// Load main categories
fetch(contextPath + 'gccservicedocuments/api/main-categories')
.then(res => res.json())
.then(categories => {
const select = document.getElementById('category-select');
// Add items before the last child option (+ Add Custom Category...)
categories.forEach(cat => {
.then(mainCats => {
const select = document.getElementById('main-category-select');
// Add items before the last child option (+ Add Custom Main Category...)
mainCats.forEach(cat => {
const opt = document.createElement('option');
opt.value = cat;
opt.textContent = cat;
opt.value = cat.id;
opt.textContent = cat.main_category_name;
select.insertBefore(opt, select.lastElementChild);
});
})
.catch(err => console.error("Error loading categories:", err));
.catch(err => console.error("Error loading main categories:", err));
// Load subcategories
fetch(contextPath + 'gccservicedocuments/api/sub-categories')
.then(res => res.json())
.then(subCats => {
allSubCategories = subCats;
onMainCategoryChange();
})
.catch(err => console.error("Error loading subcategories:", err));
}
function onMainCategoryChange() {
const mainSelect = document.getElementById('main-category-select');
const subSelect = document.getElementById('subcategory-select');
const customMainInput = document.getElementById('custom-main-category-input');
const customSubInput = document.getElementById('custom-subcategory-input');
subSelect.innerHTML = '<option value="">- Select Subcategory -</option>';
subSelect.disabled = true;
if (mainSelect.value === '__NEW__') {
customMainInput.style.display = 'block';
customMainInput.required = true;
customMainInput.focus();
const optNew = document.createElement('option');
optNew.value = '__NEW__';
optNew.textContent = '+ Add Custom Subcategory...';
subSelect.appendChild(optNew);
subSelect.value = '__NEW__';
subSelect.disabled = false;
customSubInput.style.display = 'block';
customSubInput.required = true;
} else if (mainSelect.value === '') {
customMainInput.style.display = 'none';
customMainInput.required = false;
customMainInput.value = '';
customSubInput.style.display = 'none';
customSubInput.required = false;
customSubInput.value = '';
} else {
customMainInput.style.display = 'none';
customMainInput.required = false;
customMainInput.value = '';
const selectedMid = mainSelect.value;
const filtered = allSubCategories.filter(sub => String(sub.mid) === String(selectedMid));
filtered.forEach(sub => {
const opt = document.createElement('option');
opt.value = sub.id;
opt.textContent = sub.category_name;
subSelect.appendChild(opt);
});
const optNew = document.createElement('option');
optNew.value = '__NEW__';
optNew.textContent = '+ Add Custom Subcategory...';
subSelect.appendChild(optNew);
subSelect.disabled = false;
onSubCategoryChange();
}
}
function onSubCategoryChange() {
const subSelect = document.getElementById('subcategory-select');
const customSubInput = document.getElementById('custom-subcategory-input');
if (subSelect.value === '__NEW__') {
customSubInput.style.display = 'block';
customSubInput.required = true;
customSubInput.focus();
} else {
customSubInput.style.display = 'none';
customSubInput.required = false;
customSubInput.value = '';
}
}
function loadFileTypes() {
@ -420,21 +526,6 @@
}
}
function toggleCustomCategoryInput() {
const select = document.getElementById('category-select');
const customInput = document.getElementById('custom-category-input');
if (select.value === '__NEW__') {
customInput.style.display = 'block';
customInput.required = true;
customInput.focus();
} else {
customInput.style.display = 'none';
customInput.required = false;
customInput.value = '';
}
}
function setupDragAndDrop() {
const dropzone = document.getElementById('dropzone');
@ -553,20 +644,31 @@
const title = document.getElementById('title-input').value.trim();
const type = document.getElementById('type-select').value;
const categorySelect = document.getElementById('category-select').value;
const customCategory = document.getElementById('custom-category-input').value.trim();
const mainSelect = document.getElementById('main-category-select').value;
const subSelect = document.getElementById('subcategory-select').value;
const customMain = document.getElementById('custom-main-category-input').value.trim();
const customSub = document.getElementById('custom-subcategory-input').value.trim();
let mainCategory = mainSelect;
if (mainSelect === '__NEW__') {
if (!customMain) {
alert("Please specify a custom main category name.");
return;
}
mainCategory = customMain;
}
let category = categorySelect;
if (categorySelect === '__NEW__') {
if (!customCategory) {
alert("Please specify a custom category name.");
let subCategory = subSelect;
if (subSelect === '__NEW__') {
if (!customSub) {
alert("Please specify a custom subcategory name.");
return;
}
category = customCategory;
subCategory = customSub;
}
if (!category) {
alert("Please select or type a category.");
if (!mainCategory || !subCategory) {
alert("Please select or specify both Main Category and Subcategory.");
return;
}
@ -577,7 +679,8 @@
const formData = new FormData();
formData.append('title', title);
formData.append('fileType', type);
formData.append('category', category);
formData.append('mainCategory', mainCategory);
formData.append('subCategory', subCategory);
formData.append('file', selectedFile);
const xhr = new XMLHttpRequest();

105
src/main/resources/templates/gccservicedocuments/view.html

@ -571,11 +571,25 @@
<i class="fa-solid fa-folder-open text-primary"></i>
Service Documents & Videos
</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>
<!-- Show upload portal only if user is logged in -->
<a th:if="${isLoggedIn}" th:href="@{/gccservicedocuments}" class="btn-upload-portal">
<i class="fa-solid fa-cloud-arrow-up"></i>
Upload Documents
</a>
<!-- Logout link -->
<a th:if="${isLoggedIn}" th:href="@{/gccservicedocuments/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>
<!-- Filter Card -->
@ -587,11 +601,19 @@
<input type="text" id="search-title" class="filter-input" placeholder="Search by title...">
</div>
<!-- Category Filter -->
<!-- Main Category Filter -->
<div class="filter-group">
<label class="filter-label" for="filter-category">Category</label>
<select id="filter-category" class="filter-input">
<option value="">- All Categories -</option>
<label class="filter-label" for="filter-main-category">Main Category</label>
<select id="filter-main-category" class="filter-input" onchange="onMainCategoryFilterChange()">
<option value="">- All Main Categories -</option>
</select>
</div>
<!-- Subcategory Filter -->
<div class="filter-group">
<label class="filter-label" for="filter-subcategory">Subcategory</label>
<select id="filter-subcategory" class="filter-input">
<option value="">- All Subcategories -</option>
</select>
</div>
@ -655,6 +677,7 @@
<tr>
<th style="width: 60px;" class="text-center">S.No</th>
<th>Title</th>
<th>Subcategory</th>
<th>File Type</th>
<th>Upload Date</th>
<th>Original Filename</th>
@ -734,19 +757,56 @@
});
}
let allFilterSubcategories = [];
function loadCategories() {
fetch(contextPath + 'gccservicedocuments/api/categories')
fetch(contextPath + 'gccservicedocuments/api/main-categories')
.then(response => response.json())
.then(categories => {
const select = document.getElementById('filter-category');
categories.forEach(category => {
.then(mainCats => {
const select = document.getElementById('filter-main-category');
mainCats.forEach(cat => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
option.value = cat.main_category_name;
option.setAttribute('data-id', cat.id);
option.textContent = cat.main_category_name;
select.appendChild(option);
});
})
.catch(err => console.error("Error loading categories:", err));
.catch(err => console.error("Error loading main categories:", err));
fetch(contextPath + 'gccservicedocuments/api/sub-categories')
.then(response => response.json())
.then(subCats => {
allFilterSubcategories = subCats;
populateSubcategoryFilter();
})
.catch(err => console.error("Error loading subcategories:", err));
}
function populateSubcategoryFilter(filteredSubs) {
const select = document.getElementById('filter-subcategory');
select.innerHTML = '<option value="">- All Subcategories -</option>';
const subs = filteredSubs || allFilterSubcategories;
subs.forEach(sub => {
const option = document.createElement('option');
option.value = sub.category_name;
option.textContent = sub.category_name;
select.appendChild(option);
});
}
function onMainCategoryFilterChange() {
const mainSelect = document.getElementById('filter-main-category');
const selectedOption = mainSelect.options[mainSelect.selectedIndex];
const mainId = selectedOption ? selectedOption.getAttribute('data-id') : null;
if (!mainId) {
populateSubcategoryFilter();
} else {
const filtered = allFilterSubcategories.filter(sub => String(sub.mid) === String(mainId));
populateSubcategoryFilter(filtered);
}
}
function loadDocuments() {
@ -763,7 +823,7 @@
function groupAndRenderCategories(documents) {
const grouped = {};
documents.forEach(doc => {
const cat = doc.category || 'General';
const cat = doc.main_category || 'General';
if (!grouped[cat]) {
grouped[cat] = [];
}
@ -852,7 +912,8 @@
function applyFilters() {
const searchQuery = document.getElementById('search-title').value.toLowerCase().trim();
const categoryQuery = document.getElementById('filter-category').value;
const mainCategoryQuery = document.getElementById('filter-main-category').value;
const subCategoryQuery = document.getElementById('filter-subcategory').value;
const fromDateVal = document.getElementById('filter-from-date').value.trim();
const toDateVal = document.getElementById('filter-to-date').value.trim();
@ -860,7 +921,7 @@
const toDateQuery = parseInputDate(toDateVal);
// Validation logic: if no filter is selected, show Swal warning
if (!searchQuery && !categoryQuery && !fromDateVal && !toDateVal) {
if (!searchQuery && !mainCategoryQuery && !subCategoryQuery && !fromDateVal && !toDateVal) {
Swal.fire({
title: 'Filter Selection Required',
text: 'Please select at least one filter before searching.',
@ -888,8 +949,11 @@
// Title filter
const matchesTitle = doc.title.toLowerCase().includes(searchQuery);
// Category filter
const matchesCategory = !categoryQuery || doc.category === categoryQuery;
// Main Category filter
const matchesMainCategory = !mainCategoryQuery || doc.main_category === mainCategoryQuery;
// Subcategory filter
const matchesSubCategory = !subCategoryQuery || doc.category === subCategoryQuery;
// Date filter
let matchesDate = true;
@ -903,7 +967,7 @@
}
}
return matchesTitle && matchesCategory && matchesDate;
return matchesTitle && matchesMainCategory && matchesSubCategory && matchesDate;
});
groupAndRenderCategories(filteredDocuments);
@ -915,7 +979,9 @@
showLoadingOverlay("Resetting Filters...");
setTimeout(() => {
document.getElementById('search-title').value = '';
document.getElementById('filter-category').value = '';
document.getElementById('filter-main-category').value = '';
document.getElementById('filter-subcategory').value = '';
populateSubcategoryFilter();
if (fromPicker) fromPicker.clear();
if (toPicker) toPicker.clear();
filteredDocuments = allDocuments;
@ -928,7 +994,7 @@
currentModalCategoryName = categoryName;
document.getElementById('modal-category-name').textContent = categoryName;
currentCategoryFiles = filteredDocuments.filter(d => d.category === categoryName);
currentCategoryFiles = filteredDocuments.filter(d => (d.main_category || 'General') === categoryName);
document.getElementById('modal-file-search-input').value = '';
const modalElement = document.getElementById('categoryFilesModal');
@ -989,6 +1055,7 @@
tr.innerHTML = `
<td class="text-center fw-bold text-secondary">${index + 1}</td>
<td class="fw-bold text-dark">${escapeHtml(doc.title)}</td>
<td class="text-secondary">${escapeHtml(doc.category || 'General')}</td>
<td>
<span class="badge ${isVideo ? 'bg-danger-subtle text-danger' : 'bg-info-subtle text-info'} border">
<i class="${isVideo ? 'fa-regular fa-file-video' : 'fa-regular fa-file-pdf'}"></i>

Loading…
Cancel
Save