fix: FilePreview fileType case + Tailwind v4 gradient transparent bug

- FilePreview.vue: add normalizedFileType computed to handle backend
  returning uppercase HTML/MD/PPTX (fixes preview/download buttons)
- FilePreview.vue: bg-gradient-to-r from-orange-500 -> bg-orange-500
  (Tailwind v4 gradient + CSS variable = transparent)
- ReportCard.vue: bg-gradient-to-r -> bg-orange-600 for selected state
- Add .opencode/, node_modules/, dist/ to .gitignore
- Initial git setup for publish project
This commit is contained in:
2026-05-24 20:09:42 +08:00
commit b9137204a0
78 changed files with 12950 additions and 0 deletions
@@ -0,0 +1,12 @@
package com.reportdist;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DailyReportDistributionApplication {
public static void main(String[] args) {
SpringApplication.run(DailyReportDistributionApplication.class, args);
}
}
@@ -0,0 +1,24 @@
package com.reportdist.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:./uploads/");
}
}
@@ -0,0 +1,57 @@
package com.reportdist.controller;
import com.reportdist.dto.ProjectRequest;
import com.reportdist.dto.ProjectResponse;
import com.reportdist.service.ProjectService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@RestController
@RequestMapping("/api/projects")
@CrossOrigin(origins = "*")
public class ProjectController {
private final ProjectService projectService;
public ProjectController(ProjectService projectService) {
this.projectService = projectService;
}
@GetMapping
public ResponseEntity<List<ProjectResponse>> getAllProjects() {
return ResponseEntity.ok(projectService.getAllProjects());
}
@GetMapping("/{id}")
public ResponseEntity<ProjectResponse> getProjectById(@PathVariable Long id) {
return ResponseEntity.ok(projectService.getProjectById(id));
}
@PostMapping
public ResponseEntity<ProjectResponse> createProject(@Valid @RequestBody ProjectRequest request) {
ProjectResponse response = projectService.createProject(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ProjectResponse> updateProject(
@PathVariable Long id,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "description", required = false) String description,
@RequestParam(value = "coverImage", required = false) MultipartFile coverImage) {
ProjectResponse response = projectService.updateProject(id, name, description, coverImage);
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProject(@PathVariable Long id) {
projectService.deleteProject(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,135 @@
package com.reportdist.controller;
import com.reportdist.dto.ReportRequest;
import com.reportdist.dto.ReportResponse;
import com.reportdist.service.ReportService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@RestController
@RequestMapping("/api/reports")
@CrossOrigin(origins = "*")
public class ReportController {
private final ReportService reportService;
public ReportController(ReportService reportService) {
this.reportService = reportService;
}
@GetMapping("/ping")
public ResponseEntity<?> ping() {
return ResponseEntity.ok(java.util.Map.of("version", "v4-diag", "timestamp", java.time.Instant.now().toString()));
}
@GetMapping
public ResponseEntity<?> getAllReports(@RequestParam(required = false) Long projectId) {
return ResponseEntity.ok(reportService.getAllReports(projectId));
}
@GetMapping("/{id}")
public ResponseEntity<ReportResponse> getReportById(@PathVariable Long id) {
return ResponseEntity.ok(reportService.getReportById(id));
}
@GetMapping("/{id}/preview")
public ResponseEntity<byte[]> previewReport(@PathVariable Long id) {
ReportResponse report = reportService.getReportById(id);
String contentType = "application/octet-stream";
switch (report.getFileType().toLowerCase()) {
case "html": contentType = "text/html; charset=utf-8"; break;
case "md": contentType = "text/markdown; charset=utf-8"; break;
case "pdf": contentType = "application/pdf"; break;
case "pptx": contentType = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; break;
case "ppt": contentType = "application/vnd.ms-powerpoint"; break;
}
byte[] bytes = reportService.getReportBytes(id);
return ResponseEntity.ok()
.header("Content-Type", contentType)
.header("Content-Disposition", "inline; filename=\"" + report.getFileName() + "\"")
.body(bytes);
}
@GetMapping("/{id}/download")
public ResponseEntity<byte[]> downloadReport(@PathVariable Long id) {
ReportResponse report = reportService.getReportById(id);
byte[] bytes = reportService.getReportBytes(id);
return ResponseEntity.ok()
.header("Content-Type", "application/octet-stream")
.header("Content-Disposition", "attachment; filename=\"" + report.getFileName() + "\"")
.body(bytes);
}
@GetMapping("/{id}/pdf")
public ResponseEntity<byte[]> getReportAsPdf(@PathVariable Long id) {
byte[] pdfBytes = reportService.convertReportToPdf(id);
return ResponseEntity.ok()
.header("Content-Type", "application/pdf")
.header("Content-Disposition", "inline; filename=preview.pdf")
.body(pdfBytes);
}
@PostMapping
public ResponseEntity<ReportResponse> uploadReport(
@RequestParam("file") MultipartFile file,
@RequestParam("projectId") Long projectId,
@RequestParam("fileType") String fileType) {
System.err.println("=== UPLOAD CALLED: file=" + file + " projectId=" + projectId + " fileType=" + fileType);
try {
String filename = file.getOriginalFilename();
if (filename != null) {
String extension = getFileExtension(filename).toLowerCase();
if (!isValidExtension(extension)) {
return ResponseEntity.badRequest().body(null);
}
}
ReportResponse response = reportService.uploadReport(file, projectId, fileType);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (Exception e) {
System.err.println("=== UPLOAD EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage());
e.printStackTrace();
return ResponseEntity.status(500).body(new ReportResponse(null, projectId, "ERROR", fileType, "ERROR", null, "ERROR:" + e.getClass().getSimpleName() + ":" + e.getMessage()));
}
}
@PutMapping("/{id}")
public ResponseEntity<ReportResponse> updateReport(
@PathVariable Long id,
@RequestBody ReportRequest request) {
return ResponseEntity.ok(reportService.updateReport(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteReport(@PathVariable Long id) {
reportService.deleteReport(id);
return ResponseEntity.noContent().build();
}
private String getFileExtension(String filename) {
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex > 0) {
return filename.substring(lastDotIndex + 1);
}
return "";
}
private boolean isValidExtension(String extension) {
return extension.equals("html") || extension.equals("md") ||
extension.equals("ppt") || extension.equals("pptx") || extension.equals("pdf");
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleError(Exception e) {
String msg = e.getClass().getSimpleName() + ": " + e.getMessage();
System.err.println("CONTROLLER ERROR: " + msg);
e.printStackTrace();
return ResponseEntity.status(500).body(new java.util.LinkedHashMap<String,String>() {{
put("error", msg);
put("type", e.getClass().getName());
}});
}
}
@@ -0,0 +1,29 @@
package com.reportdist.dto;
import jakarta.validation.constraints.NotBlank;
public class ProjectRequest {
@NotBlank(message = "Project name is required")
private String name;
private String description;
private String coverImage;
public ProjectRequest() {}
public ProjectRequest(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getCoverImage() { return coverImage; }
public void setCoverImage(String coverImage) { this.coverImage = coverImage; }
}
@@ -0,0 +1,80 @@
package com.reportdist.dto;
import com.reportdist.entity.Project;
import java.time.LocalDateTime;
public class ProjectResponse {
private Long id;
private String name;
private String description;
private LocalDateTime createdAt;
private String coverImage;
private long reportCount;
private long todayNewReports;
public ProjectResponse() {}
public ProjectResponse(Long id, String name, String description, LocalDateTime createdAt) {
this(id, name, description, createdAt, null, 0, 0);
}
public ProjectResponse(Long id, String name, String description, LocalDateTime createdAt, String coverImage) {
this(id, name, description, createdAt, coverImage, 0, 0);
}
public ProjectResponse(Long id, String name, String description, LocalDateTime createdAt, String coverImage, long reportCount) {
this(id, name, description, createdAt, coverImage, reportCount, 0);
}
public ProjectResponse(Long id, String name, String description, LocalDateTime createdAt, String coverImage, long reportCount, long todayNewReports) {
this.id = id;
this.name = name;
this.description = description;
this.createdAt = createdAt;
this.coverImage = coverImage;
this.reportCount = reportCount;
this.todayNewReports = todayNewReports;
}
public static ProjectResponse fromEntity(Project project) {
return fromEntity(project, 0, 0);
}
public static ProjectResponse fromEntity(Project project, long reportCount) {
return fromEntity(project, reportCount, 0);
}
public static ProjectResponse fromEntity(Project project, long reportCount, long todayNewReports) {
return new ProjectResponse(
project.getId(),
project.getName(),
project.getDescription(),
project.getCreatedAt(),
project.getCoverImage(),
reportCount,
todayNewReports
);
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public String getCoverImage() { return coverImage; }
public void setCoverImage(String coverImage) { this.coverImage = coverImage; }
public long getReportCount() { return reportCount; }
public void setReportCount(long reportCount) { this.reportCount = reportCount; }
public long getTodayNewReports() { return todayNewReports; }
public void setTodayNewReports(long todayNewReports) { this.todayNewReports = todayNewReports; }
}
@@ -0,0 +1,30 @@
package com.reportdist.dto;
import jakarta.validation.constraints.NotBlank;
public class ReportRequest {
@NotBlank(message = "File name is required")
private String fileName;
private Long projectId;
private String fileType;
public ReportRequest() {}
public ReportRequest(String fileName, Long projectId, String fileType) {
this.fileName = fileName;
this.projectId = projectId;
this.fileType = fileType;
}
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
}
@@ -0,0 +1,72 @@
package com.reportdist.dto;
import com.reportdist.entity.Report;
import java.time.LocalDateTime;
public class ReportResponse {
private Long id;
private Long projectId;
private String fileName;
private String fileType;
private String filePath;
private LocalDateTime uploadTime;
private String fileContent;
public ReportResponse() {}
public ReportResponse(Long id, Long projectId, String fileName, String fileType, String filePath, LocalDateTime uploadTime, String fileContent) {
this.id = id;
this.projectId = projectId;
this.fileName = fileName;
this.fileType = fileType;
this.filePath = filePath;
this.uploadTime = uploadTime;
this.fileContent = fileContent;
}
public static ReportResponse fromEntity(Report report) {
return new ReportResponse(
report.getId(),
report.getProjectId(),
report.getFileName(),
report.getFileType().name(),
report.getFilePath(),
report.getUploadTime(),
null
);
}
public static ReportResponse fromEntityWithContent(Report report, String fileContent) {
return new ReportResponse(
report.getId(),
report.getProjectId(),
report.getFileName(),
report.getFileType().name(),
report.getFilePath(),
report.getUploadTime(),
fileContent
);
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public LocalDateTime getUploadTime() { return uploadTime; }
public void setUploadTime(LocalDateTime uploadTime) { this.uploadTime = uploadTime; }
public String getFileContent() { return fileContent; }
public void setFileContent(String fileContent) { this.fileContent = fileContent; }
}
@@ -0,0 +1,53 @@
package com.reportdist.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "projects")
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
private String coverImage;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
public Project() {}
public Project(Long id, String name, String description, LocalDateTime createdAt) {
this.id = id;
this.name = name;
this.description = description;
this.createdAt = createdAt;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public String getCoverImage() { return coverImage; }
public void setCoverImage(String coverImage) { this.coverImage = coverImage; }
}
@@ -0,0 +1,79 @@
package com.reportdist.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "reports")
public class Report {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "project_id", nullable = false)
private Long projectId;
@Column(name = "file_name", nullable = false)
private String fileName;
@Column(name = "file_type", nullable = false)
@Enumerated(EnumType.STRING)
private FileType fileType;
@Column(name = "file_path", nullable = false)
private String filePath;
@Column(name = "upload_time", nullable = false)
private LocalDateTime uploadTime;
@Column(name = "pdf_path")
private String pdfPath;
@Column(name = "pdf_ready", nullable = false)
private boolean pdfReady = false;
@PrePersist
protected void onCreate() {
uploadTime = LocalDateTime.now();
}
public enum FileType {
HTML, MD, PPT, PPTX, PDF
}
public Report() {}
public Report(Long id, Long projectId, String fileName, FileType fileType, String filePath, LocalDateTime uploadTime) {
this.id = id;
this.projectId = projectId;
this.fileName = fileName;
this.fileType = fileType;
this.filePath = filePath;
this.uploadTime = uploadTime;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public FileType getFileType() { return fileType; }
public void setFileType(FileType fileType) { this.fileType = fileType; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public LocalDateTime getUploadTime() { return uploadTime; }
public void setUploadTime(LocalDateTime uploadTime) { this.uploadTime = uploadTime; }
public String getPdfPath() { return pdfPath; }
public void setPdfPath(String pdfPath) { this.pdfPath = pdfPath; }
public boolean isPdfReady() { return pdfReady; }
public void setPdfReady(boolean pdfReady) { this.pdfReady = pdfReady; }
}
@@ -0,0 +1,65 @@
package com.reportdist.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MultipartException.class)
public ResponseEntity<?> handleMultipart(MultipartException ex) {
System.err.println("=== MULTIPART ERROR ===");
ex.printStackTrace();
return ResponseEntity.status(500).body(java.util.Map.of(
"error", "MULTIPART:" + ex.getClass().getSimpleName() + ":" + ex.getMessage(),
"type", ex.getClass().getName()
));
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<?> handleSize(MaxUploadSizeExceededException ex) {
System.err.println("=== SIZE ERROR ===");
ex.printStackTrace();
return ResponseEntity.status(500).body(java.util.Map.of(
"error", "SIZE_LIMIT:" + ex.getMessage(),
"type", ex.getClass().getName()
));
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<?> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
System.err.println("=== TYPE MISMATCH ===");
ex.printStackTrace();
return ResponseEntity.status(400).body(java.util.Map.of(
"error", "TYPE_MISMATCH:" + ex.getName() + " cannot parse " + ex.getValue(),
"type", ex.getClass().getName()
));
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<?> handleRuntimeException(RuntimeException ex) {
System.err.println("=== RUNTIME ERROR ===");
ex.printStackTrace();
return ResponseEntity.status(500).body(java.util.Map.of(
"error", ex.getMessage() != null ? ex.getMessage() : "null",
"type", ex.getClass().getName()
));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleGeneric(Exception ex) {
System.err.println("=== GENERIC ERROR ===");
ex.printStackTrace();
return ResponseEntity.status(500).body(java.util.Map.of(
"error", ex.getClass().getSimpleName() + ":" + ex.getMessage(),
"type", ex.getClass().getName()
));
}
}
@@ -0,0 +1,9 @@
package com.reportdist.repository;
import com.reportdist.entity.Project;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProjectRepository extends JpaRepository<Project, Long> {
}
@@ -0,0 +1,19 @@
package com.reportdist.repository;
import com.reportdist.entity.Report;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface ReportRepository extends JpaRepository<Report, Long> {
List<Report> findByProjectId(Long projectId);
long countByProjectId(Long projectId);
long countByUploadTimeAfter(LocalDateTime time);
long countByProjectIdAndUploadTimeAfter(Long projectId, LocalDateTime time);
}
@@ -0,0 +1,143 @@
package com.reportdist.service;
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.rendering.RenderDestination;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class PptxToPdfService {
public static byte[] convert(String pptxFilePath) throws IOException {
Path path = Path.of(pptxFilePath);
if (!Files.exists(path)) {
throw new IOException("PPTX file not found: " + pptxFilePath);
}
try (InputStream is = Files.newInputStream(path);
XMLSlideShow pptx = new XMLSlideShow(is)) {
List<XSLFSlide> slides = pptx.getSlides();
if (slides.isEmpty()) {
throw new IOException("No slides found in PPTX file");
}
Dimension slideSize = pptx.getPageSize();
int scale = 2;
int imgWidth = (int) slideSize.getWidth() * scale;
int imgHeight = (int) slideSize.getHeight() * scale;
// First render all slides to PNG images
byte[][] slideImages = new byte[slides.size()][];
for (int i = 0; i < slides.size(); i++) {
XSLFSlide slide = slides.get(i);
BufferedImage slideImage = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = slideImage.createGraphics();
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, imgWidth, imgHeight);
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
graphics.scale(scale, scale);
try {
slide.draw(graphics);
} catch (Exception e) {
drawTextBoxes(graphics, slide);
}
graphics.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(slideImage, "PNG", baos);
slideImages[i] = baos.toByteArray();
}
// Create PDF with each slide as a page
PDDocument document = new PDDocument();
for (int i = 0; i < slideImages.length; i++) {
byte[] pngBytes = slideImages[i];
// Calculate page size to match slide aspect ratio
float imgAspect = (float) imgWidth / imgHeight;
float pageWidth = PDRectangle.A4.getWidth();
float pageHeight = pageWidth / imgAspect;
if (pageHeight > PDRectangle.A4.getHeight()) {
pageHeight = PDRectangle.A4.getHeight();
pageWidth = pageHeight * imgAspect;
}
PDPage page = new PDPage(new PDRectangle(pageWidth, pageHeight));
document.addPage(page);
// Draw image on page
try (PDPageContentStream cs = new PDPageContentStream(document, page)) {
PDImageXObject pdImage = PDImageXObject.createFromByteArray(document, pngBytes, "slide_" + i);
cs.drawImage(pdImage, 0, 0, pageWidth, pageHeight);
}
}
ByteArrayOutputStream pdfBaos = new ByteArrayOutputStream();
document.save(pdfBaos);
document.close();
return pdfBaos.toByteArray();
}
}
private static void drawTextBoxes(Graphics2D g, XSLFSlide slide) {
for (org.apache.poi.xslf.usermodel.XSLFShape shape : slide.getShapes()) {
if (shape instanceof org.apache.poi.xslf.usermodel.XSLFTextShape) {
org.apache.poi.xslf.usermodel.XSLFTextShape ts = (org.apache.poi.xslf.usermodel.XSLFTextShape) shape;
java.awt.geom.Rectangle2D rect = ts.getAnchor();
if (rect == null) continue;
g.setColor(Color.WHITE);
g.fill(rect);
g.setColor(Color.LIGHT_GRAY);
g.draw(rect);
StringBuilder sb = new StringBuilder();
for (org.apache.poi.xslf.usermodel.XSLFTextParagraph para : ts.getTextParagraphs()) {
for (org.apache.poi.xslf.usermodel.XSLFTextRun run : para.getTextRuns()) {
String text = run.getRawText();
if (text != null) sb.append(text);
}
sb.append("\n");
}
String text = sb.toString().trim();
if (!text.isEmpty()) {
g.setColor(Color.BLACK);
g.setFont(new Font("Arial", Font.PLAIN, 10));
int x = (int) rect.getX() + 5;
int y = (int) rect.getY() + 15;
for (String line : text.split("\n")) {
g.drawString(line, x, y);
y += 12;
}
}
}
}
}
}
@@ -0,0 +1,122 @@
package com.reportdist.service;
import com.reportdist.dto.ProjectRequest;
import com.reportdist.dto.ProjectResponse;
import com.reportdist.entity.Project;
import com.reportdist.repository.ProjectRepository;
import com.reportdist.repository.ReportRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@Transactional
public class ProjectService {
private final ProjectRepository projectRepository;
private final ReportRepository reportRepository;
private String uploadDir;
public ProjectService(ProjectRepository projectRepository, ReportRepository reportRepository) {
this.projectRepository = projectRepository;
this.reportRepository = reportRepository;
}
@Value("${file.upload.dir}")
public void setUploadDir(String uploadDir) {
this.uploadDir = uploadDir;
}
public List<ProjectResponse> getAllProjects() {
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
return projectRepository.findAll().stream()
.map(project -> {
long count = reportRepository.countByProjectId(project.getId());
long todayNew = reportRepository.countByProjectIdAndUploadTimeAfter(project.getId(), startOfDay);
return ProjectResponse.fromEntity(project, count, todayNew);
})
.collect(Collectors.toList());
}
public ProjectResponse getProjectById(Long id) {
Project project = projectRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Project not found with id: " + id));
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
long count = reportRepository.countByProjectId(id);
long todayNew = reportRepository.countByProjectIdAndUploadTimeAfter(id, startOfDay);
return ProjectResponse.fromEntity(project, count, todayNew);
}
public ProjectResponse createProject(ProjectRequest request) {
Project project = new Project();
project.setName(request.getName());
project.setDescription(request.getDescription());
if (request.getCoverImage() != null) {
project.setCoverImage(request.getCoverImage());
}
Project saved = projectRepository.save(project);
return ProjectResponse.fromEntity(saved, 0);
}
public ProjectResponse updateProject(Long id, String name, String description, MultipartFile coverImage) {
Project project = projectRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Project not found with id: " + id));
if (name != null) {
project.setName(name);
}
if (description != null) {
project.setDescription(description);
}
if (coverImage != null && !coverImage.isEmpty()) {
try {
// Save cover image to uploads/covers/{projectId}/
Path coverDir = Paths.get(uploadDir, "covers", String.valueOf(id));
Files.createDirectories(coverDir);
String originalFilename = coverImage.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String uniqueFileName = UUID.randomUUID().toString() + extension;
Path coverPath = coverDir.resolve(uniqueFileName);
Files.write(coverPath, coverImage.getBytes());
// Store relative path for serving
String coverUrl = "/uploads/covers/" + id + "/" + uniqueFileName;
project.setCoverImage(coverUrl);
} catch (IOException e) {
throw new RuntimeException("Failed to save cover image: " + e.getMessage(), e);
}
}
Project updated = projectRepository.save(project);
long count = reportRepository.countByProjectId(id);
long todayNew = reportRepository.countByProjectIdAndUploadTimeAfter(id, LocalDateTime.now().toLocalDate().atStartOfDay());
return ProjectResponse.fromEntity(updated, count, todayNew);
}
// Keep old method for backward compatibility
public ProjectResponse updateProject(Long id, ProjectRequest request) {
return updateProject(id, request.getName(), request.getDescription(), null);
}
public void deleteProject(Long id) {
if (!projectRepository.existsById(id)) {
throw new RuntimeException("Project not found with id: " + id);
}
projectRepository.deleteById(id);
}
}
@@ -0,0 +1,188 @@
package com.reportdist.service;
import com.reportdist.dto.ReportRequest;
import com.reportdist.dto.ReportResponse;
import com.reportdist.entity.Report;
import com.reportdist.repository.ReportRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class ReportService {
private final ReportRepository reportRepository;
private String uploadDir;
public ReportService(ReportRepository reportRepository) {
this.reportRepository = reportRepository;
}
@Value("${file.upload.dir}")
public void setUploadDir(String uploadDir) {
this.uploadDir = uploadDir;
}
public List<ReportResponse> getAllReports(Long projectId) {
List<Report> reports;
if (projectId != null) {
reports = reportRepository.findByProjectId(projectId);
} else {
reports = reportRepository.findAll();
}
return reports.stream()
.map(ReportResponse::fromEntity)
.collect(Collectors.toList());
}
public ReportResponse getReportById(Long id) {
Report report = reportRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Report not found with id: " + id));
String fileContent = readFileContent(report.getFilePath());
return ReportResponse.fromEntityWithContent(report, fileContent);
}
public ReportResponse uploadReport(MultipartFile file, Long projectId, String fileType) {
try {
// Create project subdirectory if needed
Path projectDir = Paths.get(uploadDir, String.valueOf(projectId));
Files.createDirectories(projectDir);
// Generate unique filename
String originalFilename = file.getOriginalFilename();
String uniqueFileName = System.currentTimeMillis() + "_" + originalFilename;
Path filePath = projectDir.resolve(uniqueFileName);
// Save file
file.transferTo(filePath.toFile());
// Create report entity
Report report = new Report();
report.setProjectId(projectId);
report.setFileName(originalFilename);
report.setFileType(Report.FileType.valueOf(fileType.toUpperCase()));
report.setFilePath(filePath.toString());
report.setPdfReady(false);
// Pre-render PDF for PPTX files
if (fileType.equalsIgnoreCase("pptx") || fileType.equalsIgnoreCase("ppt")) {
try {
byte[] pdfBytes = PptxToPdfService.convert(filePath.toString());
Path pdfDir = projectDir.resolve("pdfs");
Files.createDirectories(pdfDir);
String pdfFileName = uniqueFileName.replaceAll("\\.(pptx?|PPT|X)$", ".pdf");
Path pdfPath = pdfDir.resolve(pdfFileName);
Files.write(pdfPath, pdfBytes);
report.setPdfPath(pdfPath.toString());
report.setPdfReady(true);
System.out.println("PDF pre-rendered successfully: " + pdfPath);
} catch (Exception e) {
System.err.println("Failed to pre-render PDF: " + e.getMessage());
// Continue without PDF - not critical
}
}
Report saved = reportRepository.save(report);
return ReportResponse.fromEntity(saved);
} catch (IOException e) {
System.err.println("=== UPLOAD ERROR ===");
e.printStackTrace();
throw new RuntimeException("UPLOAD_FAILED:" + e.getClass().getName() + ":" + e.getMessage(), e);
}
}
public ReportResponse updateReport(Long id, ReportRequest request) {
Report report = reportRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Report not found with id: " + id));
if (request.getFileName() != null) {
report.setFileName(request.getFileName());
}
if (request.getProjectId() != null) {
report.setProjectId(request.getProjectId());
}
if (request.getFileType() != null) {
report.setFileType(Report.FileType.valueOf(request.getFileType().toUpperCase()));
}
Report updated = reportRepository.save(report);
return ReportResponse.fromEntity(updated);
}
public void deleteReport(Long id) {
Report report = reportRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Report not found with id: " + id));
// Delete the file
try {
Path filePath = Paths.get(report.getFilePath());
Files.deleteIfExists(filePath);
} catch (IOException e) {
// Log but don't fail the delete operation
System.err.println("Failed to delete file: " + e.getMessage());
}
reportRepository.deleteById(id);
}
private String readFileContent(String filePath) {
try {
Path path = Paths.get(filePath);
if (Files.exists(path)) {
return Files.readString(path);
}
return null;
} catch (IOException e) {
return null;
}
}
public byte[] getReportBytes(Long id) {
Report report = reportRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Report not found with id: " + id));
try {
return Files.readAllBytes(Paths.get(report.getFilePath()));
} catch (IOException e) {
throw new RuntimeException("Failed to read file: " + e.getMessage(), e);
}
}
public byte[] convertReportToPdf(Long id) {
Report report = reportRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Report not found with id: " + id));
String fileType = report.getFileType().name().toLowerCase();
if (!fileType.equals("pptx") && !fileType.equals("ppt")) {
throw new RuntimeException("Only PPTX files can be converted to PDF. Current file type: " + fileType);
}
// Return pre-rendered PDF if available
if (report.isPdfReady() && report.getPdfPath() != null) {
try {
Path pdfPath = Paths.get(report.getPdfPath());
if (Files.exists(pdfPath)) {
return Files.readAllBytes(pdfPath);
}
} catch (IOException e) {
System.err.println("Failed to read pre-rendered PDF: " + e.getMessage());
}
}
// Fallback: render on demand
try {
return PptxToPdfService.convert(report.getFilePath());
} catch (IOException e) {
throw new RuntimeException("Failed to convert PPTX to PDF: " + e.getMessage(), e);
}
}
}
+46
View File
@@ -0,0 +1,46 @@
server:
port: 8080
spring:
application:
name: daily-report-distribution
datasource:
url: jdbc:sqlite:./database.db
driver-class-name: org.sqlite.JDBC
jpa:
database-platform: org.hibernate.community.dialect.SQLiteDialect
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
format_sql: true
servlet:
multipart:
enabled: true
max-file-size: 100MB
max-request-size: 100MB
# Serve uploaded files statically
web:
resources:
static-locations: file:./uploads/
file:
upload:
dir: ${UPLOAD_DIR:${user.dir}/uploads}
# Spring Boot Actuator (Docker healthcheck)
management:
endpoints:
web:
exposure:
include: health,health-liveness,health-readiness
endpoint:
health:
show-details: always
probes:
enabled: true