hitler
This commit is contained in:
parent
92d569b9cd
commit
9bb6db7693
35 changed files with 785 additions and 172 deletions
|
|
@ -7,15 +7,28 @@ import org.springframework.stereotype.Component;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Component to load initial data into the database when the application starts.
|
||||
*/
|
||||
@Component
|
||||
public class DataLoader implements CommandLineRunner {
|
||||
|
||||
private final RoomRepository roomRepository;
|
||||
|
||||
/**
|
||||
* Constructs a new DataLoader with the given RoomRepository.
|
||||
* @param roomRepository The repository for managing rooms.
|
||||
*/
|
||||
public DataLoader(RoomRepository roomRepository) {
|
||||
this.roomRepository = roomRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the data loading logic.
|
||||
* If no rooms exist, it creates and saves a few default rooms.
|
||||
* @param args Command line arguments.
|
||||
* @throws Exception if an error occurs during data loading.
|
||||
*/
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
if (roomRepository.count() == 0) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,16 @@ package de.itsolutions.ticketsystem;
|
|||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Main application class for the Ticket System.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class TicketSystemApplication {
|
||||
|
||||
/**
|
||||
* Main method to start the Spring Boot application.
|
||||
* @param args Command line arguments.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TicketSystemApplication.class, args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,19 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Security configuration for the application.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
/**
|
||||
* Configures the security filter chain.
|
||||
* @param http The HttpSecurity object to configure.
|
||||
* @return The SecurityFilterChain.
|
||||
* @throws Exception if an error occurs during configuration.
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
|
|
@ -35,16 +44,30 @@ public class SecurityConfig {
|
|||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a BCryptPasswordEncoder for password hashing.
|
||||
* @return The PasswordEncoder bean.
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an AuthenticationManager bean.
|
||||
* @param authConfig The AuthenticationConfiguration.
|
||||
* @return The AuthenticationManager.
|
||||
* @throws Exception if an error occurs during configuration.
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
|
||||
return authConfig.getAuthenticationManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures CORS for the application.
|
||||
* @return The CorsConfigurationSource bean.
|
||||
*/
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||
import org.springframework.web.bind.annotation.*;
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* REST controller for authentication-related operations.
|
||||
* Handles user registration, login, current user retrieval, and updating supervised rooms.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthController {
|
||||
|
|
@ -18,40 +22,43 @@ public class AuthController {
|
|||
private final AuthService authService;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
/**
|
||||
* Constructs an AuthController with necessary services.
|
||||
* @param authService The authentication service.
|
||||
* @param authenticationManager The authentication manager.
|
||||
*/
|
||||
public AuthController(AuthService authService, AuthenticationManager authenticationManager) {
|
||||
this.authService = authService;
|
||||
this.authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new user.
|
||||
* @param request Registration details
|
||||
* @return The registered user
|
||||
* Registers a new user in the system.
|
||||
* @param request The registration request containing user details.
|
||||
* @return A ResponseEntity with the registered user.
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<User> register(@RequestBody Dtos.RegisterRequest request) {
|
||||
// registering the user, hope they remember their password
|
||||
return ResponseEntity.ok(authService.register(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in a user.
|
||||
* @param request Login credentials
|
||||
* @return The user details if login succeeds
|
||||
* Authenticates a user and returns their details upon successful login.
|
||||
* @param request The login request containing user credentials.
|
||||
* @return A ResponseEntity with the authenticated user's details.
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<User> login(@RequestBody Dtos.LoginRequest request) {
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
|
||||
);
|
||||
// Return full user details
|
||||
return ResponseEntity.ok(authService.getUserByEmail(request.getEmail()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently authenticated user.
|
||||
* @param principal The security principal
|
||||
* @return The user
|
||||
* Retrieves the currently authenticated user's information.
|
||||
* @param principal The security principal representing the authenticated user.
|
||||
* @return A ResponseEntity with the current user's details.
|
||||
*/
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<User> getCurrentUser(Principal principal) {
|
||||
|
|
@ -59,21 +66,13 @@ public class AuthController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Updates the supervised rooms for the current user.
|
||||
* @param request Room IDs
|
||||
* @param principal The security principal
|
||||
* @return The updated user
|
||||
* Updates the list of rooms supervised by the current user.
|
||||
* @param request The request containing the IDs of rooms to supervise.
|
||||
* @param principal The security principal of the current user.
|
||||
* @return A ResponseEntity with the updated user details.
|
||||
*/
|
||||
@PutMapping("/profile/rooms")
|
||||
public ResponseEntity<User> updateMyRooms(@RequestBody Dtos.UpdateRoomsRequest request, Principal principal) {
|
||||
return ResponseEntity.ok(authService.updateSupervisedRooms(principal.getName(), request.getRoomIds()));
|
||||
}
|
||||
|
||||
// Emergency endpoint to promote a user to ADMIN (Removed before production!)
|
||||
// seriously, delete this before going live or we are doomed
|
||||
@PostMapping("/dev-promote-admin")
|
||||
public ResponseEntity<User> promoteToAdmin(@RequestBody Dtos.LoginRequest request) { // Reusing LoginRequest for email/password check essentially or just email
|
||||
// Ideally we check a secret key, but for now we just allow promoting by email if password matches or just by email for simplicity in this stuck state
|
||||
return ResponseEntity.ok(authService.promoteToAdmin(request.getEmail()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ import org.springframework.web.server.ResponseStatusException;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST controller for managing comments on tickets.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/tickets/{ticketId}/comments")
|
||||
public class CommentController {
|
||||
|
|
@ -30,6 +33,12 @@ public class CommentController {
|
|||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* Retrieves all comments for a specific ticket.
|
||||
* @param ticketId The ID of the ticket.
|
||||
* @return A list of comments for the specified ticket.
|
||||
* @throws ResponseStatusException if the ticket is not found.
|
||||
*/
|
||||
@GetMapping
|
||||
public List<Comment> getComments(@PathVariable Long ticketId) {
|
||||
if (!ticketRepository.existsById(ticketId)) {
|
||||
|
|
@ -38,6 +47,13 @@ public class CommentController {
|
|||
return commentRepository.findByTicketIdOrderByCreatedAtAsc(ticketId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new comment to a ticket.
|
||||
* @param ticketId The ID of the ticket to add the comment to.
|
||||
* @param payload A map containing the comment text.
|
||||
* @return The newly created comment.
|
||||
* @throws ResponseStatusException if the text is empty, ticket is not found, or user is not found.
|
||||
*/
|
||||
@PostMapping
|
||||
public Comment addComment(@PathVariable Long ticketId, @RequestBody Map<String, String> payload) {
|
||||
String text = payload.get("text");
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST controller for managing rooms.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/rooms")
|
||||
public class RoomController {
|
||||
|
|
@ -23,11 +26,21 @@ public class RoomController {
|
|||
@Autowired
|
||||
private RoomRepository roomRepository;
|
||||
|
||||
/**
|
||||
* Retrieves all rooms.
|
||||
* @return A list of all rooms.
|
||||
*/
|
||||
@GetMapping
|
||||
public List<Room> getAllRooms() {
|
||||
return roomRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new room.
|
||||
* @param payload A map containing the name of the room.
|
||||
* @return The newly created room.
|
||||
* @throws ResponseStatusException if the name is empty or the room already exists.
|
||||
*/
|
||||
@PostMapping
|
||||
public Room createRoom(@RequestBody Map<String, String> payload) {
|
||||
String name = payload.get("name");
|
||||
|
|
@ -42,6 +55,13 @@ public class RoomController {
|
|||
return roomRepository.save(room);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing room.
|
||||
* @param id The ID of the room to update.
|
||||
* @param payload A map containing the new name of the room.
|
||||
* @return The updated room.
|
||||
* @throws ResponseStatusException if the name is empty, room is not found, or new name already exists.
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Room updateRoom(@PathVariable Long id, @RequestBody Map<String, String> payload) {
|
||||
String name = payload.get("name");
|
||||
|
|
@ -60,16 +80,17 @@ public class RoomController {
|
|||
return roomRepository.save(room);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a room by its ID.
|
||||
* @param id The ID of the room to delete.
|
||||
* @return A ResponseEntity with no content if successful.
|
||||
* @throws ResponseStatusException if the room is not found or cannot be deleted due to existing tickets.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteRoom(@PathVariable Long id) {
|
||||
if (!roomRepository.existsById(id)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found");
|
||||
}
|
||||
// Note: This might fail if constraints exist (tickets linked to room).
|
||||
// Real-world app needs constraint handling (delete tickets or block).
|
||||
// For simplicity here, we assume cascade or block.
|
||||
// Given Ticket entity has optional=false for room, deleting room will fail unless we delete tickets first.
|
||||
// Let's rely on DB error for now or add cascade logic later if requested.
|
||||
try {
|
||||
roomRepository.deleteById(id);
|
||||
} catch (Exception e) {
|
||||
|
|
@ -78,6 +99,13 @@ public class RoomController {
|
|||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports rooms from a CSV file. Each line in the file is expected to be a room name.
|
||||
* Only unique room names will be imported.
|
||||
* @param file The MultipartFile containing the CSV data.
|
||||
* @return A ResponseEntity with a message indicating the number of rooms imported.
|
||||
* @throws ResponseStatusException if the file processing fails.
|
||||
*/
|
||||
@PostMapping("/import")
|
||||
public ResponseEntity<?> importRooms(@RequestParam("file") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
|
|
@ -89,9 +117,6 @@ public class RoomController {
|
|||
int count = 0;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
String name = line.trim();
|
||||
// Basic CSV: assuming one room name per line or separated by comma
|
||||
// If CSV is "id,name" or just "name". Let's assume just "name" per line or typical CSV format handling.
|
||||
// Simple implementation: One room name per line.
|
||||
if (!name.isEmpty() && roomRepository.findByName(name).isEmpty()) {
|
||||
Room room = new Room();
|
||||
room.setName(name);
|
||||
|
|
|
|||
|
|
@ -8,31 +8,60 @@ import org.springframework.web.bind.annotation.*;
|
|||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* REST controller for managing tickets.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/tickets")
|
||||
public class TicketController {
|
||||
|
||||
private final TicketService ticketService;
|
||||
|
||||
/**
|
||||
* Constructs a new TicketController with the given TicketService.
|
||||
* @param ticketService The service for managing tickets.
|
||||
*/
|
||||
public TicketController(TicketService ticketService) {
|
||||
this.ticketService = ticketService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ticket.
|
||||
* @param request The ticket creation request.
|
||||
* @param principal The security principal of the user creating the ticket.
|
||||
* @return A ResponseEntity containing the created ticket.
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<Ticket> createTicket(@RequestBody Dtos.TicketRequest request, Principal principal) {
|
||||
return ResponseEntity.ok(ticketService.createTicket(request, principal.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves tickets relevant to the current user.
|
||||
* @param principal The security principal of the current user.
|
||||
* @return A ResponseEntity containing a list of tickets.
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<List<Ticket>> getTickets(Principal principal) {
|
||||
return ResponseEntity.ok(ticketService.getTicketsForUser(principal.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the status of a specific ticket.
|
||||
* @param id The ID of the ticket to update.
|
||||
* @param request The request containing the new status.
|
||||
* @return A ResponseEntity containing the updated ticket.
|
||||
*/
|
||||
@PatchMapping("/{id}/status")
|
||||
public ResponseEntity<Ticket> updateStatus(@PathVariable Long id, @RequestBody Dtos.TicketStatusRequest request) {
|
||||
return ResponseEntity.ok(ticketService.updateTicketStatus(id, request.getStatus()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a ticket by its ID.
|
||||
* @param id The ID of the ticket to delete.
|
||||
* @return A ResponseEntity with no content.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteTicket(@PathVariable Long id) {
|
||||
ticketService.deleteTicket(id);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ import org.springframework.web.bind.annotation.*;
|
|||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* Controller for stuff that relates to the user, but not necessarily login/register.
|
||||
* Kept separate because clean code or whatever.
|
||||
* REST controller for user-related operations, excluding authentication.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
|
|
@ -17,19 +16,22 @@ public class UserController {
|
|||
|
||||
private final AuthService authService;
|
||||
|
||||
/**
|
||||
* Constructs a UserController with the necessary authentication service.
|
||||
* @param authService The authentication service.
|
||||
*/
|
||||
public UserController(AuthService authService) {
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the theme for the current user.
|
||||
* @param request The theme update request
|
||||
* @param principal The security principal (the logged in dude)
|
||||
* @return The updated user
|
||||
* Updates the theme preference for the currently authenticated user.
|
||||
* @param request The request containing the new theme preference.
|
||||
* @param principal The security principal of the logged-in user.
|
||||
* @return A ResponseEntity containing the updated user.
|
||||
*/
|
||||
@PatchMapping("/theme")
|
||||
public ResponseEntity<User> updateTheme(@RequestBody Dtos.ThemeUpdateRequest request, Principal principal) {
|
||||
// Just delegating to service, standard procedure
|
||||
return ResponseEntity.ok(authService.updateTheme(principal.getName(), request.getTheme()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
package de.itsolutions.ticketsystem.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A container class for various Data Transfer Objects (DTOs) used in the application.
|
||||
*/
|
||||
public class Dtos {
|
||||
|
||||
/**
|
||||
* DTO for user registration requests.
|
||||
*/
|
||||
public static class RegisterRequest {
|
||||
private String firstname;
|
||||
private String lastname;
|
||||
|
|
@ -13,60 +18,186 @@ public class Dtos {
|
|||
private String role;
|
||||
private List<Long> roomIds;
|
||||
|
||||
/**
|
||||
* Gets the first name of the user.
|
||||
* @return The first name.
|
||||
*/
|
||||
public String getFirstname() { return firstname; }
|
||||
/**
|
||||
* Sets the first name of the user.
|
||||
* @param firstname The first name.
|
||||
*/
|
||||
public void setFirstname(String firstname) { this.firstname = firstname; }
|
||||
/**
|
||||
* Gets the last name of the user.
|
||||
* @return The last name.
|
||||
*/
|
||||
public String getLastname() { return lastname; }
|
||||
/**
|
||||
* Sets the last name of the user.
|
||||
* @param lastname The last name.
|
||||
*/
|
||||
public void setLastname(String lastname) { this.lastname = lastname; }
|
||||
/**
|
||||
* Gets the email of the user.
|
||||
* @return The email.
|
||||
*/
|
||||
public String getEmail() { return email; }
|
||||
/**
|
||||
* Sets the email of the user.
|
||||
* @param email The email.
|
||||
*/
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
/**
|
||||
* Gets the password of the user.
|
||||
* @return The password.
|
||||
*/
|
||||
public String getPassword() { return password; }
|
||||
/**
|
||||
* Sets the password of the user.
|
||||
* @param password The password.
|
||||
*/
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
/**
|
||||
* Gets the role of the user.
|
||||
* @return The role.
|
||||
*/
|
||||
public String getRole() { return role; }
|
||||
/**
|
||||
* Sets the role of the user.
|
||||
* @param role The role.
|
||||
*/
|
||||
public void setRole(String role) { this.role = role; }
|
||||
/**
|
||||
* Gets the list of room IDs the user supervises.
|
||||
* @return The list of room IDs.
|
||||
*/
|
||||
public List<Long> getRoomIds() { return roomIds; }
|
||||
/**
|
||||
* Sets the list of room IDs the user supervises.
|
||||
* @param roomIds The list of room IDs.
|
||||
*/
|
||||
public void setRoomIds(List<Long> roomIds) { this.roomIds = roomIds; }
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for user login requests.
|
||||
*/
|
||||
public static class LoginRequest {
|
||||
private String email;
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* Gets the email for login.
|
||||
* @return The email.
|
||||
*/
|
||||
public String getEmail() { return email; }
|
||||
/**
|
||||
* Sets the email for login.
|
||||
* @param email The email.
|
||||
*/
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
/**
|
||||
* Gets the password for login.
|
||||
* @return The password.
|
||||
*/
|
||||
public String getPassword() { return password; }
|
||||
/**
|
||||
* Sets the password for login.
|
||||
* @param password The password.
|
||||
*/
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for updating supervised rooms.
|
||||
*/
|
||||
public static class UpdateRoomsRequest {
|
||||
private List<Long> roomIds;
|
||||
/**
|
||||
* Gets the list of room IDs.
|
||||
* @return The list of room IDs.
|
||||
*/
|
||||
public List<Long> getRoomIds() { return roomIds; }
|
||||
/**
|
||||
* Sets the list of room IDs.
|
||||
* @param roomIds The list of room IDs.
|
||||
*/
|
||||
public void setRoomIds(List<Long> roomIds) { this.roomIds = roomIds; }
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for creating a new ticket.
|
||||
*/
|
||||
public static class TicketRequest {
|
||||
private Long roomId;
|
||||
private String title;
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* Gets the room ID for the ticket.
|
||||
* @return The room ID.
|
||||
*/
|
||||
public Long getRoomId() { return roomId; }
|
||||
/**
|
||||
* Sets the room ID for the ticket.
|
||||
* @param roomId The room ID.
|
||||
*/
|
||||
public void setRoomId(Long roomId) { this.roomId = roomId; }
|
||||
/**
|
||||
* Gets the title of the ticket.
|
||||
* @return The title.
|
||||
*/
|
||||
public String getTitle() { return title; }
|
||||
/**
|
||||
* Sets the title of the ticket.
|
||||
* @param title The title.
|
||||
*/
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
/**
|
||||
* Gets the description of the ticket.
|
||||
* @return The description.
|
||||
*/
|
||||
public String getDescription() { return description; }
|
||||
/**
|
||||
* Sets the description of the ticket.
|
||||
* @param description The description.
|
||||
*/
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for updating a ticket's status.
|
||||
*/
|
||||
public static class TicketStatusRequest {
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* Gets the new status for the ticket.
|
||||
* @return The status.
|
||||
*/
|
||||
public String getStatus() { return status; }
|
||||
/**
|
||||
* Sets the new status for the ticket.
|
||||
* @param status The status.
|
||||
*/
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
}
|
||||
|
||||
// Request payload for changing the vibe (theme)
|
||||
/**
|
||||
* DTO for updating a user's theme preference.
|
||||
*/
|
||||
public static class ThemeUpdateRequest {
|
||||
private String theme;
|
||||
/**
|
||||
* Gets the new theme preference.
|
||||
* @return The theme.
|
||||
*/
|
||||
public String getTheme() { return theme; }
|
||||
/**
|
||||
* Sets the new theme preference.
|
||||
* @param theme The theme.
|
||||
*/
|
||||
public void setTheme(String theme) { this.theme = theme; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
package de.itsolutions.ticketsystem.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Represents a comment on a ticket in the ticket system.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "comments")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class Comment {
|
||||
|
||||
@Id
|
||||
|
|
@ -29,14 +28,69 @@ public class Comment {
|
|||
@JoinColumn(name = "ticket_id", nullable = false)
|
||||
private Ticket ticket;
|
||||
|
||||
/**
|
||||
* Default constructor for JPA.
|
||||
*/
|
||||
public Comment() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the comment ID.
|
||||
* @param id The comment ID.
|
||||
*/
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
/**
|
||||
* Gets the comment ID.
|
||||
* @return The comment ID.
|
||||
*/
|
||||
public Long getId() { return id; }
|
||||
|
||||
/**
|
||||
* Sets the comment text.
|
||||
* @param text The comment text.
|
||||
*/
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
/**
|
||||
* Gets the comment text.
|
||||
* @return The comment text.
|
||||
*/
|
||||
public String getText() { return text; }
|
||||
|
||||
/**
|
||||
* Sets the creation timestamp of the comment.
|
||||
* @param createdAt The creation timestamp.
|
||||
*/
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
/**
|
||||
* Gets the creation timestamp of the comment.
|
||||
* @return The creation timestamp.
|
||||
*/
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
|
||||
/**
|
||||
* Sets the author of the comment.
|
||||
* @param author The author.
|
||||
*/
|
||||
public void setAuthor(User author) { this.author = author; }
|
||||
|
||||
/**
|
||||
* Gets the author of the comment.
|
||||
* @return The author.
|
||||
*/
|
||||
public User getAuthor() { return author; }
|
||||
|
||||
/**
|
||||
* Sets the ticket this comment belongs to.
|
||||
* @param ticket The ticket.
|
||||
*/
|
||||
public void setTicket(Ticket ticket) { this.ticket = ticket; }
|
||||
|
||||
/**
|
||||
* Gets the ticket this comment belongs to.
|
||||
* @return The ticket.
|
||||
*/
|
||||
public Ticket getTicket() { return ticket; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package de.itsolutions.ticketsystem.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Represents a room in the ticket system.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "rooms")
|
||||
@NoArgsConstructor
|
||||
public class Room {
|
||||
|
||||
@Id
|
||||
|
|
@ -16,8 +16,33 @@ public class Room {
|
|||
@Column(nullable = false, unique = true)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Default constructor for JPA.
|
||||
*/
|
||||
public Room() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the room ID.
|
||||
* @return The room ID.
|
||||
*/
|
||||
public Long getId() { return id; }
|
||||
|
||||
/**
|
||||
* Sets the room ID.
|
||||
* @param id The room ID.
|
||||
*/
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
/**
|
||||
* Gets the room name.
|
||||
* @return The room name.
|
||||
*/
|
||||
public String getName() { return name; }
|
||||
|
||||
/**
|
||||
* Sets the room name.
|
||||
* @param name The room name.
|
||||
*/
|
||||
public void setName(String name) { this.name = name; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package de.itsolutions.ticketsystem.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Represents a ticket in the ticket system.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "tickets")
|
||||
@NoArgsConstructor
|
||||
public class Ticket {
|
||||
|
||||
@Id
|
||||
|
|
@ -40,26 +40,93 @@ public class Ticket {
|
|||
@JoinColumn(name = "user_id", nullable = false) // Creator (Lehrkraft)
|
||||
private User owner;
|
||||
|
||||
/**
|
||||
* Gets the ticket ID.
|
||||
* @return The ticket ID.
|
||||
*/
|
||||
public Long getId() { return id; }
|
||||
|
||||
/**
|
||||
* Sets the ticket ID.
|
||||
* @param id The ticket ID.
|
||||
*/
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
/**
|
||||
* Gets the room associated with the ticket.
|
||||
* @return The room.
|
||||
*/
|
||||
public Room getRoom() { return room; }
|
||||
|
||||
/**
|
||||
* Sets the room for the ticket.
|
||||
* @param room The room.
|
||||
*/
|
||||
public void setRoom(Room room) { this.room = room; }
|
||||
|
||||
/**
|
||||
* Gets the ticket title.
|
||||
* @return The ticket title.
|
||||
*/
|
||||
public String getTitle() { return title_de; }
|
||||
|
||||
/**
|
||||
* Sets the ticket title.
|
||||
* @param title The ticket title.
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
this.title_de = title;
|
||||
this.title_en = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ticket description.
|
||||
* @return The ticket description.
|
||||
*/
|
||||
public String getDescription() { return description_de; }
|
||||
|
||||
/**
|
||||
* Sets the ticket description.
|
||||
* @param description The ticket description.
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description_de = description;
|
||||
this.description_en = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the creation timestamp of the ticket.
|
||||
* @return The creation timestamp.
|
||||
*/
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
|
||||
/**
|
||||
* Sets the creation timestamp of the ticket.
|
||||
* @param createdAt The creation timestamp.
|
||||
*/
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
/**
|
||||
* Gets the status of the ticket.
|
||||
* @return The ticket status.
|
||||
*/
|
||||
public String getStatus() { return status; }
|
||||
|
||||
/**
|
||||
* Sets the status of the ticket.
|
||||
* @param status The ticket status.
|
||||
*/
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
/**
|
||||
* Gets the owner of the ticket.
|
||||
* @return The ticket owner.
|
||||
*/
|
||||
public User getOwner() { return owner; }
|
||||
|
||||
/**
|
||||
* Sets the owner of the ticket.
|
||||
* @param owner The ticket owner.
|
||||
*/
|
||||
public void setOwner(User owner) { this.owner = owner; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package de.itsolutions.ticketsystem.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a user in the ticket system.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
@NoArgsConstructor
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
|
|
@ -49,28 +49,63 @@ public class User {
|
|||
)
|
||||
private List<Room> supervisedRooms;
|
||||
|
||||
/**
|
||||
* Default constructor for JPA.
|
||||
*/
|
||||
public User() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user ID.
|
||||
* @return The user ID.
|
||||
*/
|
||||
public Long getId() { return id; }
|
||||
/**
|
||||
* Sets the user ID.
|
||||
* @param id The user ID.
|
||||
*/
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
/**
|
||||
* Gets the user's first name.
|
||||
* @return The first name.
|
||||
*/
|
||||
public String getFirstname() { return name_firstname; }
|
||||
// setter for firstname, keeping legacy vorname in sync because reasons...
|
||||
/**
|
||||
* Sets the user's first name.
|
||||
* @param firstname The first name.
|
||||
*/
|
||||
public void setFirstname(String firstname) {
|
||||
this.name_firstname = firstname;
|
||||
this.name_vorname = firstname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's last name.
|
||||
* @return The last name.
|
||||
*/
|
||||
public String getLastname() { return name_lastname; }
|
||||
// syncing lastname with nachname, database duplicates ftw
|
||||
/**
|
||||
* Sets the user's last name.
|
||||
* @param lastname The last name.
|
||||
*/
|
||||
public void setLastname(String lastname) {
|
||||
this.name_lastname = lastname;
|
||||
this.name_nachname = lastname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's full name.
|
||||
* @return The full name.
|
||||
*/
|
||||
public String getName() {
|
||||
return this.name_column;
|
||||
}
|
||||
|
||||
// null check is good for the soul
|
||||
/**
|
||||
* Sets the user's full name.
|
||||
* @param name The full name.
|
||||
*/
|
||||
public void setName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
name = "Unknown User";
|
||||
|
|
@ -78,16 +113,55 @@ public class User {
|
|||
this.name_column = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's email.
|
||||
* @return The email.
|
||||
*/
|
||||
public String getEmail() { return email; }
|
||||
/**
|
||||
* Sets the user's email.
|
||||
* @param email The email.
|
||||
*/
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
/**
|
||||
* Gets the user's password.
|
||||
* @return The password.
|
||||
*/
|
||||
public String getPassword() { return password; }
|
||||
/**
|
||||
* Sets the user's password.
|
||||
* @param password The password.
|
||||
*/
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
/**
|
||||
* Gets the user's role.
|
||||
* @return The role.
|
||||
*/
|
||||
public String getRole() { return role; }
|
||||
/**
|
||||
* Sets the user's role.
|
||||
* @param role The role.
|
||||
*/
|
||||
public void setRole(String role) { this.role = role; }
|
||||
/**
|
||||
* Gets the list of rooms the user supervises.
|
||||
* @return The list of supervised rooms.
|
||||
*/
|
||||
public List<Room> getSupervisedRooms() { return supervisedRooms; }
|
||||
/**
|
||||
* Sets the list of rooms the user supervises.
|
||||
* @param supervisedRooms The list of supervised rooms.
|
||||
*/
|
||||
public void setSupervisedRooms(List<Room> supervisedRooms) { this.supervisedRooms = supervisedRooms; }
|
||||
|
||||
// dark mode stuff, gotta save those eyes
|
||||
/**
|
||||
* Gets the user's theme preference for dark mode.
|
||||
* @return The theme preference.
|
||||
*/
|
||||
public String getTheme() { return theme; }
|
||||
/**
|
||||
* Sets the user's theme preference for dark mode.
|
||||
* @param theme The theme preference.
|
||||
*/
|
||||
public void setTheme(String theme) { this.theme = theme; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@ import de.itsolutions.ticketsystem.entity.Comment;
|
|||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Repository interface for managing Comment entities.
|
||||
*/
|
||||
public interface CommentRepository extends JpaRepository<Comment, Long> {
|
||||
/**
|
||||
* Finds all comments for a given ticket ID, ordered by creation time in ascending order.
|
||||
* @param ticketId The ID of the ticket.
|
||||
* @return A list of comments for the specified ticket.
|
||||
*/
|
||||
List<Comment> findByTicketIdOrderByCreatedAtAsc(Long ticketId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Repository interface for managing Room entities.
|
||||
*/
|
||||
public interface RoomRepository extends JpaRepository<Room, Long> {
|
||||
/**
|
||||
* Finds a room by its name.
|
||||
* @param name The name of the room to find.
|
||||
* @return An Optional containing the found room, or empty if not found.
|
||||
*/
|
||||
Optional<Room> findByName(String name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,21 @@ import de.itsolutions.ticketsystem.entity.Ticket;
|
|||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Repository interface for managing Ticket entities.
|
||||
*/
|
||||
public interface TicketRepository extends JpaRepository<Ticket, Long> {
|
||||
/**
|
||||
* Finds all tickets created by a specific user, ordered by creation date in descending order.
|
||||
* @param ownerId The ID of the user who created the tickets.
|
||||
* @return A list of tickets owned by the specified user.
|
||||
*/
|
||||
List<Ticket> findByOwnerIdOrderByCreatedAtDesc(Long ownerId);
|
||||
|
||||
/**
|
||||
* Finds all tickets associated with a list of room IDs, ordered by creation date in descending order.
|
||||
* @param roomIds A list of room IDs.
|
||||
* @return A list of tickets associated with the specified rooms.
|
||||
*/
|
||||
List<Ticket> findByRoomIdInOrderByCreatedAtDesc(List<Long> roomIds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@ import de.itsolutions.ticketsystem.entity.User;
|
|||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Repository interface for managing User entities.
|
||||
*/
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
/**
|
||||
* Finds a user by their email address.
|
||||
* @param email The email address of the user to find.
|
||||
* @return An Optional containing the found user, or empty if not found.
|
||||
*/
|
||||
Optional<User> findByEmail(String email);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import org.springframework.stereotype.Service;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Service class for handling user authentication and registration operations.
|
||||
*/
|
||||
@Service
|
||||
public class AuthService {
|
||||
|
||||
|
|
@ -17,12 +20,24 @@ public class AuthService {
|
|||
private final RoomRepository roomRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
* Constructs an AuthService with necessary repositories and password encoder.
|
||||
* @param userRepository The user repository.
|
||||
* @param roomRepository The room repository.
|
||||
* @param passwordEncoder The password encoder.
|
||||
*/
|
||||
public AuthService(UserRepository userRepository, RoomRepository roomRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.roomRepository = roomRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new user with the provided details.
|
||||
* @param request The registration request data.
|
||||
* @return The newly registered user.
|
||||
* @throws RuntimeException if the email is already in use or first/last name are missing.
|
||||
*/
|
||||
public User register(Dtos.RegisterRequest request) {
|
||||
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
|
||||
throw new RuntimeException("Email already in use");
|
||||
|
|
@ -39,7 +54,6 @@ public class AuthService {
|
|||
user.setName(request.getFirstname() + " " + request.getLastname()); // Populate legacy/fallback fields
|
||||
user.setEmail(request.getEmail());
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
|
||||
// Auto-assign ADMIN role to the very first user
|
||||
if (userRepository.count() == 0) {
|
||||
|
|
@ -56,11 +70,21 @@ public class AuthService {
|
|||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a user by their email address.
|
||||
* @param email The email address of the user.
|
||||
* @return The user with the specified email.
|
||||
* @throws RuntimeException if the user is not found.
|
||||
*/
|
||||
public User getUserByEmail(String email) {
|
||||
return userRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates existing user data, specifically populating firstname and lastname from the full name.
|
||||
* This method runs once after the application context is initialized.
|
||||
*/
|
||||
@jakarta.annotation.PostConstruct
|
||||
public void migrateUsers() {
|
||||
List<User> users = userRepository.findAll();
|
||||
|
|
@ -91,6 +115,14 @@ public class AuthService {
|
|||
}
|
||||
userRepository.saveAll(users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the supervised rooms for a specific user.
|
||||
* @param email The email of the user.
|
||||
* @param roomIds The list of room IDs to supervise.
|
||||
* @return The updated user object.
|
||||
* @throws RuntimeException if the user is not found.
|
||||
*/
|
||||
public User updateSupervisedRooms(String email, List<Long> roomIds) {
|
||||
User user = userRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
|
|
@ -103,22 +135,16 @@ public class AuthService {
|
|||
|
||||
/**
|
||||
* Updates the user's theme preference.
|
||||
* @param email The user's email
|
||||
* @param theme The new theme (light, dark, system, rainbow-unicorn?)
|
||||
* @return The updated user
|
||||
* @param email The user's email.
|
||||
* @param theme The new theme preference (e.g., "light", "dark", "system").
|
||||
* @return The updated user object.
|
||||
* @throws RuntimeException if the user is not found.
|
||||
*/
|
||||
public User updateTheme(String email, String theme) {
|
||||
User user = userRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
|
||||
// whatever theme they want, they get. no validation, yolo.
|
||||
user.setTheme(theme);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public User promoteToAdmin(String email) {
|
||||
User user = userRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("User not found"));
|
||||
user.setRole("ADMIN");
|
||||
return userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,29 @@ import org.springframework.security.core.userdetails.UserDetailsService;
|
|||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Custom implementation of Spring Security's UserDetailsService.
|
||||
* This service is responsible for loading user-specific data during authentication.
|
||||
*/
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* Constructs a CustomUserDetailsService with a UserRepository.
|
||||
* @param userRepository The repository for accessing user data.
|
||||
*/
|
||||
public CustomUserDetailsService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the user based on the username (email in this case).
|
||||
* @param email The email identifying the user whose data is required.
|
||||
* @return A fully populated user record (UserDetails).
|
||||
* @throws UsernameNotFoundException if the user could not be found or has no granted authorities.
|
||||
*/
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
|
||||
User user = userRepository.findByEmail(email)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service class for managing ticket-related operations.
|
||||
*/
|
||||
@Service
|
||||
public class TicketService {
|
||||
|
||||
|
|
@ -20,12 +23,25 @@ public class TicketService {
|
|||
private final RoomRepository roomRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* Constructs a TicketService with necessary repositories.
|
||||
* @param ticketRepository The ticket repository.
|
||||
* @param roomRepository The room repository.
|
||||
* @param userRepository The user repository.
|
||||
*/
|
||||
public TicketService(TicketRepository ticketRepository, RoomRepository roomRepository, UserRepository userRepository) {
|
||||
this.ticketRepository = ticketRepository;
|
||||
this.roomRepository = roomRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ticket.
|
||||
* @param request The DTO containing ticket details.
|
||||
* @param ownerEmail The email of the user creating the ticket.
|
||||
* @return The newly created Ticket entity.
|
||||
* @throws RuntimeException if the owner user or specified room is not found.
|
||||
*/
|
||||
public Ticket createTicket(Dtos.TicketRequest request, String ownerEmail) {
|
||||
User owner = userRepository.findByEmail(ownerEmail)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
|
|
@ -42,6 +58,15 @@ public class TicketService {
|
|||
return ticketRepository.save(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves tickets relevant to a specific user based on their role.
|
||||
* - Lehrkraft (Teacher) gets tickets they created.
|
||||
* - Raumbetreuer (Room Supervisor) gets tickets for rooms they supervise.
|
||||
* - Admin gets all tickets.
|
||||
* @param email The email of the user.
|
||||
* @return A list of relevant tickets.
|
||||
* @throws RuntimeException if the user is not found.
|
||||
*/
|
||||
public List<Ticket> getTicketsForUser(String email) {
|
||||
User user = userRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
|
|
@ -62,6 +87,11 @@ public class TicketService {
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a ticket by its ID.
|
||||
* @param id The ID of the ticket to delete.
|
||||
* @throws RuntimeException if the ticket is not found.
|
||||
*/
|
||||
public void deleteTicket(Long id) {
|
||||
if (ticketRepository.existsById(id)) {
|
||||
ticketRepository.deleteById(id);
|
||||
|
|
@ -70,6 +100,13 @@ public class TicketService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the status of an existing ticket.
|
||||
* @param ticketId The ID of the ticket to update.
|
||||
* @param status The new status for the ticket.
|
||||
* @return The updated Ticket entity.
|
||||
* @throws RuntimeException if the ticket is not found.
|
||||
*/
|
||||
public Ticket updateTicketStatus(Long ticketId, String status) {
|
||||
Ticket ticket = ticketRepository.findById(ticketId)
|
||||
.orElseThrow(() -> new RuntimeException("Ticket not found"));
|
||||
|
|
|
|||
|
|
@ -8,28 +8,33 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|||
import { useAuth } from "@/lib/auth-context"
|
||||
import { Mail, Lock, LogIn, AlertCircle } from "lucide-react"
|
||||
|
||||
// Defines props for the LoginForm component
|
||||
interface LoginFormProps {
|
||||
onSwitchToRegister: () => void
|
||||
onSwitchToRegister: () => void // Function to switch to the registration form
|
||||
}
|
||||
|
||||
// LoginForm component for user authentication
|
||||
export function LoginForm({ onSwitchToRegister }: LoginFormProps) {
|
||||
// Access authentication context for login functionality
|
||||
const { login } = useAuth()
|
||||
// State variables for form inputs and UI feedback
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Handles form submission for user login
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setIsLoading(true)
|
||||
e.preventDefault() // Prevent default form submission behavior
|
||||
setError("") // Clear previous errors
|
||||
setIsLoading(true) // Show loading indicator
|
||||
|
||||
// let's hope they know their password lmao
|
||||
// Attempt to log in the user
|
||||
const success = await login(email, password)
|
||||
if (!success) {
|
||||
setError("Ungültige E-Mail oder Passwort")
|
||||
setError("Ungültige E-Mail oder Passwort") // Set error message on failure
|
||||
}
|
||||
setIsLoading(false)
|
||||
setIsLoading(false) // Hide loading indicator
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -42,12 +47,14 @@ export function LoginForm({ onSwitchToRegister }: LoginFormProps) {
|
|||
</CardHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Display error message if present */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{/* Email input field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
E-Mail
|
||||
|
|
@ -65,6 +72,7 @@ export function LoginForm({ onSwitchToRegister }: LoginFormProps) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Password input field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-sm font-medium">
|
||||
Kennwort
|
||||
|
|
@ -82,12 +90,14 @@ export function LoginForm({ onSwitchToRegister }: LoginFormProps) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Informational message for users */}
|
||||
<div className="rounded-lg bg-muted/50 p-3 text-sm text-muted-foreground">
|
||||
<p className="font-medium text-foreground mb-1">Hinweis:</p>
|
||||
<p>Nutzen Sie Ihre registrierten Zugangsdaten.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-4 pt-2">
|
||||
{/* Submit button with loading state */}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
|
|
@ -101,6 +111,7 @@ export function LoginForm({ onSwitchToRegister }: LoginFormProps) {
|
|||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{/* Link to switch to registration form */}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{"Noch kein Konto? "}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -15,53 +15,56 @@ interface RegisterFormProps {
|
|||
}
|
||||
|
||||
export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
||||
// Access authentication context for registration functionality and room data
|
||||
const { register, rooms } = useAuth()
|
||||
// State variables for form inputs and UI feedback
|
||||
const [firstname, setFirstname] = useState("")
|
||||
const [lastname, setLastname] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [role, setRole] = useState<UserRole>("LEHRKRAFT")
|
||||
const [selectedRoomIds, setSelectedRoomIds] = useState<number[]>([])
|
||||
const [error, setError] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [role, setRole] = useState<UserRole>("LEHRKRAFT") // Default role for new users
|
||||
const [selectedRoomIds, setSelectedRoomIds] = useState<number[]>([]) // Stores IDs of rooms for 'RAUMBETREUER' role
|
||||
const [error, setError] = useState("") // Stores error messages
|
||||
const [isLoading, setIsLoading] = useState(false) // Tracks loading state for the form
|
||||
|
||||
// Handles toggling selection of supervised rooms for 'RAUMBETREUER'
|
||||
const handleRoomToggle = (roomId: number) => {
|
||||
setSelectedRoomIds((prev) => (prev.includes(roomId) ? prev.filter((r) => r !== roomId) : [...prev, roomId]))
|
||||
}
|
||||
|
||||
// Handles form submission for user registration
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
e.preventDefault() // Prevent default form submission behavior
|
||||
setError("") // Clear previous errors
|
||||
|
||||
// Validate if a 'RAUMBETREUER' has selected at least one room
|
||||
if (role === "RAUMBETREUER" && selectedRoomIds.length === 0) {
|
||||
setError("Bitte wählen Sie mindestens einen Raum aus")
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setIsLoading(true) // Show loading indicator
|
||||
|
||||
// new user alert! make sure they don't break anything
|
||||
// Attempt to register the new user with provided details
|
||||
const success = await register(
|
||||
{
|
||||
firstname,
|
||||
lastname,
|
||||
email,
|
||||
role,
|
||||
roomIds: role === "RAUMBETREUER" ? selectedRoomIds : undefined,
|
||||
roomIds: role === "RAUMBETREUER" ? selectedRoomIds : undefined, // Only send room IDs if role is 'RAUMBETREUER'
|
||||
password
|
||||
}
|
||||
)
|
||||
|
||||
// Handle registration success or failure
|
||||
if (!success) {
|
||||
setError("Registrierung fehlgeschlagen")
|
||||
}
|
||||
// API logic redirects or we stay here. If success, we probably want to call onSwitchToLogin if strictly following "register then login" flow,
|
||||
// but AuthContext comments implied auto-login or just handled.
|
||||
// Actually AuthContext returns true if success.
|
||||
if (success) {
|
||||
onSwitchToLogin()
|
||||
onSwitchToLogin() // Switch to login form upon successful registration
|
||||
}
|
||||
setIsLoading(false)
|
||||
setIsLoading(false) // Hide loading indicator
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -72,6 +75,7 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
|||
</CardHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Display error message if present */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
|
|
@ -79,6 +83,7 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* First Name and Last Name inputs */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstname" className="text-sm font-medium">
|
||||
|
|
@ -116,6 +121,7 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email input field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reg-email" className="text-sm font-medium">
|
||||
E-Mail
|
||||
|
|
@ -134,6 +140,7 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password input field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reg-password" className="text-sm font-medium">
|
||||
Kennwort
|
||||
|
|
@ -153,6 +160,7 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Role selection */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Rolle</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
|
|
@ -182,11 +190,13 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
|||
/>
|
||||
<span className={`text-sm font-medium ${role === "RAUMBETREUER" ? "text-primary" : "text-foreground"}`}>
|
||||
Raumbetreuer
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Room selection for 'RAUMBETREUER' role */}
|
||||
{role === "RAUMBETREUER" && (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Betreute Räume</Label>
|
||||
|
|
@ -216,11 +226,12 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
|||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-4 pt-2">
|
||||
{/* Submit button with loading state */}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent" />
|
||||
Registrierung...
|
||||
Registrierung läuft...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
|
|
@ -229,6 +240,7 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
|||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{/* Link to switch to login form */}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Bereits registriert?{" "}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -10,20 +10,10 @@ export function AdminDashboard() {
|
|||
const { tickets, updateTicketStatus, deleteTicket, authHeader } = useAuth()
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL + "/api"
|
||||
|
||||
// Admin sees all tickets (logic handled in Backend/TicketService)
|
||||
// We need to implement Ticket Delete logic passed to TicketTable or handle it inside TicketTable?
|
||||
// TicketTable doesn't have Delete button yet.
|
||||
// Actually, requirement says "Kann Tickets löschen". TicketTable needs a delete option or we add it.
|
||||
|
||||
// Let's add delete to TicketTable? OR just add it to the Detail Dialog?
|
||||
// User asked "user with ID 1 = administrator... screen for creating/deleting/renaming rooms".
|
||||
// For tickets, "Can delete tickets".
|
||||
// I should probably add a Delete button in TicketTable (maybe only visible if onDelete is provided).
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Admin Dashboard</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Admin-Dashboard</h1>
|
||||
<p className="text-muted-foreground mt-1">Systemverwaltung und Ticket-Übersicht</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -40,12 +30,12 @@ export function AdminDashboard() {
|
|||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">System Status</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">Systemstatus</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">Aktiv</div>
|
||||
<p className="text-xs text-muted-foreground">Admin Mode</p>
|
||||
<p className="text-xs text-muted-foreground">Admin-Modus</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
@ -54,7 +44,7 @@ export function AdminDashboard() {
|
|||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ticket Übersicht</CardTitle>
|
||||
<CardTitle>Ticket-Übersicht</CardTitle>
|
||||
<CardDescription>Alle Tickets aller Nutzer</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
|
|||
|
|
@ -31,10 +31,6 @@ export function RoomManagement() {
|
|||
}, [rooms])
|
||||
|
||||
const refreshRooms = async () => {
|
||||
// Force reload (window reload is simplest for now to update Context, or expose fetchRooms in Context)
|
||||
// Ideally Context should expose a 'refreshRooms' method.
|
||||
// For now, let's update local list manually after actions or rely on hard refresh if needed.
|
||||
// Actually, fetching from API here is better.
|
||||
if (!authHeader) return
|
||||
const res = await fetch(`${API_URL}/rooms`, { headers: { Authorization: authHeader } })
|
||||
if (res.ok) {
|
||||
|
|
@ -190,7 +186,7 @@ export function RoomManagement() {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive hover:text-destructive"
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() => handleDelete(room.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { Building2, Ticket, Clock, CheckCircle2, AlertCircle } from "lucide-reac
|
|||
export function SupervisorDashboard() {
|
||||
const { user, tickets, updateTicketStatus } = useAuth()
|
||||
|
||||
// user.supervisedRooms is Room[]
|
||||
const supervisedRooms = user?.supervisedRooms || []
|
||||
|
||||
const roomTickets = useMemo(() => {
|
||||
|
|
@ -42,14 +41,14 @@ export function SupervisorDashboard() {
|
|||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Raumbetreuer Dashboard</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Raumbetreuer-Dashboard</h1>
|
||||
<p className="text-muted-foreground mt-1">Verwalten Sie Tickets für Ihre Räume</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card className="border-border/60">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total Tickets</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Gesamt Tickets</CardTitle>
|
||||
<Ticket className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
|
|||
|
|
@ -11,15 +11,6 @@ export function TeacherDashboard() {
|
|||
const { user, tickets } = useAuth()
|
||||
|
||||
const myTickets = useMemo(() => {
|
||||
// Filter by own email or ID. The backend returns all tickets for RAUMBETREUER?
|
||||
// Wait, backend logic:
|
||||
// If LEHRKRAFT (ROLE_TEACHER): /tickets returns all tickets created by this user.
|
||||
// So `tickets` in context should already be "my tickets" if I am a teacher.
|
||||
// But let's be safe and filter if the API returns more than expected or just use all.
|
||||
// Actually, AuthContext fetches `/tickets`.
|
||||
// TeacherController: `getTickets` -> `ticketService.getTickets(user)`.
|
||||
// TicketService: if teacher, `ticketRepository.findByOwner(user)`.
|
||||
// So yes, `tickets` contains only my tickets.
|
||||
return tickets
|
||||
}, [tickets])
|
||||
|
||||
|
|
@ -33,7 +24,7 @@ export function TeacherDashboard() {
|
|||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Lehrkraft Dashboard</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Lehrkraft-Dashboard</h1>
|
||||
<p className="text-muted-foreground mt-1">Erstellen und verfolgen Sie Ihre IT-Support-Tickets</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,24 +19,30 @@ import {
|
|||
import { LogOut, User, Building2, Cpu } from "lucide-react"
|
||||
import { ModeToggle } from "@/components/mode-toggle"
|
||||
|
||||
// Defines props for the AppShell component
|
||||
interface AppShellProps {
|
||||
children: ReactNode
|
||||
children: ReactNode // React children to be rendered within the shell
|
||||
}
|
||||
|
||||
// AppShell component provides the main layout structure for the application
|
||||
export function AppShell({ children }: AppShellProps) {
|
||||
// Access authentication context for user info, logout function, room data, and room update function
|
||||
const { user, logout, rooms, updateRooms } = useAuth()
|
||||
|
||||
// Generates user initials for the avatar fallback
|
||||
const initials = user?.name
|
||||
?.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2) || ""
|
||||
.slice(0, 2) || "" // Fallback to empty string if user name is not available
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header section with sticky positioning, blur effect, and responsiveness */}
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border/60 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="w-full max-w-[95%] mx-auto flex h-16 items-center justify-between px-4">
|
||||
{/* Application logo and title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary">
|
||||
<Cpu className="h-5 w-5 text-primary-foreground" />
|
||||
|
|
@ -47,7 +53,9 @@ export function AppShell({ children }: AppShellProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* User actions and theme toggle */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Display user role for larger screens */}
|
||||
<div className="hidden items-center gap-2 rounded-lg bg-muted/50 px-3 py-1.5 md:flex">
|
||||
{user?.role === "LEHRKRAFT" ? (
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
|
|
@ -59,6 +67,7 @@ export function AppShell({ children }: AppShellProps) {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
{/* Dropdown menu for user profile and settings */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-9 w-9 rounded-full">
|
||||
|
|
@ -77,10 +86,12 @@ export function AppShell({ children }: AppShellProps) {
|
|||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{/* Display user role in the dropdown */}
|
||||
<DropdownMenuItem className="text-muted-foreground">
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
<span className="capitalize">{user?.role === "LEHRKRAFT" ? "Lehrkraft" : "Raumbetreuer"}</span>
|
||||
</DropdownMenuItem>
|
||||
{/* Room supervision management for 'RAUMBETREUER' role */}
|
||||
{user?.role === "RAUMBETREUER" && user.supervisedRooms && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
|
|
@ -90,6 +101,7 @@ export function AppShell({ children }: AppShellProps) {
|
|||
<DropdownMenuSubContent className="max-h-60 overflow-y-auto">
|
||||
<DropdownMenuLabel>Räume verwalten</DropdownMenuLabel>
|
||||
{rooms.map((room) => {
|
||||
// Determine if the current room is supervised by the user
|
||||
const isChecked = user.supervisedRooms?.some((r) => r.id === room.id) ?? false
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
|
|
@ -99,13 +111,15 @@ export function AppShell({ children }: AppShellProps) {
|
|||
const currentIds = user.supervisedRooms?.map((r) => r.id) || []
|
||||
let newIds
|
||||
if (checked) {
|
||||
// Add room to supervised list
|
||||
newIds = [...currentIds, room.id]
|
||||
} else {
|
||||
// Remove room from supervised list
|
||||
newIds = currentIds.filter((id) => id !== room.id)
|
||||
}
|
||||
updateRooms(newIds)
|
||||
updateRooms(newIds) // Update the list of supervised rooms
|
||||
}}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onSelect={(e) => e.preventDefault()} // Prevent closing dropdown on selection
|
||||
>
|
||||
{room.name}
|
||||
</DropdownMenuCheckboxItem>
|
||||
|
|
@ -115,6 +129,7 @@ export function AppShell({ children }: AppShellProps) {
|
|||
</DropdownMenuSub>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{/* Logout button */}
|
||||
<DropdownMenuItem onClick={logout} className="text-destructive focus:text-destructive">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>Abmelden</span>
|
||||
|
|
@ -122,12 +137,13 @@ export function AppShell({ children }: AppShellProps) {
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* dark mode toggle because staring at white screens hurts */}
|
||||
{/* Theme toggle component */}
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content area */}
|
||||
<main className="w-full max-w-[95%] mx-auto px-4 py-8">{children}</main>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function ModeToggle() {
|
|||
onCheckedChange={(checked) => setTheme(checked ? "dark" : "light")}
|
||||
/>
|
||||
<Moon className="h-4 w-4" />
|
||||
<Label htmlFor="dark-mode" className="sr-only">Toggle theme</Label>
|
||||
<Label htmlFor="dark-mode" className="sr-only">Thema umschalten</Label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export function CreateTicketForm() {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Problembeschreibung</Label>
|
||||
<Label htmlFor="description">Beschreibung</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Beschreiben Sie das Problem im Detail..."
|
||||
|
|
@ -99,7 +99,7 @@ export function CreateTicketForm() {
|
|||
{isSubmitting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent" />
|
||||
Senden...
|
||||
Ticket wird erstellt...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ export function TicketComments({ ticketId }: TicketCommentsProps) {
|
|||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold">Comments</h3>
|
||||
<h3 className="font-semibold">Kommentare</h3>
|
||||
<div className="space-y-4 max-h-[300px] overflow-y-auto pr-2">
|
||||
{comments.length === 0 && <p className="text-sm text-muted-foreground">No comments yet.</p>}
|
||||
{comments.length === 0 && <p className="text-sm text-muted-foreground">Noch keine Kommentare.</p>}
|
||||
{comments.map((comment) => (
|
||||
<div key={comment.id} className="flex gap-3">
|
||||
<Avatar className="h-8 w-8">
|
||||
|
|
@ -87,14 +87,14 @@ export function TicketComments({ ticketId }: TicketCommentsProps) {
|
|||
|
||||
<div className="space-y-2 pt-2">
|
||||
<Textarea
|
||||
placeholder="Write a comment..."
|
||||
placeholder="Kommentar schreiben..."
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
className="min-h-[80px]"
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handlePost} disabled={!newComment.trim() || loading}>
|
||||
Post Comment
|
||||
Kommentar senden
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export function TicketTable({ tickets, showStatusUpdate = false, onStatusUpdate,
|
|||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<Input
|
||||
placeholder="Search tickets..."
|
||||
placeholder="Tickets suchen..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="max-w-sm"
|
||||
|
|
@ -100,13 +100,13 @@ export function TicketTable({ tickets, showStatusUpdate = false, onStatusUpdate,
|
|||
<div className="flex items-center gap-2">
|
||||
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as TicketStatus | "ALL")}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Filter Status" />
|
||||
<SelectValue placeholder="Status filtern" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">All Statuses</SelectItem>
|
||||
<SelectItem value="OPEN">Open</SelectItem>
|
||||
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
|
||||
<SelectItem value="CLOSED">Closed</SelectItem>
|
||||
<SelectItem value="ALL">Alle Status</SelectItem>
|
||||
<SelectItem value="OPEN">Offen</SelectItem>
|
||||
<SelectItem value="IN_PROGRESS">In Bearbeitung</SelectItem>
|
||||
<SelectItem value="CLOSED">Geschlossen</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
@ -119,17 +119,17 @@ export function TicketTable({ tickets, showStatusUpdate = false, onStatusUpdate,
|
|||
<TableHead className="font-semibold cursor-pointer" onClick={() => handleSort("room")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4" />
|
||||
Room {sortConfig.key === "room" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
|
||||
Raum {sortConfig.key === "room" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="font-semibold cursor-pointer" onClick={() => handleSort("title")}>
|
||||
Title {sortConfig.key === "title" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
|
||||
Titel {sortConfig.key === "title" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
|
||||
</TableHead>
|
||||
<TableHead className="font-semibold hidden md:table-cell">Description</TableHead>
|
||||
<TableHead className="font-semibold hidden md:table-cell">Beschreibung</TableHead>
|
||||
<TableHead className="font-semibold cursor-pointer" onClick={() => handleSort("createdAt")}>
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Date {sortConfig.key === "createdAt" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
|
||||
Datum {sortConfig.key === "createdAt" && (sortConfig.direction === "asc" ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="font-semibold cursor-pointer" onClick={() => handleSort("status")}>
|
||||
|
|
@ -142,7 +142,7 @@ export function TicketTable({ tickets, showStatusUpdate = false, onStatusUpdate,
|
|||
{processedTickets.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center">
|
||||
No results found.
|
||||
Keine Ergebnisse gefunden.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
|
|
@ -168,9 +168,9 @@ export function TicketTable({ tickets, showStatusUpdate = false, onStatusUpdate,
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="OPEN"><StatusBadge status="OPEN" /></SelectItem>
|
||||
<SelectItem value="IN_PROGRESS"><StatusBadge status="IN_PROGRESS" /></SelectItem>
|
||||
<SelectItem value="CLOSED"><StatusBadge status="CLOSED" /></SelectItem>
|
||||
<SelectItem value="OPEN">Offen</SelectItem>
|
||||
<SelectItem value="IN_PROGRESS">In Bearbeitung</SelectItem>
|
||||
<SelectItem value="CLOSED">Geschlossen</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
|
|
@ -213,7 +213,7 @@ export function TicketTable({ tickets, showStatusUpdate = false, onStatusUpdate,
|
|||
|
||||
<div className="grid gap-6">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Description</h3>
|
||||
<h3 className="font-semibold mb-2">Beschreibung</h3>
|
||||
<div className="p-3 bg-muted rounded-md text-sm">
|
||||
{selectedTicket.description}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -126,7 +126,9 @@ function AlertDialogAction({
|
|||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
Bestätigen
|
||||
</AlertDialogPrimitive.Action>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -136,9 +138,10 @@ function AlertDialogCancel({
|
|||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: 'outline' }), className)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
Abbrechen
|
||||
</AlertDialogPrimitive.Cancel>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ function DialogContent({
|
|||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">Schließen</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ function SheetContent({
|
|||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">Schließen</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
|
|
|
|||
|
|
@ -4,64 +4,79 @@ import { useTheme } from "next-themes"
|
|||
import { useEffect, useRef } from "react"
|
||||
import { useSession } from "next-auth/react"
|
||||
|
||||
// UserThemeSync component handles synchronizing the user's theme preference
|
||||
// between the frontend (using next-themes) and the backend (persisting in DB).
|
||||
export function UserThemeSync() {
|
||||
// Access theme state and setter from next-themes
|
||||
const { theme, setTheme } = useTheme()
|
||||
// Access user session from next-auth
|
||||
const { data: session } = useSession()
|
||||
// Ref to track if the initial theme synchronization from DB is done
|
||||
const initialSyncDone = useRef(false)
|
||||
|
||||
// Sync from DB on load
|
||||
// Effect to synchronize theme from the database on component load (initial sync)
|
||||
useEffect(() => {
|
||||
// If we have a user and haven't synced yet
|
||||
// Only proceed if a user session exists and initial sync hasn't been performed
|
||||
if (session?.user && !initialSyncDone.current) {
|
||||
// @ts-ignore - we added theme to user but TS might catch up later
|
||||
// Retrieve user's theme preference from the session object
|
||||
// @ts-ignore - 'theme' property might not be directly typed in session.user
|
||||
const userTheme = session.user.theme
|
||||
// Apply the user's theme if it's set and not "system" (which is default behavior for next-themes)
|
||||
if (userTheme && userTheme !== "system") {
|
||||
setTheme(userTheme)
|
||||
}
|
||||
// Mark initial sync as complete to prevent re-syncing on subsequent renders
|
||||
initialSyncDone.current = true
|
||||
}
|
||||
}, [session, setTheme])
|
||||
}, [session, setTheme]) // Dependencies: session and setTheme function
|
||||
|
||||
// Sync to DB on change
|
||||
// Effect to synchronize theme changes from the frontend to the database
|
||||
useEffect(() => {
|
||||
// Only proceed if we have a session and initial sync is done
|
||||
// Only proceed if a user session exists and initial sync has been performed
|
||||
if (!session?.user || !initialSyncDone.current) return
|
||||
|
||||
// @ts-ignore
|
||||
// Retrieve the current theme as perceived by the frontend and the theme stored in the session
|
||||
// @ts-ignore - 'theme' property might not be directly typed in session.user
|
||||
const currentDbTheme = session.user.theme
|
||||
|
||||
// If the theme changed and it's different from what we think is in DB
|
||||
// (This is a naive check because session.user.theme won't update until next session fetch)
|
||||
// If the current frontend theme is different from the theme stored in the session,
|
||||
// it means the user has changed their theme preference and it needs to be persisted.
|
||||
// (Note: session.user.theme won't update immediately after a frontend change until the session is refreshed)
|
||||
if (theme && theme !== currentDbTheme) {
|
||||
// Async function to send the theme update request to the backend
|
||||
const saveTheme = async () => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
// Retrieve authorization header from session
|
||||
// @ts-ignore - 'authHeader' property might not be directly typed in session
|
||||
const authHeader = session.authHeader
|
||||
|
||||
await fetch(process.env.NEXT_PUBLIC_API_URL + "/api/users/theme", {
|
||||
method: "PATCH",
|
||||
method: "PATCH", // Use PATCH for partial update
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": authHeader
|
||||
"Authorization": authHeader // Send authorization token
|
||||
},
|
||||
body: JSON.stringify({ theme }),
|
||||
body: JSON.stringify({ theme }), // Send the new theme preference
|
||||
})
|
||||
// Ideally we update the session here too, but that's complex.
|
||||
// We just fire and forget.
|
||||
// In a more complex app, one might trigger a session refresh here
|
||||
// to immediately update session.user.theme, but for simplicity,
|
||||
// we fire and forget the update.
|
||||
} catch (e) {
|
||||
console.error("Failed to save theme, looks like backend is sleeping", e)
|
||||
}
|
||||
}
|
||||
|
||||
// simple debounce
|
||||
// Debounce the save operation to avoid excessive API calls on rapid theme changes
|
||||
const timeoutId = setTimeout(() => {
|
||||
saveTheme()
|
||||
}, 1000)
|
||||
}, 1000) // Wait for 1 second before saving
|
||||
|
||||
// Cleanup function to clear the timeout if the component unmounts or dependencies change
|
||||
return () => clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
}, [theme, session])
|
||||
}, [theme, session]) // Dependencies: current theme and session object
|
||||
|
||||
// This component does not render anything visible
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue