diff --git a/Backend/src/main/java/de/itsolutions/ticketsystem/controller/CommentController.java b/Backend/src/main/java/de/itsolutions/ticketsystem/controller/CommentController.java new file mode 100644 index 0000000..cbb2e1a --- /dev/null +++ b/Backend/src/main/java/de/itsolutions/ticketsystem/controller/CommentController.java @@ -0,0 +1,63 @@ +package de.itsolutions.ticketsystem.controller; + +import de.itsolutions.ticketsystem.entity.Comment; +import de.itsolutions.ticketsystem.entity.Ticket; +import de.itsolutions.ticketsystem.entity.User; +import de.itsolutions.ticketsystem.repository.CommentRepository; +import de.itsolutions.ticketsystem.repository.TicketRepository; +import de.itsolutions.ticketsystem.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/tickets/{ticketId}/comments") +public class CommentController { + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private TicketRepository ticketRepository; + + @Autowired + private UserRepository userRepository; + + @GetMapping + public List getComments(@PathVariable Long ticketId) { + if (!ticketRepository.existsById(ticketId)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"); + } + return commentRepository.findByTicketIdOrderByCreatedAtAsc(ticketId); + } + + @PostMapping + public Comment addComment(@PathVariable Long ticketId, @RequestBody Map payload) { + String text = payload.get("text"); + if (text == null || text.trim().isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Text is required"); + } + + Ticket ticket = ticketRepository.findById(ticketId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + String email = auth.getName(); + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")); + + Comment comment = new Comment(); + comment.setText(text); + comment.setTicket(ticket); + comment.setAuthor(user); + + return commentRepository.save(comment); + } +} diff --git a/Backend/src/main/java/de/itsolutions/ticketsystem/entity/Comment.java b/Backend/src/main/java/de/itsolutions/ticketsystem/entity/Comment.java new file mode 100644 index 0000000..96318a3 --- /dev/null +++ b/Backend/src/main/java/de/itsolutions/ticketsystem/entity/Comment.java @@ -0,0 +1,42 @@ +package de.itsolutions.ticketsystem.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + +@Entity +@Table(name = "comments") +@Data +@NoArgsConstructor +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, columnDefinition = "TEXT") + private String text; + + @Column(nullable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + @ManyToOne(optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User author; + + @ManyToOne(optional = false) + @JoinColumn(name = "ticket_id", nullable = false) + private Ticket ticket; + + public void setId(Long id) { this.id = id; } + public Long getId() { return id; } + public void setText(String text) { this.text = text; } + public String getText() { return text; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + public LocalDateTime getCreatedAt() { return createdAt; } + public void setAuthor(User author) { this.author = author; } + public User getAuthor() { return author; } + public void setTicket(Ticket ticket) { this.ticket = ticket; } + public Ticket getTicket() { return ticket; } +} diff --git a/Backend/src/main/java/de/itsolutions/ticketsystem/repository/CommentRepository.java b/Backend/src/main/java/de/itsolutions/ticketsystem/repository/CommentRepository.java new file mode 100644 index 0000000..06dc040 --- /dev/null +++ b/Backend/src/main/java/de/itsolutions/ticketsystem/repository/CommentRepository.java @@ -0,0 +1,9 @@ +package de.itsolutions.ticketsystem.repository; + +import de.itsolutions.ticketsystem.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findByTicketIdOrderByCreatedAtAsc(Long ticketId); +} diff --git a/Frontend/components/layout/app-shell.tsx b/Frontend/components/layout/app-shell.tsx index 1037509..2f2b66b 100644 --- a/Frontend/components/layout/app-shell.tsx +++ b/Frontend/components/layout/app-shell.tsx @@ -35,7 +35,7 @@ export function AppShell({ children }: AppShellProps) { return (
-
+
@@ -124,7 +124,7 @@ export function AppShell({ children }: AppShellProps) {
-
{children}
+
{children}
) } diff --git a/Frontend/components/tickets/ticket-comments.tsx b/Frontend/components/tickets/ticket-comments.tsx new file mode 100644 index 0000000..57d1e40 --- /dev/null +++ b/Frontend/components/tickets/ticket-comments.tsx @@ -0,0 +1,103 @@ +"use client" + +import { useState, useEffect, useCallback } from "react" +import { useAuth } from "@/lib/auth-context" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import type { Comment } from "@/lib/types" + +interface TicketCommentsProps { + ticketId: number +} + +export function TicketComments({ ticketId }: TicketCommentsProps) { + const [comments, setComments] = useState([]) + const [newComment, setNewComment] = useState("") + const [loading, setLoading] = useState(false) + const { user, authHeader } = useAuth() + const API_URL = process.env.NEXT_PUBLIC_API_URL + "/api" + + const fetchComments = useCallback(async () => { + if (!authHeader) return + try { + const res = await fetch(`${API_URL}/tickets/${ticketId}/comments`, { + headers: { "Authorization": authHeader } + }) + if (res.ok) { + const data = await res.json() + setComments(data) + } + } catch (e) { + console.error(e) + } + }, [ticketId, authHeader]) + + useEffect(() => { + if (authHeader) { + fetchComments() + } + }, [fetchComments, authHeader]) + + const handlePost = async () => { + if (!authHeader || !newComment.trim()) return + setLoading(true) + try { + const res = await fetch(`${API_URL}/tickets/${ticketId}/comments`, { + method: "POST", + headers: { + "Authorization": authHeader, + "Content-Type": "application/json" + }, + body: JSON.stringify({ text: newComment }) + }) + if (res.ok) { + setNewComment("") + fetchComments() + } + } catch (e) { + console.error(e) + } finally { + setLoading(false) + } + } + + return ( +
+

Comments

+
+ {comments.length === 0 &&

No comments yet.

} + {comments.map((comment) => ( +
+ + {comment.author.name ? comment.author.name.charAt(0) : '?'} + +
+
+ {comment.author.name} + {new Date(comment.createdAt).toLocaleString()} +
+
+

{comment.text}

+
+
+
+ ))} +
+ +
+