32 changed files with 2279 additions and 2 deletions
-
BINgcc_manpower_uploads/workorderfiles/1011 WO.pdf
-
BINgcc_manpower_uploads/workorderfiles/485 - 2025 WO 1.pdf
-
BINgcc_manpower_uploads/workorderfiles/618 Extension WO.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C111802025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C114642025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C11702026.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C12182025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C124102025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C12472026.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C126592025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C12852026.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C132302025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C136782025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C137392025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C139982025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C145902023.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C14852025.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C15962026.pdf
-
BINgcc_manpower_uploads/workorderfiles/I.T.C.C.No.C16182025.pdf
-
BINgcc_manpower_uploads/workorderfiles/WO (4).pdf
-
7pom.xml
-
2src/main/java/com/gcc_manpower/GccManpowerApplication.java
-
10src/main/java/com/gcc_manpower/config/DataSourceConfig.java
-
8src/main/java/com/gcc_manpower/config/WebConfig.java
-
192src/main/java/com/gcc_manpower/controller/GccServiceDocumentController.java
-
180src/main/java/com/gcc_manpower/service/GccServiceDocumentService.java
-
98src/main/java/com/gcc_manpower/service/VideoCompressionService.java
-
4src/main/resources/application-local.properties
-
4src/main/resources/application-prod.properties
-
22src/main/resources/templates/fragments/documentsheader.html
-
637src/main/resources/templates/gccservicedocuments/upload.html
-
1115src/main/resources/templates/gccservicedocuments/view.html
@ -0,0 +1,192 @@ |
|||
package com.gcc_manpower.controller; |
|||
|
|||
import com.gcc_manpower.service.GccServiceDocumentService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Controller; |
|||
import org.springframework.ui.Model; |
|||
import org.springframework.web.bind.annotation.*; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
import jakarta.servlet.http.HttpSession; |
|||
import java.io.File; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
@Controller |
|||
@RequestMapping("/gccservicedocuments") |
|||
public class GccServiceDocumentController { |
|||
|
|||
@Autowired |
|||
private GccServiceDocumentService documentService; |
|||
|
|||
@Autowired |
|||
private com.gcc_manpower.service.VideoCompressionService videoCompressionService; |
|||
|
|||
@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); |
|||
|
|||
model.addAttribute("isLoggedIn", true); |
|||
model.addAttribute("isAdmin", true); |
|||
model.addAttribute("usrName", "Guest"); |
|||
model.addAttribute("curPage", "gccservicedocuments"); |
|||
|
|||
return "gccservicedocuments/upload"; |
|||
} |
|||
|
|||
@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); |
|||
|
|||
model.addAttribute("isLoggedIn", true); |
|||
model.addAttribute("isAdmin", true); |
|||
model.addAttribute("usrName", "Guest"); |
|||
model.addAttribute("curPage", "gccservicedocuments_upload"); |
|||
|
|||
return "gccservicedocuments/view"; |
|||
} |
|||
|
|||
@GetMapping("/api/list") |
|||
@ResponseBody |
|||
public List<Map<String, Object>> listDocuments( |
|||
@RequestParam(value = "category", required = false) String category, |
|||
@RequestParam(value = "fromDate", required = false) String fromDate, |
|||
@RequestParam(value = "toDate", required = false) String toDate) { |
|||
return documentService.searchDocuments(category, fromDate, toDate); |
|||
} |
|||
|
|||
@GetMapping("/api/categories") |
|||
@ResponseBody |
|||
public List<String> getCategories() { |
|||
return documentService.getAllCategories(); |
|||
} |
|||
|
|||
@GetMapping("/api/file-types") |
|||
@ResponseBody |
|||
public List<Map<String, Object>> getFileTypes() { |
|||
return documentService.getAllFileTypes(); |
|||
} |
|||
|
|||
@PostMapping("/api/upload") |
|||
@ResponseBody |
|||
public Map<String, Object> uploadFile( |
|||
@RequestParam("title") String title, |
|||
@RequestParam("category") String category, |
|||
@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; |
|||
// } |
|||
|
|||
if (file.isEmpty()) { |
|||
response.put("success", false); |
|||
response.put("error", "Please select a file to upload."); |
|||
return response; |
|||
} |
|||
|
|||
try { |
|||
// Clean category name for folder names (letters, numbers, underscores, dashes, |
|||
// spaces) |
|||
String cleanCategory = category.trim().replaceAll("[^a-zA-Z0-9-_ ]", "").trim(); |
|||
if (cleanCategory.isEmpty()) { |
|||
cleanCategory = "General"; |
|||
} |
|||
|
|||
// Build the folder directory path: [documentUploadPathConfig]/[cleanCategory]/ |
|||
File uploadDir = new File(documentUploadPathConfig, cleanCategory).getAbsoluteFile(); |
|||
if (!uploadDir.exists()) { |
|||
uploadDir.mkdirs(); |
|||
} |
|||
|
|||
// Generate unique filename to avoid overwrites |
|||
String originalFilename = file.getOriginalFilename(); |
|||
String nameWithoutExt = ""; |
|||
String ext = ""; |
|||
if (originalFilename != null && originalFilename.contains(".")) { |
|||
int dotIdx = originalFilename.lastIndexOf("."); |
|||
nameWithoutExt = originalFilename.substring(0, dotIdx); |
|||
ext = originalFilename.substring(dotIdx); |
|||
} else { |
|||
nameWithoutExt = originalFilename != null ? originalFilename : "file"; |
|||
} |
|||
|
|||
nameWithoutExt = nameWithoutExt.replaceAll("[^a-zA-Z0-9-_]", "_"); |
|||
String savedFilename = nameWithoutExt + "_" + System.currentTimeMillis() + ext; |
|||
|
|||
File destFile = new File(uploadDir, savedFilename).getAbsoluteFile(); |
|||
file.transferTo(destFile); |
|||
|
|||
// Save Web URL in database: |
|||
// "/gccservicedocuments/files/[category]/[savedFilename]" |
|||
String fileWebUrl = "/gccservicedocuments/files/" + cleanCategory + "/" + savedFilename; |
|||
|
|||
boolean saved = documentService.saveDocument( |
|||
title.trim(), |
|||
cleanCategory, |
|||
fileType, |
|||
fileWebUrl, |
|||
originalFilename); |
|||
|
|||
if (saved) { |
|||
if ("video".equalsIgnoreCase(fileType)) { |
|||
videoCompressionService.compressVideoAsync(destFile); |
|||
} |
|||
response.put("success", true); |
|||
response.put("message", "File uploaded and registered successfully."); |
|||
} else { |
|||
// Remove saved file on DB insert failure |
|||
destFile.delete(); |
|||
response.put("success", false); |
|||
response.put("error", "Failed to record document details in the database."); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
response.put("success", false); |
|||
response.put("error", "File upload failed: " + e.getMessage()); |
|||
} |
|||
|
|||
return response; |
|||
} |
|||
|
|||
@PostMapping("/api/delete") |
|||
@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; |
|||
// } |
|||
|
|||
boolean deleted = documentService.deleteDocument(id); |
|||
if (deleted) { |
|||
response.put("success", true); |
|||
response.put("message", "File record removed successfully."); |
|||
} else { |
|||
response.put("success", false); |
|||
response.put("error", "Failed to delete the file record."); |
|||
} |
|||
|
|||
return response; |
|||
} |
|||
} |
|||
@ -0,0 +1,180 @@ |
|||
package com.gcc_manpower.service; |
|||
|
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
import javax.sql.DataSource; |
|||
import jakarta.annotation.PostConstruct; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
@Service |
|||
public class GccServiceDocumentService { |
|||
|
|||
private JdbcTemplate jdbcTemplate; |
|||
|
|||
@Autowired |
|||
public void setDataSource(@Qualifier("mysqlServiceDocumentsDataSource") DataSource dataSource) { |
|||
this.jdbcTemplate = new JdbcTemplate(dataSource); |
|||
} |
|||
|
|||
private int getOrCreateCategory(String categoryName) { |
|||
// 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"; |
|||
List<Integer> ids = jdbcTemplate.queryForList(checkIdSql, Integer.class, id); |
|||
if (!ids.isEmpty()) { |
|||
return ids.get(0); |
|||
} |
|||
} catch (NumberFormatException e) { |
|||
// Not a number, fallback to name matching |
|||
} |
|||
|
|||
String selectSql = "SELECT id FROM category_list WHERE category_name = ? AND is_delete = 0"; |
|||
try { |
|||
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, categoryName.trim()); |
|||
if (!ids.isEmpty()) { |
|||
return ids.get(0); |
|||
} |
|||
} catch (Exception e) { |
|||
System.err.println("Error checking category: " + e.getMessage()); |
|||
} |
|||
|
|||
// Insert new category |
|||
String insertSql = "INSERT INTO category_list (category_name, is_active, is_delete) VALUES (?, 1, 0)"; |
|||
try { |
|||
jdbcTemplate.update(insertSql, categoryName.trim()); |
|||
// Fetch the generated ID |
|||
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, categoryName.trim()); |
|||
if (!ids.isEmpty()) { |
|||
return ids.get(0); |
|||
} |
|||
} catch (Exception e) { |
|||
System.err.println("Error creating category: " + e.getMessage()); |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
private int getOrCreateFileType(String fileTypeName) { |
|||
// Try parsing as ID first |
|||
try { |
|||
int id = Integer.parseInt(fileTypeName.trim()); |
|||
String checkIdSql = "SELECT id FROM upload_file_types WHERE id = ? AND is_delete = 0"; |
|||
List<Integer> ids = jdbcTemplate.queryForList(checkIdSql, Integer.class, id); |
|||
if (!ids.isEmpty()) { |
|||
return ids.get(0); |
|||
} |
|||
} catch (NumberFormatException e) { |
|||
// Not a number, fallback to name matching |
|||
} |
|||
|
|||
String selectSql = "SELECT id FROM upload_file_types WHERE file_type_name = ? AND is_delete = 0"; |
|||
try { |
|||
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, fileTypeName.trim()); |
|||
if (!ids.isEmpty()) { |
|||
return ids.get(0); |
|||
} |
|||
} catch (Exception e) { |
|||
System.err.println("Error checking file type: " + e.getMessage()); |
|||
} |
|||
|
|||
// Insert new file type |
|||
String insertSql = "INSERT INTO upload_file_types (file_type_name, is_active, is_delete) VALUES (?, 1, 0)"; |
|||
try { |
|||
jdbcTemplate.update(insertSql, fileTypeName.trim()); |
|||
// Fetch the generated ID |
|||
List<Integer> ids = jdbcTemplate.queryForList(selectSql, Integer.class, fileTypeName.trim()); |
|||
if (!ids.isEmpty()) { |
|||
return ids.get(0); |
|||
} |
|||
} catch (Exception e) { |
|||
System.err.println("Error creating file type: " + e.getMessage()); |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
public boolean saveDocument(String title, String category, String fileType, String filePath, |
|||
String originalFilename) { |
|||
int categoryId = getOrCreateCategory(category); |
|||
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, |
|||
originalFilename); |
|||
return rows > 0; |
|||
} catch (Exception e) { |
|||
System.err.println("Error saving service document: " + e.getMessage()); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public List<Map<String, Object>> searchDocuments(String category, 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 " |
|||
+ |
|||
"FROM documents_uploads d " + |
|||
"LEFT JOIN category_list c ON d.category = c.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 (fromDate != null && !fromDate.trim().isEmpty()) { |
|||
sql.append(" AND DATE(d.upload_date) >= ?"); |
|||
params.add(fromDate.trim()); |
|||
} |
|||
if (toDate != null && !toDate.trim().isEmpty()) { |
|||
sql.append(" AND DATE(d.upload_date) <= ?"); |
|||
params.add(toDate.trim()); |
|||
} |
|||
|
|||
sql.append(" ORDER BY d.category "); |
|||
|
|||
try { |
|||
return jdbcTemplate.queryForList(sql.toString(), params.toArray()); |
|||
} catch (Exception e) { |
|||
System.err.println("Error searching service documents: " + e.getMessage()); |
|||
return new ArrayList<>(); |
|||
} |
|||
} |
|||
|
|||
public boolean deleteDocument(Long id) { |
|||
String sql = "UPDATE documents_uploads SET is_active = 0, is_delete = 1 WHERE id = ?"; |
|||
try { |
|||
int rows = jdbcTemplate.update(sql, id); |
|||
return rows > 0; |
|||
} catch (Exception e) { |
|||
System.err.println("Error deleting service document ID " + id + ": " + e.getMessage()); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public List<String> getAllCategories() { |
|||
String sql = "SELECT category_name FROM category_list WHERE is_active = 1 AND is_delete = 0 ORDER BY category_name"; |
|||
try { |
|||
return jdbcTemplate.queryForList(sql, String.class); |
|||
} catch (Exception e) { |
|||
System.err.println("Error fetching document 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 { |
|||
return jdbcTemplate.queryForList(sql); |
|||
} catch (Exception e) { |
|||
System.err.println("Error fetching file types: " + e.getMessage()); |
|||
return new ArrayList<>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
package com.gcc_manpower.service; |
|||
|
|||
import org.springframework.scheduling.annotation.Async; |
|||
import org.springframework.stereotype.Service; |
|||
import ws.schild.jave.Encoder; |
|||
import ws.schild.jave.MultimediaObject; |
|||
import ws.schild.jave.encode.AudioAttributes; |
|||
import ws.schild.jave.encode.EncodingAttributes; |
|||
import ws.schild.jave.encode.VideoAttributes; |
|||
import ws.schild.jave.info.AudioInfo; |
|||
|
|||
import java.io.File; |
|||
import java.util.logging.Level; |
|||
import java.util.logging.Logger; |
|||
|
|||
@Service |
|||
public class VideoCompressionService { |
|||
|
|||
private static final Logger LOGGER = Logger.getLogger(VideoCompressionService.class.getName()); |
|||
|
|||
@Async |
|||
public void compressVideoAsync(File originalFile) { |
|||
if (originalFile == null || !originalFile.exists() || !originalFile.isFile()) { |
|||
LOGGER.warning("Invalid file provided for compression."); |
|||
return; |
|||
} |
|||
|
|||
String originalPath = originalFile.getAbsolutePath(); |
|||
long originalSize = originalFile.length(); |
|||
LOGGER.info("Starting background video compression for file: " + originalPath + " (Size: " + (originalSize / (1024 * 1024)) + " MB)"); |
|||
|
|||
// Create a temporary file in the same directory for the compressed output |
|||
File tempFile = new File(originalFile.getParentFile(), "temp_" + System.currentTimeMillis() + "_" + originalFile.getName()); |
|||
|
|||
try { |
|||
// Read input file details |
|||
MultimediaObject mediaObject = new MultimediaObject(originalFile); |
|||
AudioInfo audioInfo = mediaObject.getInfo().getAudio(); |
|||
|
|||
// Set up video encoding attributes |
|||
VideoAttributes video = new VideoAttributes(); |
|||
video.setCodec("h264"); // Standard widely supported web codec |
|||
video.setBitRate(1500000); // Target 1.5 Mbps for compression |
|||
video.setFrameRate(24); |
|||
|
|||
EncodingAttributes attrs = new EncodingAttributes(); |
|||
attrs.setOutputFormat("mp4"); |
|||
attrs.setVideoAttributes(video); |
|||
|
|||
// Set up audio encoding attributes if audio stream exists |
|||
if (audioInfo != null) { |
|||
AudioAttributes audio = new AudioAttributes(); |
|||
audio.setCodec("aac"); |
|||
audio.setBitRate(128000); // Target 128 kbps audio |
|||
audio.setChannels(2); |
|||
audio.setSamplingRate(44100); |
|||
attrs.setAudioAttributes(audio); |
|||
} |
|||
|
|||
// Encode/Compress |
|||
Encoder encoder = new Encoder(); |
|||
encoder.encode(mediaObject, tempFile, attrs); |
|||
|
|||
// Replace original file with compressed file if successful |
|||
if (tempFile.exists() && tempFile.length() > 0) { |
|||
long compressedSize = tempFile.length(); |
|||
|
|||
// Delete original file |
|||
boolean deletedOriginal = originalFile.delete(); |
|||
if (!deletedOriginal) { |
|||
LOGGER.warning("Failed to delete the original uncompressed file. Trying to force replace."); |
|||
} |
|||
|
|||
// Rename temp file to original file name |
|||
boolean renamed = tempFile.renameTo(originalFile); |
|||
if (renamed) { |
|||
long sizeReduction = originalSize - compressedSize; |
|||
LOGGER.info("Compression complete! Saved to: " + originalPath); |
|||
LOGGER.info(String.format("File size reduced by %.2f%% (from %d MB to %d MB)", |
|||
((double) sizeReduction / originalSize) * 100, |
|||
originalSize / (1024 * 1024), |
|||
compressedSize / (1024 * 1024))); |
|||
} else { |
|||
LOGGER.severe("Failed to rename temp compressed file to original filename: " + originalPath); |
|||
} |
|||
} else { |
|||
LOGGER.severe("Compression failed: Temp file was not created or is empty."); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
LOGGER.log(Level.SEVERE, "Exception occurred during video compression for file: " + originalPath, e); |
|||
// Cleanup temp file in case of failure |
|||
if (tempFile.exists()) { |
|||
tempFile.delete(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<th:block th:fragment="documentsheader"> |
|||
<!-- Awesome Font --> |
|||
<link rel="stylesheet" th:href="@{/assets/font-awesome-6.5.2/css/all.min.css}" type="text/css"> |
|||
<!-- Bootstrap CSS --> |
|||
<link rel="stylesheet" th:href="@{/assets/bootstrap-5.3.2/css/bootstrap.min.css}" type="text/css"> |
|||
<!-- CSS for Header --> |
|||
<link rel="stylesheet" th:href="@{/assets/css/header-styles.css}" type="text/css"> |
|||
<div class="header"> |
|||
<div class="logo-left"> |
|||
<img th:src="@{/assets/header-images/gcc1.png}" alt="Greater Chennai Corporation Logo"> |
|||
<div class="logo-text"> |
|||
<h2>GREATER</h2> |
|||
<h2>CHENNAI</h2> |
|||
<h2>CORPORATION</h2> |
|||
<p>பெருநகர சென்னை மாநகராட்சி</p> |
|||
</div> |
|||
</div> |
|||
<div class="title"> |
|||
<h2>GCC Training Material Services Details</h2> |
|||
</div> |
|||
</div> |
|||
</th:block> |
|||
@ -0,0 +1,637 @@ |
|||
<!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 Documents Upload Portal</title> |
|||
|
|||
<!-- CSRF Tokens --> |
|||
<meta th:if="${_csrf != null}" name="_csrf" th:content="${_csrf.token}" /> |
|||
<meta th:if="${_csrf != null}" name="_csrf_header" th:content="${_csrf.headerName}" /> |
|||
|
|||
<!-- Header Fragment --> |
|||
<th:block th:replace="~{fragments/documentsheader :: documentsheader}"></th:block> |
|||
|
|||
<!-- Google Fonts --> |
|||
<link |
|||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Poppins:wght@300;400;500;600;700&display=swap" |
|||
rel="stylesheet"> |
|||
|
|||
<!-- Styles --> |
|||
<style> |
|||
body { |
|||
background-color: #f4f7f6; |
|||
font-family: 'Poppins', sans-serif; |
|||
color: #333; |
|||
} |
|||
|
|||
.main-container { |
|||
max-width: 800px; |
|||
margin: 40px auto; |
|||
padding: 0 20px; |
|||
} |
|||
|
|||
.page-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 30px; |
|||
} |
|||
|
|||
.page-title { |
|||
font-family: 'Outfit', sans-serif; |
|||
font-size: 2rem; |
|||
font-weight: 700; |
|||
color: #0f5c78; |
|||
margin: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
} |
|||
|
|||
.btn-back { |
|||
background-color: #ffffff; |
|||
color: #475569; |
|||
font-weight: 600; |
|||
border: 1.5px solid #cbd5e1; |
|||
padding: 10px 20px; |
|||
border-radius: 30px; |
|||
transition: all 0.3s ease; |
|||
text-decoration: none; |
|||
display: inline-flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.btn-back:hover { |
|||
background-color: #f1f5f9; |
|||
color: #0f5c78; |
|||
border-color: #0f5c78; |
|||
} |
|||
|
|||
.upload-card { |
|||
background: #ffffff; |
|||
border-radius: 20px; |
|||
border: 1px solid rgba(15, 92, 120, 0.08); |
|||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.04); |
|||
padding: 40px; |
|||
} |
|||
|
|||
.form-label { |
|||
font-size: 0.9rem; |
|||
font-weight: 600; |
|||
color: #475569; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.form-control-custom { |
|||
height: 48px; |
|||
border-radius: 10px; |
|||
border: 1.5px solid #e2e8f0; |
|||
padding: 10px 16px; |
|||
font-size: 0.95rem; |
|||
font-weight: 500; |
|||
color: #334155; |
|||
background-color: #f8fafc; |
|||
transition: all 0.3s ease; |
|||
width: 100%; |
|||
} |
|||
|
|||
.form-control-custom:focus { |
|||
outline: none; |
|||
border-color: #0f5c78; |
|||
background-color: #ffffff; |
|||
box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15); |
|||
} |
|||
|
|||
.form-group { |
|||
margin-bottom: 24px; |
|||
} |
|||
|
|||
.category-select-group { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 12px; |
|||
} |
|||
|
|||
/* Drag & Drop Zone */ |
|||
.dropzone-container { |
|||
border: 2px dashed #cbd5e1; |
|||
border-radius: 16px; |
|||
padding: 40px 20px; |
|||
text-align: center; |
|||
background-color: #f8fafc; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease; |
|||
position: relative; |
|||
margin-bottom: 24px; |
|||
} |
|||
|
|||
.dropzone-container:hover, |
|||
.dropzone-container.dragover { |
|||
border-color: #0f5c78; |
|||
background-color: rgba(15, 92, 120, 0.03); |
|||
} |
|||
|
|||
.dropzone-icon { |
|||
font-size: 3rem; |
|||
color: #94a3b8; |
|||
margin-bottom: 16px; |
|||
transition: color 0.3s ease; |
|||
} |
|||
|
|||
.dropzone-container:hover .dropzone-icon { |
|||
color: #0f5c78; |
|||
} |
|||
|
|||
.dropzone-text { |
|||
font-size: 1rem; |
|||
color: #475569; |
|||
font-weight: 500; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.dropzone-hint { |
|||
font-size: 0.8rem; |
|||
color: #94a3b8; |
|||
} |
|||
|
|||
.file-input-hidden { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
opacity: 0; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.selected-file-banner { |
|||
background-color: #f0fdf4; |
|||
border: 1px solid #bbf7d0; |
|||
border-radius: 10px; |
|||
padding: 12px 18px; |
|||
margin-bottom: 24px; |
|||
display: none; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.selected-file-details { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
color: #166534; |
|||
font-size: 0.9rem; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.btn-remove-file { |
|||
background: none; |
|||
border: none; |
|||
color: #b91c1c; |
|||
font-size: 1.1rem; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
/* Progress Bar */ |
|||
.progress-container { |
|||
display: none; |
|||
margin-bottom: 24px; |
|||
} |
|||
|
|||
.progress-bar-wrapper { |
|||
background-color: #e2e8f0; |
|||
height: 8px; |
|||
border-radius: 10px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.progress-bar-fill { |
|||
background: linear-gradient(90deg, #0f5c78, #1781a6); |
|||
height: 100%; |
|||
width: 0%; |
|||
transition: width 0.1s ease; |
|||
} |
|||
|
|||
.progress-text { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
font-size: 0.85rem; |
|||
font-weight: 600; |
|||
color: #64748b; |
|||
margin-bottom: 6px; |
|||
} |
|||
|
|||
.btn-submit-upload { |
|||
width: 100%; |
|||
height: 50px; |
|||
border-radius: 12px; |
|||
background: linear-gradient(135deg, #0f5c78, #1781a6); |
|||
color: #ffffff; |
|||
font-weight: 600; |
|||
font-size: 1rem; |
|||
border: none; |
|||
box-shadow: 0 4px 15px rgba(15, 92, 120, 0.2); |
|||
cursor: pointer; |
|||
transition: all 0.3s ease; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.btn-submit-upload:hover { |
|||
background: linear-gradient(135deg, #1781a6, #0f5c78); |
|||
transform: translateY(-2px); |
|||
box-shadow: 0 6px 20px rgba(15, 92, 120, 0.3); |
|||
} |
|||
|
|||
.btn-submit-upload:disabled { |
|||
background: #cbd5e1; |
|||
color: #94a3b8; |
|||
box-shadow: none; |
|||
cursor: not-allowed; |
|||
transform: none; |
|||
} |
|||
</style> |
|||
</head> |
|||
|
|||
<body> |
|||
|
|||
<!-- Navigation Fragment --> |
|||
<!-- <th:block th:replace="~{fragments/navbar :: navbar(curPage=${curPage}, isAdmin=${isAdmin})}"></th:block> --> |
|||
|
|||
<div class="main-container"> |
|||
|
|||
<!-- Header --> |
|||
<div class="page-header"> |
|||
<h1 class="page-title"> |
|||
<i class="fa-solid fa-cloud-arrow-up text-primary"></i> |
|||
Upload Documents |
|||
</h1> |
|||
<a th:href="@{/gccservicedocuments/view}" class="btn-back"> |
|||
<i class="fa-solid fa-arrow-left"></i> |
|||
Back to View Report |
|||
</a> |
|||
</div> |
|||
|
|||
<!-- Upload Card --> |
|||
<div class="upload-card"> |
|||
<form id="upload-form" onsubmit="handleUpload(event)"> |
|||
|
|||
<!-- Title --> |
|||
<div class="form-group"> |
|||
<label class="form-label" for="title-input">Document / Video Title *</label> |
|||
<input type="text" id="title-input" required class="form-control-custom" |
|||
placeholder="Enter a descriptive title"> |
|||
</div> |
|||
|
|||
<!-- File Type & Category Row --> |
|||
<div class="row"> |
|||
<div class="col-md-6 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> |
|||
</select> |
|||
|
|||
<!-- 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> |
|||
</div> |
|||
|
|||
<!-- Dropzone Area --> |
|||
<div class="dropzone-container" id="dropzone"> |
|||
<i class="fa-solid fa-cloud-arrow-up dropzone-icon"></i> |
|||
<p class="dropzone-text" id="dropzone-msg">Drag and drop file here or click to browse</p> |
|||
<p class="dropzone-hint" id="dropzone-hint">Allowed documents: PDF, Word, Excel, TXT</p> |
|||
<input type="file" id="file-chooser" class="file-input-hidden" onchange="onFileSelected()"> |
|||
</div> |
|||
|
|||
<!-- Selected File Banner --> |
|||
<div class="selected-file-banner" id="file-banner"> |
|||
<div class="selected-file-details"> |
|||
<i class="fa-solid fa-circle-check"></i> |
|||
<span id="file-name-display">filename.pdf</span> |
|||
<span id="file-size-display" style="opacity: 0.7; font-size: 0.8rem;">(0 MB)</span> |
|||
</div> |
|||
<button type="button" class="btn-remove-file" onclick="clearSelectedFile()" title="Remove file"> |
|||
<i class="fa-solid fa-circle-xmark"></i> |
|||
</button> |
|||
</div> |
|||
|
|||
<!-- Progress Bar --> |
|||
<div class="progress-container" id="progress-container"> |
|||
<div class="progress-text"> |
|||
<span id="upload-status">Uploading file...</span> |
|||
<span id="progress-percent">0%</span> |
|||
</div> |
|||
<div class="progress-bar-wrapper"> |
|||
<div class="progress-bar-fill" id="progress-fill"></div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Submit Button --> |
|||
<button type="submit" class="btn-submit-upload" id="submit-btn" disabled> |
|||
<i class="fa-solid fa-upload"></i> Start Upload |
|||
</button> |
|||
</form> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<!-- Footer Fragment --> |
|||
<th:block th:replace="~{fragments/footer :: footer}"></th:block> |
|||
|
|||
<script th:inline="javascript"> |
|||
const contextPath = /*[[@{/}]]*/ ''; |
|||
let selectedFile = null; |
|||
|
|||
document.addEventListener('DOMContentLoaded', () => { |
|||
loadExistingCategories(); |
|||
loadFileTypes(); |
|||
setupDragAndDrop(); |
|||
}); |
|||
|
|||
function loadExistingCategories() { |
|||
fetch(contextPath + 'gccservicedocuments/api/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 => { |
|||
const opt = document.createElement('option'); |
|||
opt.value = cat; |
|||
opt.textContent = cat; |
|||
select.insertBefore(opt, select.lastElementChild); |
|||
}); |
|||
}) |
|||
.catch(err => console.error("Error loading categories:", err)); |
|||
} |
|||
|
|||
function loadFileTypes() { |
|||
fetch(contextPath + 'gccservicedocuments/api/file-types') |
|||
.then(res => res.json()) |
|||
.then(fileTypes => { |
|||
const select = document.getElementById('type-select'); |
|||
select.innerHTML = '<option value="">- Select File Type -</option>'; |
|||
fileTypes.forEach(ft => { |
|||
const opt = document.createElement('option'); |
|||
opt.value = ft.id; |
|||
opt.textContent = ft.file_type_name; |
|||
opt.setAttribute('data-name', ft.file_type_name.toLowerCase()); |
|||
select.appendChild(opt); |
|||
}); |
|||
adjustFileAcceptance(); |
|||
}) |
|||
.catch(err => console.error("Error loading file types:", err)); |
|||
} |
|||
|
|||
function adjustFileAcceptance() { |
|||
const typeSelect = document.getElementById('type-select'); |
|||
const fileChooser = document.getElementById('file-chooser'); |
|||
const hint = document.getElementById('dropzone-hint'); |
|||
|
|||
const selectedOption = typeSelect.options[typeSelect.selectedIndex]; |
|||
const fileTypeName = selectedOption ? selectedOption.getAttribute('data-name') : ''; |
|||
|
|||
if (fileTypeName === 'video') { |
|||
fileChooser.accept = 'video/mp4,video/webm,video/ogg,video/quicktime'; |
|||
hint.textContent = 'Allowed videos: MP4, WebM, OGG, MOV'; |
|||
} else { |
|||
fileChooser.accept = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt'; |
|||
hint.textContent = 'Allowed documents: PDF, Word, Excel, PowerPoint, TXT'; |
|||
} |
|||
|
|||
// If a file was already selected, clear it to prevent invalid file types from submitting |
|||
if (selectedFile) { |
|||
clearSelectedFile(); |
|||
} |
|||
} |
|||
|
|||
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'); |
|||
|
|||
['dragenter', 'dragover'].forEach(eventName => { |
|||
dropzone.addEventListener(eventName, (e) => { |
|||
e.preventDefault(); |
|||
dropzone.classList.add('dragover'); |
|||
}, false); |
|||
}); |
|||
|
|||
['dragleave', 'drop'].forEach(eventName => { |
|||
dropzone.addEventListener(eventName, (e) => { |
|||
e.preventDefault(); |
|||
dropzone.classList.remove('dragover'); |
|||
}, false); |
|||
}); |
|||
|
|||
dropzone.addEventListener('drop', (e) => { |
|||
const dt = e.dataTransfer; |
|||
const files = dt.files; |
|||
if (files.length > 0) { |
|||
document.getElementById('file-chooser').files = files; |
|||
onFileSelected(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function validateFileExtension(file, fileTypeName) { |
|||
if (!fileTypeName) return false; |
|||
const fileName = file.name.toLowerCase(); |
|||
const dotIdx = fileName.lastIndexOf('.'); |
|||
if (dotIdx === -1) return false; |
|||
const extension = fileName.substring(dotIdx); |
|||
|
|||
if (fileTypeName === 'video') { |
|||
const allowed = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']; |
|||
return allowed.includes(extension); |
|||
} else if (fileTypeName === 'pdf') { |
|||
return extension === '.pdf'; |
|||
} else if (fileTypeName === 'word') { |
|||
return extension === '.doc' || extension === '.docx'; |
|||
} else if (fileTypeName === 'excel') { |
|||
return extension === '.xls' || extension === '.xlsx'; |
|||
} else if (fileTypeName === 'presentation') { |
|||
return extension === '.ppt' || extension === '.pptx'; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
function onFileSelected() { |
|||
const fileInput = document.getElementById('file-chooser'); |
|||
if (fileInput.files.length > 0) { |
|||
const file = fileInput.files[0]; |
|||
const typeSelect = document.getElementById('type-select'); |
|||
const selectedOption = typeSelect.options[typeSelect.selectedIndex]; |
|||
const fileTypeName = selectedOption ? selectedOption.getAttribute('data-name') : ''; |
|||
|
|||
if (!typeSelect.value) { |
|||
if (typeof Swal !== 'undefined') { |
|||
Swal.fire({ |
|||
title: 'Select File Type', |
|||
text: 'Please select a File Type first before choosing a file.', |
|||
icon: 'warning', |
|||
confirmButtonColor: '#0f5c78' |
|||
}); |
|||
} else { |
|||
alert("Please select a File Type first before choosing a file."); |
|||
} |
|||
fileInput.value = ''; |
|||
return; |
|||
} |
|||
|
|||
if (!validateFileExtension(file, fileTypeName)) { |
|||
const typeLabel = selectedOption.textContent; |
|||
if (typeof Swal !== 'undefined') { |
|||
Swal.fire({ |
|||
title: 'Invalid File Format', |
|||
text: `The selected file does not match the chosen File Type (${typeLabel}). Please choose a valid file.`, |
|||
icon: 'error', |
|||
confirmButtonColor: '#0f5c78' |
|||
}); |
|||
} else { |
|||
alert(`The selected file does not match the chosen File Type (${typeLabel}). Please choose a valid file.`); |
|||
} |
|||
fileInput.value = ''; |
|||
return; |
|||
} |
|||
|
|||
selectedFile = file; |
|||
|
|||
// Show banner |
|||
document.getElementById('file-name-display').textContent = selectedFile.name; |
|||
document.getElementById('file-size-display').textContent = '(' + (selectedFile.size / (1024 * 1024)).toFixed(2) + ' MB)'; |
|||
|
|||
document.getElementById('dropzone').style.display = 'none'; |
|||
document.getElementById('file-banner').style.display = 'flex'; |
|||
document.getElementById('submit-btn').disabled = false; |
|||
} |
|||
} |
|||
|
|||
function clearSelectedFile() { |
|||
document.getElementById('file-chooser').value = ''; |
|||
selectedFile = null; |
|||
document.getElementById('dropzone').style.display = 'block'; |
|||
document.getElementById('file-banner').style.display = 'none'; |
|||
document.getElementById('submit-btn').disabled = true; |
|||
} |
|||
|
|||
function handleUpload(event) { |
|||
event.preventDefault(); |
|||
|
|||
if (!selectedFile) { |
|||
alert("Please select a file."); |
|||
return; |
|||
} |
|||
|
|||
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(); |
|||
|
|||
let category = categorySelect; |
|||
if (categorySelect === '__NEW__') { |
|||
if (!customCategory) { |
|||
alert("Please specify a custom category name."); |
|||
return; |
|||
} |
|||
category = customCategory; |
|||
} |
|||
|
|||
if (!category) { |
|||
alert("Please select or type a category."); |
|||
return; |
|||
} |
|||
|
|||
// CSRF security token |
|||
const csrfMeta = document.querySelector('meta[name="_csrf"]'); |
|||
const csrfHeaderMeta = document.querySelector('meta[name="_csrf_header"]'); |
|||
|
|||
const formData = new FormData(); |
|||
formData.append('title', title); |
|||
formData.append('fileType', type); |
|||
formData.append('category', category); |
|||
formData.append('file', selectedFile); |
|||
|
|||
const xhr = new XMLHttpRequest(); |
|||
xhr.open('POST', contextPath + 'gccservicedocuments/api/upload', true); |
|||
|
|||
if (csrfMeta && csrfHeaderMeta) { |
|||
xhr.setRequestHeader(csrfHeaderMeta.content, csrfMeta.content); |
|||
} |
|||
|
|||
// Show progress elements |
|||
const progressContainer = document.getElementById('progress-container'); |
|||
const progressFill = document.getElementById('progress-fill'); |
|||
const progressPercent = document.getElementById('progress-percent'); |
|||
const submitBtn = document.getElementById('submit-btn'); |
|||
|
|||
progressContainer.style.display = 'block'; |
|||
submitBtn.disabled = true; |
|||
|
|||
xhr.upload.addEventListener('progress', (e) => { |
|||
if (e.lengthComputable) { |
|||
const percent = Math.round((e.loaded / e.total) * 100); |
|||
progressFill.style.width = percent + '%'; |
|||
progressPercent.textContent = percent + '%'; |
|||
} |
|||
}); |
|||
|
|||
xhr.onload = function () { |
|||
if (xhr.status === 200) { |
|||
const response = JSON.parse(xhr.responseText); |
|||
if (response.success) { |
|||
alert(response.message || "Upload completed successfully!"); |
|||
// Redirect back to main page |
|||
window.location.href = contextPath + 'gccservicedocuments/view'; |
|||
} else { |
|||
alert("Upload failed: " + response.error); |
|||
progressContainer.style.display = 'none'; |
|||
submitBtn.disabled = false; |
|||
} |
|||
} else { |
|||
alert("An error occurred during file upload. Code: " + xhr.status); |
|||
progressContainer.style.display = 'none'; |
|||
submitBtn.disabled = false; |
|||
} |
|||
}; |
|||
|
|||
xhr.onerror = function () { |
|||
alert("Connection error occurred while uploading file."); |
|||
progressContainer.style.display = 'none'; |
|||
submitBtn.disabled = false; |
|||
}; |
|||
|
|||
xhr.send(formData); |
|||
} |
|||
</script> |
|||
</body> |
|||
|
|||
</html> |
|||
1115
src/main/resources/templates/gccservicedocuments/view.html
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue