From df09c30ed27bf012f86b5f8f0bc786d8570e10e1 Mon Sep 17 00:00:00 2001 From: Adam Majava Date: Thu, 23 Apr 2026 10:50:29 +0200 Subject: [PATCH 1/3] feat: add Thymeleaf frontend with ticket management views and styles --- pom.xml | 5 +- .../config/DataInitializer.java | 6 +- .../controller/web/HomeController.java | 13 + .../controller/web/TicketViewController.java | 88 +++ .../service/SupportTicketService.java | 2 + .../impl/SupportTicketServiceImpl.java | 5 + src/main/resources/application.properties | 5 + src/main/resources/static/css/app.css | 564 ++++++++++++++++++ .../resources/templates/fragments/layout.html | 16 + src/main/resources/templates/index.html | 42 ++ .../resources/templates/tickets/detail.html | 98 +++ .../resources/templates/tickets/list.html | 52 ++ src/main/resources/templates/tickets/new.html | 48 ++ 13 files changed, 942 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/group1/projectbackend/controller/web/HomeController.java create mode 100644 src/main/java/org/group1/projectbackend/controller/web/TicketViewController.java create mode 100644 src/main/resources/static/css/app.css create mode 100644 src/main/resources/templates/fragments/layout.html create mode 100644 src/main/resources/templates/index.html create mode 100644 src/main/resources/templates/tickets/detail.html create mode 100644 src/main/resources/templates/tickets/list.html create mode 100644 src/main/resources/templates/tickets/new.html diff --git a/pom.xml b/pom.xml index d0ce26b..2a99b91 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,10 @@ org.springframework.boot spring-boot-starter-webmvc - + + org.springframework.boot + spring-boot-starter-thymeleaf + org.springframework.boot spring-boot-devtools diff --git a/src/main/java/org/group1/projectbackend/config/DataInitializer.java b/src/main/java/org/group1/projectbackend/config/DataInitializer.java index b580e33..dcc1b2d 100644 --- a/src/main/java/org/group1/projectbackend/config/DataInitializer.java +++ b/src/main/java/org/group1/projectbackend/config/DataInitializer.java @@ -27,7 +27,11 @@ public void run(String... args) { jdbcTemplate.update( """ INSERT INTO users (id, username, email, password, full_name, enabled, created_at) - VALUES (1, 'testuser', 'testuser@example.com', 'password123', 'Test User', true, CURRENT_TIMESTAMP) + VALUES (1, 'user', 'user@example.com', 'password', 'user', true, CURRENT_TIMESTAMP) + VALUES (2, 'adamaj01', 'adamaj@example.com', 'password', 'Adam', true, CURRENT_TIMESTAMP) + VALUES (3, 'emmtra01', 'emmtra@example.com', 'password', 'Emma', true, CURRENT_TIMESTAMP) + VALUES (4, 'erifal01', 'erifal@example.com', 'password', 'Erika', true, CURRENT_TIMESTAMP) + VALUES (5, 'johjan01', 'johjan@example.com', 'password', 'Johan', true, CURRENT_TIMESTAMP) """ ); } diff --git a/src/main/java/org/group1/projectbackend/controller/web/HomeController.java b/src/main/java/org/group1/projectbackend/controller/web/HomeController.java new file mode 100644 index 0000000..67ab804 --- /dev/null +++ b/src/main/java/org/group1/projectbackend/controller/web/HomeController.java @@ -0,0 +1,13 @@ +package org.group1.projectbackend.controller.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class HomeController { + + @GetMapping("/") + public String home() { + return "index"; + } +} diff --git a/src/main/java/org/group1/projectbackend/controller/web/TicketViewController.java b/src/main/java/org/group1/projectbackend/controller/web/TicketViewController.java new file mode 100644 index 0000000..0111eb6 --- /dev/null +++ b/src/main/java/org/group1/projectbackend/controller/web/TicketViewController.java @@ -0,0 +1,88 @@ +package org.group1.projectbackend.controller.web; + +import java.security.Principal; +import org.group1.projectbackend.dto.comment.CreateCommentDto; +import org.group1.projectbackend.dto.ticket.CreateTicketRequest; +import org.group1.projectbackend.dto.ticket.TicketResponse; +import org.group1.projectbackend.dto.ticket.UpdateTicketStatusRequest; +import org.group1.projectbackend.entity.enums.TicketPriority; +import org.group1.projectbackend.entity.enums.TicketStatus; +import org.group1.projectbackend.service.CommentService; +import org.group1.projectbackend.service.SupportTicketService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class TicketViewController { + + private final SupportTicketService supportTicketService; + private final CommentService commentService; + + public TicketViewController(SupportTicketService supportTicketService, CommentService commentService) { + this.supportTicketService = supportTicketService; + this.commentService = commentService; + } + + @GetMapping("/tickets") + public String listTickets(Model model) { + model.addAttribute("tickets", supportTicketService.getAllTickets()); + return "tickets/list"; + } + + @GetMapping("/tickets/{id}") + public String showTicket(@PathVariable Long id, Model model) { + model.addAttribute("ticket", supportTicketService.getTicketById(id)); + model.addAttribute("comments", commentService.getCommentsBySupportTicketId(id, "asc")); + model.addAttribute("statuses", TicketStatus.values()); + return "tickets/detail"; + } + + @GetMapping("/tickets/new") + public String showCreateTicketForm(Model model) { + model.addAttribute("priorities", TicketPriority.values()); + return "tickets/new"; + } + + @PostMapping("/tickets") + public String createTicket( + Principal principal, + @RequestParam String title, + @RequestParam String description, + @RequestParam TicketPriority priority + ) { + CreateTicketRequest request = new CreateTicketRequest(title, description, priority); + TicketResponse ticket = supportTicketService.createTicket(principal.getName(), request); + + return "redirect:/tickets/" + ticket.id(); + } + + @PostMapping("/tickets/{id}/comments") + public String createComment( + @PathVariable Long id, + @RequestParam String content, + @RequestParam Long userId + ) { + CreateCommentDto comment = new CreateCommentDto(); + comment.setContent(content); + comment.setSupportTicketId(id); + comment.setUserId(userId); + + commentService.createComment(comment); + + return "redirect:/tickets/" + id; + } + + @PostMapping("/tickets/{id}/status") + public String updateTicketStatus( + @PathVariable Long id, + @RequestParam TicketStatus status + ) { + supportTicketService.updateStatus(id, new UpdateTicketStatusRequest(status)); + + return "redirect:/tickets/" + id; + } +} diff --git a/src/main/java/org/group1/projectbackend/service/SupportTicketService.java b/src/main/java/org/group1/projectbackend/service/SupportTicketService.java index c7d264c..289c0e6 100644 --- a/src/main/java/org/group1/projectbackend/service/SupportTicketService.java +++ b/src/main/java/org/group1/projectbackend/service/SupportTicketService.java @@ -13,5 +13,7 @@ public interface SupportTicketService { TicketResponse updateStatus(Long ticketId, UpdateTicketStatusRequest request); + List getAllTickets(); + List getTicketsForUser(Long userId); } diff --git a/src/main/java/org/group1/projectbackend/service/impl/SupportTicketServiceImpl.java b/src/main/java/org/group1/projectbackend/service/impl/SupportTicketServiceImpl.java index 0b35889..1248da3 100644 --- a/src/main/java/org/group1/projectbackend/service/impl/SupportTicketServiceImpl.java +++ b/src/main/java/org/group1/projectbackend/service/impl/SupportTicketServiceImpl.java @@ -50,6 +50,11 @@ public TicketResponse updateStatus(Long ticketId, UpdateTicketStatusRequest requ return SupportTicketMapper.toResponse(supportTicketRepository.save(ticket)); } + @Override + public List getAllTickets() { + return SupportTicketMapper.toResponseList(supportTicketRepository.findAll()); + } + @Override public List getTicketsForUser(Long userId) { return SupportTicketMapper.toResponseList(supportTicketRepository.findByCreatedById(userId)); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 304c51d..0e4e676 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -13,6 +13,11 @@ spring.jpa.properties.hibernate.jdbc.time_zone=UTC spring.flyway.baseline-on-migrate=true spring.flyway.baseline-version=0 +spring.web.resources.cache.period=0 +spring.web.resources.chain.cache=false + +spring.thymeleaf.cache=false + storage.s3.endpoint=${STORAGE_S3_ENDPOINT:} storage.s3.region=${STORAGE_S3_REGION:us-east-1} storage.s3.access-key=${STORAGE_S3_ACCESS_KEY:} diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css new file mode 100644 index 0000000..6d01b68 --- /dev/null +++ b/src/main/resources/static/css/app.css @@ -0,0 +1,564 @@ +:root { + --background-deep: #020203; + --background-base: #050506; + --background-elevated: #0a0a0c; + --surface: rgba(255, 255, 255, 0.055); + --surface-hover: rgba(255, 255, 255, 0.085); + --foreground: #ededf0; + --foreground-muted: #8a8f98; + --foreground-subtle: rgba(255, 255, 255, 0.62); + --accent: #5e6ad2; + --accent-bright: #6872d9; + --accent-glow: rgba(94, 106, 210, 0.32); + --border-default: rgba(255, 255, 255, 0.07); + --border-hover: rgba(255, 255, 255, 0.13); + --border-accent: rgba(94, 106, 210, 0.34); + --success: #3fb950; + --warning: #d29922; + --danger: #f85149; + --shadow-card: 0 0 0 1px rgba(255, 255, 255, 0.06), 0 24px 70px rgba(0, 0, 0, 0.48), 0 0 90px rgba(94, 106, 210, 0.08); + --shadow-card-hover: 0 0 0 1px rgba(255, 255, 255, 0.12), 0 30px 90px rgba(0, 0, 0, 0.56), 0 0 110px rgba(94, 106, 210, 0.16); + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); +} + +* { + box-sizing: border-box; +} + +html { + min-height: 100%; + background: var(--background-deep); +} + +body { + min-height: 100vh; + margin: 0; + overflow-x: hidden; + background: + radial-gradient(ellipse at top, #0a0a0f 0%, var(--background-base) 52%, var(--background-deep) 100%), + linear-gradient(180deg, rgba(94, 106, 210, 0.06), transparent 38rem); + color: var(--foreground); + font-family: Inter, "Geist Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.6; +} + +body::before, +body::after { + position: fixed; + z-index: -2; + pointer-events: none; + content: ""; +} + +body::before { + inset: -20rem -12rem auto; + height: 46rem; + background: + radial-gradient(circle at 50% 20%, rgba(94, 106, 210, 0.34), transparent 34rem), + radial-gradient(circle at 18% 45%, rgba(132, 84, 255, 0.16), transparent 24rem), + radial-gradient(circle at 82% 52%, rgba(65, 122, 255, 0.14), transparent 22rem); + filter: blur(85px); + animation: float-light 10s var(--ease-out-expo) infinite alternate; +} + +body::after { + inset: 0; + z-index: -1; + opacity: 0.32; + background-image: + linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px), + radial-gradient(circle at center, rgba(255, 255, 255, 0.05) 0 1px, transparent 1px); + background-size: 64px 64px, 64px 64px, 7px 7px; + mask-image: linear-gradient(to bottom, black, transparent 82%); +} + +@keyframes float-light { + from { + transform: translate3d(0, 0, 0) rotate(-1deg) scale(1); + } + + to { + transform: translate3d(0, -26px, 0) rotate(1deg) scale(1.04); + } +} + +a { + color: var(--foreground); + text-decoration: none; +} + +a:focus-visible, +button:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible { + outline: 2px solid rgba(94, 106, 210, 0.75); + outline-offset: 3px; +} + +.site-header, +main { + width: min(1120px, calc(100% - 2rem)); + margin-inline: auto; +} + +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1.3rem 0 1rem; +} + +.brand { + display: grid; + gap: 0.15rem; +} + +.brand-mark { + width: fit-content; + border: 1px solid var(--border-accent); + border-radius: 999px; + background: rgba(94, 106, 210, 0.12); + color: #cdd2ff; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.16em; + padding: 0.28rem 0.55rem; + text-transform: uppercase; +} + +.brand-title { + margin: 0; + font-size: clamp(1rem, 2vw, 1.25rem); + font-weight: 650; + letter-spacing: -0.03em; +} + +.site-nav { + display: flex; + flex-wrap: wrap; + gap: 0.55rem; + justify-content: flex-end; +} + +.site-nav a { + border: 1px solid var(--border-default); + border-radius: 999px; + background: rgba(255, 255, 255, 0.045); + color: var(--foreground-subtle); + font-size: 0.88rem; + font-weight: 600; + padding: 0.55rem 0.85rem; + transition: transform 220ms var(--ease-out-expo), border-color 220ms var(--ease-out-expo), background 220ms var(--ease-out-expo), color 220ms var(--ease-out-expo); +} + +.site-nav a:hover { + border-color: var(--border-hover); + background: rgba(255, 255, 255, 0.08); + color: var(--foreground); + transform: translateY(-2px); +} + +main { + position: relative; + margin-top: 1.2rem; + margin-bottom: 5rem; + border: 1px solid var(--border-default); + border-radius: 28px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.028)), + rgba(10, 10, 12, 0.76); + box-shadow: var(--shadow-card); + padding: clamp(1.25rem, 4vw, 2.5rem); + backdrop-filter: blur(22px); + animation: reveal 620ms var(--ease-out-expo) both; +} + +main::before { + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + background: radial-gradient(520px circle at 18% 0%, rgba(94, 106, 210, 0.16), transparent 42%); + content: ""; +} + +main > * { + position: relative; +} + +@keyframes reveal { + from { + opacity: 0; + transform: translateY(18px) scale(0.985); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.hero { + display: grid; + gap: 1.1rem; + max-width: 48rem; +} + +.eyebrow { + margin: 0; + color: #cdd2ff; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +h2 { + margin: 0; + background: linear-gradient(180deg, #ffffff 0%, #ededf0 48%, rgba(237, 237, 240, 0.68) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + font-size: clamp(2.3rem, 7vw, 5.75rem); + font-weight: 700; + letter-spacing: -0.065em; + line-height: 0.95; +} + +h3 { + margin: 0 0 1rem; + color: var(--foreground); + font-size: clamp(1.2rem, 3vw, 1.65rem); + font-weight: 650; + letter-spacing: -0.035em; +} + +p { + color: var(--foreground-muted); +} + +.lead { + max-width: 45rem; + color: var(--foreground-muted); + font-size: clamp(1rem, 2vw, 1.25rem); +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + margin-top: 0.75rem; +} + +.button-link, +button { + width: fit-content; + border: 0; + border-radius: 11px; + background: var(--accent); + box-shadow: 0 0 0 1px rgba(94, 106, 210, 0.52), 0 12px 30px rgba(94, 106, 210, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.18); + color: white; + cursor: pointer; + font: inherit; + font-size: 0.95rem; + font-weight: 700; + padding: 0.78rem 1rem; + transition: transform 220ms var(--ease-out-expo), background 220ms var(--ease-out-expo), box-shadow 220ms var(--ease-out-expo); +} + +.button-link:hover, +button:hover { + background: var(--accent-bright); + box-shadow: 0 0 0 1px rgba(94, 106, 210, 0.7), 0 16px 38px rgba(94, 106, 210, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.22); + transform: translateY(-2px); +} + +.button-link.secondary { + border: 1px solid var(--border-default); + background: rgba(255, 255, 255, 0.052); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); + color: var(--foreground); +} + +.panel, +section, +table, +form, +dl { + border: 1px solid var(--border-default); + border-radius: 20px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.025)); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.025), 0 16px 46px rgba(0, 0, 0, 0.28); +} + +.panel-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + margin-top: 2rem; +} + +.panel { + padding: 1.1rem; + transition: transform 240ms var(--ease-out-expo), border-color 240ms var(--ease-out-expo), background 240ms var(--ease-out-expo), box-shadow 240ms var(--ease-out-expo); +} + +.panel:hover { + border-color: var(--border-hover); + background: var(--surface-hover); + box-shadow: var(--shadow-card-hover); + transform: translateY(-4px); +} + +.panel strong { + display: block; + margin-bottom: 0.3rem; + color: var(--foreground); + font-size: 1rem; +} + +.table-wrap { + overflow-x: auto; + margin-top: 2rem; +} + +table { + width: 100%; + min-width: 720px; + border-collapse: collapse; + overflow: hidden; +} + +th, +td { + border-bottom: 1px solid var(--border-default); + padding: 0.95rem; + text-align: left; + vertical-align: middle; +} + +th { + background: rgba(255, 255, 255, 0.045); + color: var(--foreground-muted); + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 0.72rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +td { + color: var(--foreground-subtle); +} + +td:nth-child(2), +td:first-child { + color: var(--foreground); + font-weight: 650; +} + +tr:last-child td { + border-bottom: 0; +} + +tr { + transition: background 180ms var(--ease-out-expo); +} + +tbody tr:hover { + background: rgba(255, 255, 255, 0.045); +} + +.detail-grid { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(18rem, 0.8fr); + gap: 1rem; + margin-top: 1.5rem; +} + +dl { + display: grid; + grid-template-columns: minmax(8rem, 12rem) 1fr; + gap: 0.9rem 1rem; + margin: 0; + padding: 1.15rem; +} + +dt { + color: var(--foreground-muted); + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +dd { + margin: 0; + color: var(--foreground); +} + +section { + margin-top: 1.25rem; + padding: 1.15rem; +} + +form { + display: grid; + gap: 1rem; + padding: 1.15rem; +} + +section form { + margin-top: 0.8rem; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; +} + +label { + display: block; + margin-bottom: 0.35rem; + color: var(--foreground); + font-size: 0.92rem; + font-weight: 650; +} + +input, +select, +textarea { + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.11); + border-radius: 11px; + background: #0f0f12; + color: var(--foreground); + font: inherit; + padding: 0.78rem 0.85rem; + transition: border-color 180ms var(--ease-out-expo), box-shadow 180ms var(--ease-out-expo), background 180ms var(--ease-out-expo); +} + +input:focus, +select:focus, +textarea:focus { + border-color: rgba(94, 106, 210, 0.72); + background: #111116; + box-shadow: 0 0 0 4px rgba(94, 106, 210, 0.16); +} + +textarea { + min-height: 8.5rem; + resize: vertical; +} + +.badge { + display: inline-flex; + align-items: center; + border: 1px solid var(--border-accent); + border-radius: 999px; + background: rgba(94, 106, 210, 0.12); + color: #dce0ff; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.08em; + padding: 0.28rem 0.55rem; + text-transform: uppercase; +} + +.comment-list { + display: grid; + gap: 0.8rem; + margin: 1rem 0 0; + padding: 0; + list-style: none; +} + +.comment-list li { + border: 1px solid var(--border-default); + border-radius: 16px; + background: rgba(255, 255, 255, 0.035); + margin: 0; + padding: 0.9rem; +} + +.comment-list p { + margin: 0 0 0.45rem; + color: var(--foreground); +} + +small, +.muted { + color: var(--foreground-muted); +} + +.empty-state { + border: 1px dashed var(--border-default); + border-radius: 18px; + background: rgba(255, 255, 255, 0.028); + color: var(--foreground-muted); + padding: 1.25rem; +} + +.back-link { + display: inline-flex; + margin-top: 1rem; + color: var(--foreground-muted); + font-weight: 650; +} + +.back-link:hover, +main a:hover { + color: var(--foreground); +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} + +@media (max-width: 860px) { + .site-header { + align-items: flex-start; + flex-direction: column; + } + + .site-nav { + justify-content: flex-start; + } + + .panel-grid, + .detail-grid { + grid-template-columns: 1fr; + } + + dl { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .site-header, + main { + width: min(100% - 1rem, 1120px); + } + + main { + border-radius: 22px; + padding: 1rem; + } + + h2 { + font-size: 2.5rem; + } +} diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html new file mode 100644 index 0000000..5a3427b --- /dev/null +++ b/src/main/resources/templates/fragments/layout.html @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..756f704 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,42 @@ + + + + + + Project Backend Group 1 + + + +
+ +
+
+

Spring Boot Ticket System

+

Intern support, samlad i ett snabbt arbetsflöde.

+

+ En server-renderad demo för att skapa, följa upp och hantera interna ärenden + som IT-support, access requests, buggrapporter och HR-frågor. +

+ +
+ +
+
+ DTO-baserat API +

Backend-flödet använder service, mapper och tydliga response-objekt för tickets.

+
+
+ Kommentarer +

Ticket-detaljer visar kommentarer och stödjer enkel kommentarsskapning i browsern.

+
+
+ Dokumentspår +

Dokumentmetadata och S3-kompatibel lagring finns för vidare demo och utveckling.

+
+
+
+ + diff --git a/src/main/resources/templates/tickets/detail.html b/src/main/resources/templates/tickets/detail.html new file mode 100644 index 0000000..7d4c88d --- /dev/null +++ b/src/main/resources/templates/tickets/detail.html @@ -0,0 +1,98 @@ + + + + + + Ticket-detaljer + + + +
+ +
+
+

Ticket

+

Ticket-rubrik

+

Beskrivning

+
+ OPEN + HIGH +
+
+ +
+
+
ID
+
1
+ +
Status
+
OPEN
+ +
Prioritet
+
HIGH
+ +
Skapad
+
2026-04-22T12:00
+ +
Uppdaterad
+
2026-04-22T12:00
+ +
Skapad av
+
+ testuser + (#1) +
+
+ +
+

Uppdatera status

+
+
+ + +
+ + +
+
+
+ +
+

Kommentarer

+ +
+ + +
+ + +
+ + +
+ +

Det finns inga kommentarer för denna ticket ännu.

+ +
    +
  • +

    Kommentarens innehåll

    + + testuser + - + 2026-04-22T12:00 + +
  • +
+
+ + Tillbaka till ticket-listan +
+ + diff --git a/src/main/resources/templates/tickets/list.html b/src/main/resources/templates/tickets/list.html new file mode 100644 index 0000000..f668043 --- /dev/null +++ b/src/main/resources/templates/tickets/list.html @@ -0,0 +1,52 @@ + + + + + + Ticket-lista + + + +
+ +
+
+

Tickets

+

Alla ärenden i ett tydligt arbetsflöde.

+

Följ status, prioritet och skapade ärenden direkt från den server-renderade demon.

+ +
+ +

Det finns inga tickets att visa ännu.

+ +
+ + + + + + + + + + + + + + + + + + + + + +
IDTitelStatusPrioritetSkapadDetaljer
1ExempelticketOPENHIGH2026-04-22T12:00 + Visa +
+
+
+ + diff --git a/src/main/resources/templates/tickets/new.html b/src/main/resources/templates/tickets/new.html new file mode 100644 index 0000000..5904cc7 --- /dev/null +++ b/src/main/resources/templates/tickets/new.html @@ -0,0 +1,48 @@ + + + + + + Skapa ticket + + + +
+ +
+
+

Ny ticket

+

Skapa ett ärende med tydlig prioritet.

+

Ange titel, beskrivning och prioritet. Efter skapning skickas du direkt till detaljsidan.

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + Tillbaka till ticket-listan +
+ + From 1917395ba320f5e624824c94bbaff83dd9543bdc Mon Sep 17 00:00:00 2001 From: Adam Majava Date: Thu, 23 Apr 2026 10:59:33 +0200 Subject: [PATCH 2/3] feat: optimize user insertion in DataInitializer with batch insert syntax --- .../config/DataInitializer.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/group1/projectbackend/config/DataInitializer.java b/src/main/java/org/group1/projectbackend/config/DataInitializer.java index dcc1b2d..e27a33e 100644 --- a/src/main/java/org/group1/projectbackend/config/DataInitializer.java +++ b/src/main/java/org/group1/projectbackend/config/DataInitializer.java @@ -24,15 +24,14 @@ public void run(String... args) { return; } - jdbcTemplate.update( - """ - INSERT INTO users (id, username, email, password, full_name, enabled, created_at) - VALUES (1, 'user', 'user@example.com', 'password', 'user', true, CURRENT_TIMESTAMP) - VALUES (2, 'adamaj01', 'adamaj@example.com', 'password', 'Adam', true, CURRENT_TIMESTAMP) - VALUES (3, 'emmtra01', 'emmtra@example.com', 'password', 'Emma', true, CURRENT_TIMESTAMP) - VALUES (4, 'erifal01', 'erifal@example.com', 'password', 'Erika', true, CURRENT_TIMESTAMP) - VALUES (5, 'johjan01', 'johjan@example.com', 'password', 'Johan', true, CURRENT_TIMESTAMP) - """ - ); + jdbcTemplate.update(""" + INSERT INTO users (id, username, email, password, full_name, enabled, created_at) + VALUES + (1, 'user', 'user@example.com', 'password', 'user', true, CURRENT_TIMESTAMP), + (2, 'adamaj01', 'adamaj@example.com', 'password', 'Adam', true, CURRENT_TIMESTAMP), + (3, 'emmtra01', 'emmtra@example.com', 'password', 'Emma', true, CURRENT_TIMESTAMP), + (4, 'erifa101', 'erifa1@example.com', 'password', 'Erika', true, CURRENT_TIMESTAMP), + (5, 'johjan01', 'johjan@example.com', 'password', 'Johan', true, CURRENT_TIMESTAMP) +"""); } } From 0bd83dc5aa41001cd8e958f8b21fa24756f97dd3 Mon Sep 17 00:00:00 2001 From: Adam Majava Date: Thu, 23 Apr 2026 11:11:11 +0200 Subject: [PATCH 3/3] feat: enhance ticket creation and comment submission with validation and user association --- .../controller/web/TicketViewController.java | 34 +++++++++++++++---- src/main/resources/static/css/app.css | 12 +++---- .../resources/templates/tickets/detail.html | 2 -- src/main/resources/templates/tickets/new.html | 11 +++--- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/group1/projectbackend/controller/web/TicketViewController.java b/src/main/java/org/group1/projectbackend/controller/web/TicketViewController.java index 0111eb6..c574f43 100644 --- a/src/main/java/org/group1/projectbackend/controller/web/TicketViewController.java +++ b/src/main/java/org/group1/projectbackend/controller/web/TicketViewController.java @@ -1,17 +1,23 @@ package org.group1.projectbackend.controller.web; +import jakarta.validation.Valid; import java.security.Principal; import org.group1.projectbackend.dto.comment.CreateCommentDto; import org.group1.projectbackend.dto.ticket.CreateTicketRequest; import org.group1.projectbackend.dto.ticket.TicketResponse; import org.group1.projectbackend.dto.ticket.UpdateTicketStatusRequest; +import org.group1.projectbackend.entity.User; import org.group1.projectbackend.entity.enums.TicketPriority; import org.group1.projectbackend.entity.enums.TicketStatus; +import org.group1.projectbackend.exception.ResourceNotFoundException; +import org.group1.projectbackend.repository.UserRepository; import org.group1.projectbackend.service.CommentService; import org.group1.projectbackend.service.SupportTicketService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -21,10 +27,16 @@ public class TicketViewController { private final SupportTicketService supportTicketService; private final CommentService commentService; + private final UserRepository userRepository; - public TicketViewController(SupportTicketService supportTicketService, CommentService commentService) { + public TicketViewController( + SupportTicketService supportTicketService, + CommentService commentService, + UserRepository userRepository + ) { this.supportTicketService = supportTicketService; this.commentService = commentService; + this.userRepository = userRepository; } @GetMapping("/tickets") @@ -43,6 +55,7 @@ public String showTicket(@PathVariable Long id, Model model) { @GetMapping("/tickets/new") public String showCreateTicketForm(Model model) { + model.addAttribute("createTicketRequest", new CreateTicketRequest("", "", null)); model.addAttribute("priorities", TicketPriority.values()); return "tickets/new"; } @@ -50,11 +63,15 @@ public String showCreateTicketForm(Model model) { @PostMapping("/tickets") public String createTicket( Principal principal, - @RequestParam String title, - @RequestParam String description, - @RequestParam TicketPriority priority + @Valid @ModelAttribute CreateTicketRequest request, + BindingResult bindingResult, + Model model ) { - CreateTicketRequest request = new CreateTicketRequest(title, description, priority); + if (bindingResult.hasErrors()) { + model.addAttribute("priorities", TicketPriority.values()); + return "tickets/new"; + } + TicketResponse ticket = supportTicketService.createTicket(principal.getName(), request); return "redirect:/tickets/" + ticket.id(); @@ -64,12 +81,15 @@ public String createTicket( public String createComment( @PathVariable Long id, @RequestParam String content, - @RequestParam Long userId + Principal principal ) { + User user = userRepository.findByUsername(principal.getName()) + .orElseThrow(() -> new ResourceNotFoundException("User not found with username: " + principal.getName())); + CreateCommentDto comment = new CreateCommentDto(); comment.setContent(content); comment.setSupportTicketId(id); - comment.setUserId(userId); + comment.setUserId(user.getId()); commentService.createComment(comment); diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index 6d01b68..99f8117 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -38,7 +38,7 @@ body { radial-gradient(ellipse at top, #0a0a0f 0%, var(--background-base) 52%, var(--background-deep) 100%), linear-gradient(180deg, rgba(94, 106, 210, 0.06), transparent 38rem); color: var(--foreground); - font-family: Inter, "Geist Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-family: Inter, Geist Sans, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; line-height: 1.6; } @@ -122,7 +122,7 @@ main { border-radius: 999px; background: rgba(94, 106, 210, 0.12); color: #cdd2ff; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-family: SFMono-Regular, Consolas, "Liberation Mono", monospace; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.16em; @@ -211,7 +211,7 @@ main > * { .eyebrow { margin: 0; color: #cdd2ff; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-family: SFMono-Regular, Consolas, "Liberation Mono", monospace; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.18em; @@ -345,7 +345,7 @@ td { th { background: rgba(255, 255, 255, 0.045); color: var(--foreground-muted); - font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-family: SFMono-Regular, Consolas, "Liberation Mono", monospace; font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; @@ -390,7 +390,7 @@ dl { dt { color: var(--foreground-muted); - font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-family: SFMono-Regular, Consolas, "Liberation Mono", monospace; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em; @@ -462,7 +462,7 @@ textarea { border-radius: 999px; background: rgba(94, 106, 210, 0.12); color: #dce0ff; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-family: SFMono-Regular, Consolas, "Liberation Mono", monospace; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; diff --git a/src/main/resources/templates/tickets/detail.html b/src/main/resources/templates/tickets/detail.html index 7d4c88d..cb878ab 100644 --- a/src/main/resources/templates/tickets/detail.html +++ b/src/main/resources/templates/tickets/detail.html @@ -68,8 +68,6 @@

Uppdatera status

Kommentarer

- -
diff --git a/src/main/resources/templates/tickets/new.html b/src/main/resources/templates/tickets/new.html index 5904cc7..0a92b4e 100644 --- a/src/main/resources/templates/tickets/new.html +++ b/src/main/resources/templates/tickets/new.html @@ -16,20 +16,22 @@

Skapa ett ärende med tydlig prioritet.

Ange titel, beskrivning och prioritet. Efter skapning skickas du direkt till detaljsidan.

- +
- + + Titel är obligatorisk
- + + Beskrivning är obligatorisk
- + Prioritet är obligatorisk