Compare commits
No commits in common. "8cd7991603f604267c3fc1bf514cf5ebf62a4e13" and "96508d63cd556815b4638df845497dc1e4bff108" have entirely different histories.
8cd7991603
...
96508d63cd
37 changed files with 70 additions and 2192 deletions
24
AGENTS.md
24
AGENTS.md
|
|
@ -37,8 +37,7 @@ docker compose up -d # starts postgres, backend, frontend
|
||||||
### All-in-one
|
### All-in-one
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew check # frontend lint → frontend test → backend test → coverage verification
|
./gradlew check # frontend lint → frontend test → backend test → integration test
|
||||||
./gradlew coverage # backend + frontend tests with coverage reports
|
|
||||||
./gradlew up # docker compose up -d
|
./gradlew up # docker compose up -d
|
||||||
./gradlew down # docker compose down
|
./gradlew down # docker compose down
|
||||||
./gradlew reset # docker compose down -v && docker compose up -d (full DB reset)
|
./gradlew reset # docker compose down -v && docker compose up -d (full DB reset)
|
||||||
|
|
@ -53,7 +52,6 @@ npm run dev # dev server on :3000 with HMR
|
||||||
npm run build # production build
|
npm run build # production build
|
||||||
npm run lint # ESLint
|
npm run lint # ESLint
|
||||||
npm run test # vitest
|
npm run test # vitest
|
||||||
npm run test:coverage # vitest with coverage (HTML at frontend/coverage/)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend (Spring Boot 4 + Java 21)
|
### Backend (Spring Boot 4 + Java 21)
|
||||||
|
|
@ -230,26 +228,6 @@ the same PR — never merge code without corresponding tests.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Coverage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew coverage # backend + frontend tests with coverage
|
|
||||||
```
|
|
||||||
|
|
||||||
Coverage thresholds are enforced during `./gradlew check`. PRs must maintain
|
|
||||||
or improve coverage.
|
|
||||||
|
|
||||||
| Layer | Lines | Branches | Functions |
|
|
||||||
|----------|-------|----------|-----------|
|
|
||||||
| Backend | 70% | 60% | — |
|
|
||||||
| Frontend | 70% | 60% | 70% |
|
|
||||||
|
|
||||||
HTML reports:
|
|
||||||
- Backend: `backend/build/reports/jacoco/index.html`
|
|
||||||
- Frontend: `frontend/coverage/index.html`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## External References
|
## External References
|
||||||
|
|
||||||
For detailed conventions, load `@CODING_GUIDELINES.md`.
|
For detailed conventions, load `@CODING_GUIDELINES.md`.
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,6 @@ Conventions and standards for the BilHej codebase. These exist to keep the proje
|
||||||
- **No commented-out code.** Delete it. Git history keeps it if needed.
|
- **No commented-out code.** Delete it. Git history keeps it if needed.
|
||||||
- **Keep functions small.** A function should do one thing. If it's over 30 lines, it probably does too much.
|
- **Keep functions small.** A function should do one thing. If it's over 30 lines, it probably does too much.
|
||||||
- **No magic numbers.** Use named constants or enums.
|
- **No magic numbers.** Use named constants or enums.
|
||||||
- **Treat warnings as mistakes.** LSP diagnostics, compiler warnings, and lint
|
|
||||||
warnings are bugs. Never commit code that produces them. If a warning is a
|
|
||||||
known false positive (e.g. Lombok `@RequiredArgsConstructor` triggering
|
|
||||||
"uninitialized final field"), suppress it explicitly at the narrowest scope
|
|
||||||
with a comment explaining why:
|
|
||||||
- Java: `@SuppressWarnings("...") // Lombok generates constructor`
|
|
||||||
- TypeScript: `// @ts-expect-error — pinia getActivePinia returns null in test context`
|
|
||||||
Uncommented suppressions are indistinguishable from ignoring a real problem
|
|
||||||
and are treated as errors.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -312,24 +303,6 @@ the same PR — never merge code without corresponding tests.
|
||||||
raw SQL in test code. Tests interact with the database the same way
|
raw SQL in test code. Tests interact with the database the same way
|
||||||
production code does: through the ORM.
|
production code does: through the ORM.
|
||||||
|
|
||||||
### Coverage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew coverage # backend + frontend tests with coverage
|
|
||||||
```
|
|
||||||
|
|
||||||
Coverage is enforced via `./gradlew check`. Thresholds:
|
|
||||||
|
|
||||||
| Layer | Lines | Branches | Functions |
|
|
||||||
|----------|-------|----------|-----------|
|
|
||||||
| Backend | 70% | 60% | — |
|
|
||||||
| Frontend | 70% | 60% | 70% |
|
|
||||||
|
|
||||||
- Backend: JaCoCo (`backend/build/reports/jacoco/index.html`).
|
|
||||||
- Frontend: Vitest v8 provider (`frontend/coverage/index.html`).
|
|
||||||
- PRs must maintain or improve coverage levels. If a new feature changes
|
|
||||||
coverage, update the test suite — never lower thresholds without discussion.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Linting & Formatting
|
## 8. Linting & Formatting
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'jacoco'
|
|
||||||
id 'org.springframework.boot' version '4.0.6'
|
id 'org.springframework.boot' version '4.0.6'
|
||||||
id 'io.spring.dependency-management' version '1.1.7'
|
id 'io.spring.dependency-management' version '1.1.7'
|
||||||
}
|
}
|
||||||
|
|
@ -45,39 +44,4 @@ dependencies {
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
finalizedBy jacocoTestReport
|
|
||||||
}
|
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion = "0.8.12"
|
|
||||||
}
|
|
||||||
|
|
||||||
jacocoTestReport {
|
|
||||||
dependsOn test
|
|
||||||
reports {
|
|
||||||
xml.required = true
|
|
||||||
csv.required = false
|
|
||||||
html.required = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jacocoTestCoverageVerification {
|
|
||||||
dependsOn jacocoTestReport
|
|
||||||
violationRules {
|
|
||||||
rule {
|
|
||||||
limit {
|
|
||||||
minimum = 0.70
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rule {
|
|
||||||
limit {
|
|
||||||
counter = 'BRANCH'
|
|
||||||
minimum = 0.60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('check').configure {
|
|
||||||
dependsOn jacocoTestCoverageVerification
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ public class SecurityConfig {
|
||||||
.requestMatchers("/api/auth/register", "/api/auth/login").permitAll()
|
.requestMatchers("/api/auth/register", "/api/auth/login").permitAll()
|
||||||
.requestMatchers("/api/webhooks/**").permitAll()
|
.requestMatchers("/api/webhooks/**").permitAll()
|
||||||
.requestMatchers("/api/vehicles/**").permitAll()
|
.requestMatchers("/api/vehicles/**").permitAll()
|
||||||
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
|
||||||
.anyRequest().authenticated())
|
.anyRequest().authenticated())
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
package se.bilhalsning.controller;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PatchMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import se.bilhalsning.dto.AdminOrderResponse;
|
|
||||||
import se.bilhalsning.dto.UpdateStatusRequest;
|
|
||||||
import se.bilhalsning.dto.UpdateTrackingRequest;
|
|
||||||
import se.bilhalsning.entity.Order;
|
|
||||||
import se.bilhalsning.service.OrderService;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/admin")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AdminController {
|
|
||||||
|
|
||||||
private final OrderService orderService;
|
|
||||||
|
|
||||||
@GetMapping("/orders")
|
|
||||||
public ResponseEntity<List<AdminOrderResponse>> listAllOrders() {
|
|
||||||
List<AdminOrderResponse> orders = orderService.getAllOrders().stream()
|
|
||||||
.map(this::toAdminResponse)
|
|
||||||
.toList();
|
|
||||||
return ResponseEntity.ok(orders);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PatchMapping("/orders/{id}/status")
|
|
||||||
public ResponseEntity<AdminOrderResponse> updateStatus(
|
|
||||||
@PathVariable UUID id,
|
|
||||||
@Valid @RequestBody UpdateStatusRequest request) {
|
|
||||||
Order order = orderService.updateOrderStatus(id, request.status());
|
|
||||||
return ResponseEntity.ok(toAdminResponse(order));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PatchMapping("/orders/{id}")
|
|
||||||
public ResponseEntity<AdminOrderResponse> updateTracking(
|
|
||||||
@PathVariable UUID id,
|
|
||||||
@Valid @RequestBody UpdateTrackingRequest request) {
|
|
||||||
Order order = orderService.updateTracking(id, request.trackingId());
|
|
||||||
return ResponseEntity.ok(toAdminResponse(order));
|
|
||||||
}
|
|
||||||
|
|
||||||
private AdminOrderResponse toAdminResponse(Order order) {
|
|
||||||
String email = order.getUser() != null ? order.getUser().getEmail() : "";
|
|
||||||
return new AdminOrderResponse(
|
|
||||||
order.getId(),
|
|
||||||
email,
|
|
||||||
order.getPlate(),
|
|
||||||
order.getLetterText(),
|
|
||||||
order.getStatus().getValue(),
|
|
||||||
order.getTrackingId(),
|
|
||||||
order.getAmountPaid(),
|
|
||||||
order.getCreatedAt()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -63,7 +63,6 @@ public class OrderController {
|
||||||
order.getPlate(),
|
order.getPlate(),
|
||||||
order.getStatus().getValue(),
|
order.getStatus().getValue(),
|
||||||
order.getTrackingId(),
|
order.getTrackingId(),
|
||||||
order.getAmountPaid(),
|
|
||||||
order.getCreatedAt()
|
order.getCreatedAt()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
package se.bilhalsning.controller;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import se.bilhalsning.dto.OrderResponse;
|
|
||||||
import se.bilhalsning.entity.Order;
|
|
||||||
import se.bilhalsning.service.OrderService;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/payment")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class PaymentController {
|
|
||||||
|
|
||||||
private final OrderService orderService;
|
|
||||||
|
|
||||||
@PostMapping("/{orderId}/pay")
|
|
||||||
public ResponseEntity<OrderResponse> pay(@PathVariable UUID orderId) {
|
|
||||||
Order order = orderService.markAsPaid(orderId);
|
|
||||||
return ResponseEntity.ok(toResponse(order));
|
|
||||||
}
|
|
||||||
|
|
||||||
private OrderResponse toResponse(Order order) {
|
|
||||||
return new OrderResponse(
|
|
||||||
order.getId(),
|
|
||||||
order.getPlate(),
|
|
||||||
order.getStatus().getValue(),
|
|
||||||
order.getTrackingId(),
|
|
||||||
order.getAmountPaid(),
|
|
||||||
order.getCreatedAt()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package se.bilhalsning.dto;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public record AdminOrderResponse(
|
|
||||||
UUID id,
|
|
||||||
String email,
|
|
||||||
String plate,
|
|
||||||
String letterText,
|
|
||||||
String status,
|
|
||||||
String trackingId,
|
|
||||||
BigDecimal amountPaid,
|
|
||||||
Instant createdAt
|
|
||||||
) {}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package se.bilhalsning.dto;
|
package se.bilhalsning.dto;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
@ -9,6 +8,5 @@ public record OrderResponse(
|
||||||
String plate,
|
String plate,
|
||||||
String status,
|
String status,
|
||||||
String trackingId,
|
String trackingId,
|
||||||
BigDecimal amountPaid,
|
|
||||||
Instant createdAt
|
Instant createdAt
|
||||||
) {}
|
) {}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package se.bilhalsning.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.Pattern;
|
|
||||||
|
|
||||||
public record UpdateStatusRequest(
|
|
||||||
@NotBlank(message = "Status krävs")
|
|
||||||
@Pattern(
|
|
||||||
regexp = "pending_payment|paid|lookup_started|sent|delivered|failed",
|
|
||||||
message = "Ogiltig status"
|
|
||||||
)
|
|
||||||
String status
|
|
||||||
) {}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package se.bilhalsning.dto;
|
|
||||||
|
|
||||||
public record UpdateTrackingRequest(
|
|
||||||
String trackingId
|
|
||||||
) {}
|
|
||||||
|
|
@ -2,10 +2,7 @@ package se.bilhalsning.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.FetchType;
|
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.ManyToOne;
|
|
||||||
import jakarta.persistence.PrePersist;
|
import jakarta.persistence.PrePersist;
|
||||||
import jakarta.persistence.PreUpdate;
|
import jakarta.persistence.PreUpdate;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
|
@ -24,10 +21,6 @@ public class Order {
|
||||||
@Column(name = "user_id", nullable = false, columnDefinition = "uuid")
|
@Column(name = "user_id", nullable = false, columnDefinition = "uuid")
|
||||||
private UUID userId;
|
private UUID userId;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
|
||||||
@JoinColumn(name = "user_id", insertable = false, updatable = false)
|
|
||||||
private User user;
|
|
||||||
|
|
||||||
@Column(name = "plate", nullable = false, length = 10)
|
@Column(name = "plate", nullable = false, length = 10)
|
||||||
private String plate;
|
private String plate;
|
||||||
|
|
||||||
|
|
@ -82,14 +75,6 @@ public class Order {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User getUser() {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUser(User user) {
|
|
||||||
this.user = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPlate() {
|
public String getPlate() {
|
||||||
return plate;
|
return plate;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package se.bilhalsning.repository;
|
package se.bilhalsning.repository;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.EntityGraph;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import se.bilhalsning.entity.Order;
|
import se.bilhalsning.entity.Order;
|
||||||
|
|
@ -12,9 +11,5 @@ import java.util.UUID;
|
||||||
@Repository
|
@Repository
|
||||||
public interface OrderRepository extends JpaRepository<Order, UUID> {
|
public interface OrderRepository extends JpaRepository<Order, UUID> {
|
||||||
List<Order> findByUserIdOrderByCreatedAtDesc(UUID userId);
|
List<Order> findByUserIdOrderByCreatedAtDesc(UUID userId);
|
||||||
|
|
||||||
List<Order> findByStatus(OrderStatus status);
|
List<Order> findByStatus(OrderStatus status);
|
||||||
|
|
||||||
@EntityGraph(attributePaths = {"user"})
|
|
||||||
List<Order> findAllByOrderByCreatedAtDesc();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,16 +45,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
var userDetails = userDetailsService.loadUserByUsername(username);
|
var userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
|
||||||
if (jwtService.isTokenValid(token)) {
|
if (jwtService.isTokenValid(token)) {
|
||||||
String role = jwtService.extractRole(token);
|
|
||||||
List<org.springframework.security.core.authority.SimpleGrantedAuthority> authorities =
|
|
||||||
new java.util.ArrayList<>();
|
|
||||||
if (role != null) {
|
|
||||||
authorities.add(new org.springframework.security.core.authority.SimpleGrantedAuthority(
|
|
||||||
"ROLE_" + role.toUpperCase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
var authToken = new UsernamePasswordAuthenticationToken(
|
var authToken = new UsernamePasswordAuthenticationToken(
|
||||||
userDetails, null, authorities);
|
userDetails, null, userDetails.getAuthorities());
|
||||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import se.bilhalsning.entity.OrderStatus;
|
||||||
import se.bilhalsning.exception.OrderNotFoundException;
|
import se.bilhalsning.exception.OrderNotFoundException;
|
||||||
import se.bilhalsning.repository.OrderRepository;
|
import se.bilhalsning.repository.OrderRepository;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
@ -34,34 +33,4 @@ public class OrderService {
|
||||||
return orderRepository.findById(id)
|
return orderRepository.findById(id)
|
||||||
.orElseThrow(() -> new OrderNotFoundException(id));
|
.orElseThrow(() -> new OrderNotFoundException(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Order> getAllOrders() {
|
|
||||||
return orderRepository.findAllByOrderByCreatedAtDesc();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Order updateOrderStatus(UUID orderId, String statusString) {
|
|
||||||
Order order = orderRepository.findById(orderId)
|
|
||||||
.orElseThrow(() -> new OrderNotFoundException(orderId));
|
|
||||||
|
|
||||||
OrderStatus newStatus = OrderStatus.valueOf(statusString.toUpperCase());
|
|
||||||
order.setStatus(newStatus);
|
|
||||||
return orderRepository.save(order);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Order updateTracking(UUID orderId, String trackingId) {
|
|
||||||
Order order = orderRepository.findById(orderId)
|
|
||||||
.orElseThrow(() -> new OrderNotFoundException(orderId));
|
|
||||||
|
|
||||||
order.setTrackingId(trackingId);
|
|
||||||
return orderRepository.save(order);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Order markAsPaid(UUID orderId) {
|
|
||||||
Order order = orderRepository.findById(orderId)
|
|
||||||
.orElseThrow(() -> new OrderNotFoundException(orderId));
|
|
||||||
|
|
||||||
order.setStatus(OrderStatus.PAID);
|
|
||||||
order.setAmountPaid(new BigDecimal("49.00"));
|
|
||||||
return orderRepository.save(order);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
package se.bilhalsning.controller;
|
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
import se.bilhalsning.entity.Order;
|
|
||||||
import se.bilhalsning.entity.OrderStatus;
|
|
||||||
import se.bilhalsning.entity.User;
|
|
||||||
import se.bilhalsning.exception.OrderNotFoundException;
|
|
||||||
import se.bilhalsning.service.OrderService;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
@AutoConfigureMockMvc
|
|
||||||
class AdminControllerTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private OrderService orderService;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturn403WhenNotAuthenticated() throws Exception {
|
|
||||||
mockMvc.perform(get("/api/admin/orders"))
|
|
||||||
.andExpect(status().isForbidden());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "test@bilhalsning.se", roles = "USER")
|
|
||||||
void shouldReturn403ForNonAdminUser() throws Exception {
|
|
||||||
mockMvc.perform(get("/api/admin/orders"))
|
|
||||||
.andExpect(status().isForbidden());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
|
||||||
void shouldReturnAllOrdersForAdmin() throws Exception {
|
|
||||||
Order order = createOrder(UUID.randomUUID(), "ABC123", "test@bilhalsning.se", OrderStatus.SENT);
|
|
||||||
when(orderService.getAllOrders()).thenReturn(List.of(order));
|
|
||||||
|
|
||||||
mockMvc.perform(get("/api/admin/orders"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$").isArray())
|
|
||||||
.andExpect(jsonPath("$[0].id").value(order.getId().toString()))
|
|
||||||
.andExpect(jsonPath("$[0].email").value("test@bilhalsning.se"))
|
|
||||||
.andExpect(jsonPath("$[0].plate").value("ABC123"))
|
|
||||||
.andExpect(jsonPath("$[0].letterText").value("Test letter"))
|
|
||||||
.andExpect(jsonPath("$[0].status").value("sent"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
|
||||||
void shouldReturnEmptyArrayWhenNoOrders() throws Exception {
|
|
||||||
when(orderService.getAllOrders()).thenReturn(List.of());
|
|
||||||
|
|
||||||
mockMvc.perform(get("/api/admin/orders"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$").isArray())
|
|
||||||
.andExpect(jsonPath("$").isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturn403WhenPatchingStatusWithoutAuth() throws Exception {
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}/status",
|
|
||||||
"c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"status\":\"paid\"}"))
|
|
||||||
.andExpect(status().isForbidden());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "test@bilhalsning.se", roles = "USER")
|
|
||||||
void shouldReturn403WhenPatchingStatusAsNonAdmin() throws Exception {
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}/status",
|
|
||||||
"c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"status\":\"paid\"}"))
|
|
||||||
.andExpect(status().isForbidden());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
|
||||||
void shouldUpdateOrderStatusSuccessfully() throws Exception {
|
|
||||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
|
||||||
Order order = createOrder(orderId, "ABC123", "test@bilhalsning.se", OrderStatus.PAID);
|
|
||||||
|
|
||||||
when(orderService.updateOrderStatus(eq(orderId), eq("paid"))).thenReturn(order);
|
|
||||||
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}/status", orderId)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"status\":\"paid\"}"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.id").value(orderId.toString()))
|
|
||||||
.andExpect(jsonPath("$.status").value("paid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
|
||||||
void shouldReturn400WhenStatusIsInvalid() throws Exception {
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}/status",
|
|
||||||
"c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"status\":\"invalid_status\"}"))
|
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
|
||||||
void shouldReturn400WhenStatusIsBlank() throws Exception {
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}/status",
|
|
||||||
"c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"status\":\"\"}"))
|
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
|
||||||
void shouldReturn404WhenOrderNotFound() throws Exception {
|
|
||||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
|
||||||
when(orderService.updateOrderStatus(eq(orderId), eq("paid")))
|
|
||||||
.thenThrow(new OrderNotFoundException(orderId));
|
|
||||||
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}/status", orderId)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"status\":\"paid\"}"))
|
|
||||||
.andExpect(status().isNotFound());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturn403WhenPatchingTrackingWithoutAuth() throws Exception {
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}",
|
|
||||||
"c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"trackingId\":\"PN123456789\"}"))
|
|
||||||
.andExpect(status().isForbidden());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "test@bilhalsning.se", roles = "USER")
|
|
||||||
void shouldReturn403WhenPatchingTrackingAsNonAdmin() throws Exception {
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}",
|
|
||||||
"c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"trackingId\":\"PN123456789\"}"))
|
|
||||||
.andExpect(status().isForbidden());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
|
||||||
void shouldUpdateTrackingSuccessfully() throws Exception {
|
|
||||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
|
||||||
Order order = createOrder(orderId, "ABC123", "test@bilhalsning.se", OrderStatus.SENT);
|
|
||||||
order.setTrackingId("PN123456789");
|
|
||||||
|
|
||||||
when(orderService.updateTracking(eq(orderId), eq("PN123456789"))).thenReturn(order);
|
|
||||||
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}", orderId)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"trackingId\":\"PN123456789\"}"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.id").value(orderId.toString()))
|
|
||||||
.andExpect(jsonPath("$.trackingId").value("PN123456789"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
|
||||||
void shouldClearTrackingWhenNull() throws Exception {
|
|
||||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
|
||||||
Order order = createOrder(orderId, "ABC123", "test@bilhalsning.se", OrderStatus.SENT);
|
|
||||||
order.setTrackingId(null);
|
|
||||||
|
|
||||||
when(orderService.updateTracking(eq(orderId), eq(null))).thenReturn(order);
|
|
||||||
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}", orderId)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"trackingId\":null}"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.trackingId").doesNotExist());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
|
||||||
void shouldReturn404WhenOrderNotFoundForTracking() throws Exception {
|
|
||||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
|
||||||
when(orderService.updateTracking(eq(orderId), eq("PN123456789")))
|
|
||||||
.thenThrow(new OrderNotFoundException(orderId));
|
|
||||||
|
|
||||||
mockMvc.perform(patch("/api/admin/orders/{id}", orderId)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content("{\"trackingId\":\"PN123456789\"}"))
|
|
||||||
.andExpect(status().isNotFound());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Order createOrder(UUID orderId, String plate, String email, OrderStatus status) {
|
|
||||||
User user = new User();
|
|
||||||
user.setEmail(email);
|
|
||||||
|
|
||||||
Order order = new Order();
|
|
||||||
order.setId(orderId);
|
|
||||||
order.setUser(user);
|
|
||||||
order.setPlate(plate);
|
|
||||||
order.setLetterText("Test letter");
|
|
||||||
order.setStatus(status);
|
|
||||||
order.setTrackingId(null);
|
|
||||||
order.setAmountPaid(new BigDecimal("49.00"));
|
|
||||||
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
package se.bilhalsning.controller;
|
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
import se.bilhalsning.entity.Order;
|
|
||||||
import se.bilhalsning.entity.OrderStatus;
|
|
||||||
import se.bilhalsning.exception.OrderNotFoundException;
|
|
||||||
import se.bilhalsning.service.OrderService;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
@AutoConfigureMockMvc
|
|
||||||
class PaymentControllerTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private OrderService orderService;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturn403WhenNotAuthenticated() throws Exception {
|
|
||||||
mockMvc.perform(post("/api/payment/{orderId}/pay",
|
|
||||||
"c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"))
|
|
||||||
.andExpect(status().isForbidden());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "test@bilhalsning.se")
|
|
||||||
void shouldMarkOrderAsPaidSuccessfully() throws Exception {
|
|
||||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
|
||||||
Order order = new Order();
|
|
||||||
order.setId(orderId);
|
|
||||||
order.setPlate("ABC123");
|
|
||||||
order.setStatus(OrderStatus.PAID);
|
|
||||||
order.setAmountPaid(new BigDecimal("49.00"));
|
|
||||||
|
|
||||||
when(orderService.markAsPaid(eq(orderId))).thenReturn(order);
|
|
||||||
|
|
||||||
mockMvc.perform(post("/api/payment/{orderId}/pay", orderId)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.id").value(orderId.toString()))
|
|
||||||
.andExpect(jsonPath("$.status").value("paid"))
|
|
||||||
.andExpect(jsonPath("$.amountPaid").value(49.00));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "test@bilhalsning.se")
|
|
||||||
void shouldReturn404WhenOrderNotFound() throws Exception {
|
|
||||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
|
||||||
when(orderService.markAsPaid(eq(orderId)))
|
|
||||||
.thenThrow(new OrderNotFoundException(orderId));
|
|
||||||
|
|
||||||
mockMvc.perform(post("/api/payment/{orderId}/pay", orderId)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON))
|
|
||||||
.andExpect(status().isNotFound());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
package se.bilhalsning.entity;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
class OrderStatusConverterTest {
|
|
||||||
|
|
||||||
private final OrderStatusConverter converter = new OrderStatusConverter();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnValueWhenStatusIsNotNull() {
|
|
||||||
assertEquals("sent", converter.convertToDatabaseColumn(OrderStatus.SENT));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnNullWhenStatusIsNull() {
|
|
||||||
assertNull(converter.convertToDatabaseColumn(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnEnumWhenDbDataMatchesValue() {
|
|
||||||
assertEquals(OrderStatus.PAID, converter.convertToEntityAttribute("paid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnNullWhenDbDataIsNull() {
|
|
||||||
assertNull(converter.convertToEntityAttribute(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldThrowWhenDbDataDoesNotMatchAnyStatus() {
|
|
||||||
assertThrows(IllegalArgumentException.class,
|
|
||||||
() -> converter.convertToEntityAttribute("bogus"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRoundtripAllEnumValues() {
|
|
||||||
for (OrderStatus status : OrderStatus.values()) {
|
|
||||||
String db = converter.convertToDatabaseColumn(status);
|
|
||||||
assertEquals(status, converter.convertToEntityAttribute(db));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
package se.bilhalsning.entity;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
class SubscriptionConverterTest {
|
|
||||||
|
|
||||||
private final SubscriptionConverter converter = new SubscriptionConverter();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnValueWhenSubscriptionIsNotNull() {
|
|
||||||
assertEquals("basic", converter.convertToDatabaseColumn(Subscription.BASIC));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnNullWhenSubscriptionIsNull() {
|
|
||||||
assertNull(converter.convertToDatabaseColumn(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnEnumWhenDbDataMatchesValue() {
|
|
||||||
assertEquals(Subscription.PRO, converter.convertToEntityAttribute("pro"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnNullWhenDbDataIsNull() {
|
|
||||||
assertNull(converter.convertToEntityAttribute(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldThrowWhenDbDataDoesNotMatchAnySubscription() {
|
|
||||||
assertThrows(IllegalArgumentException.class,
|
|
||||||
() -> converter.convertToEntityAttribute("premium"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRoundtripAllEnumValues() {
|
|
||||||
for (Subscription subscription : Subscription.values()) {
|
|
||||||
String db = converter.convertToDatabaseColumn(subscription);
|
|
||||||
assertEquals(subscription, converter.convertToEntityAttribute(db));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12
build.gradle
12
build.gradle
|
|
@ -15,18 +15,6 @@ tasks.register('frontendTest', Exec) {
|
||||||
commandLine 'npm', 'run', 'test'
|
commandLine 'npm', 'run', 'test'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('frontendCoverage', Exec) {
|
|
||||||
description = 'Run Vitest with coverage in the frontend directory'
|
|
||||||
workingDir = file("${rootProject.projectDir}/frontend")
|
|
||||||
commandLine 'npm', 'run', 'test:coverage'
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('coverage') {
|
|
||||||
group = 'verification'
|
|
||||||
description = 'Run backend + frontend tests with coverage thresholds'
|
|
||||||
dependsOn(':backend:jacocoTestReport', 'frontendCoverage')
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('frontendE2E', Exec) {
|
tasks.register('frontendE2E', Exec) {
|
||||||
description = 'Run Playwright E2E tests in Docker (CI mode)'
|
description = 'Run Playwright E2E tests in Docker (CI mode)'
|
||||||
workingDir = file("${rootProject.projectDir}/frontend")
|
workingDir = file("${rootProject.projectDir}/frontend")
|
||||||
|
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test'
|
|
||||||
|
|
||||||
test.describe('Admin dashboard', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('/logga-in')
|
|
||||||
await page.getByLabel('E-postadress').fill('admin@bilhalsning.se')
|
|
||||||
await page.getByLabel('Lösenord').fill('test1234')
|
|
||||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
|
||||||
await page.waitForURL('/')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('admin can navigate to admin page', async ({ page }) => {
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole('heading', { name: 'Administration' }),
|
|
||||||
).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('non-admin user is redirected away from admin', async ({ page }) => {
|
|
||||||
await page.evaluate(() => localStorage.clear())
|
|
||||||
await page.goto('/logga-in')
|
|
||||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
|
||||||
await page.getByLabel('Lösenord').fill('test1234')
|
|
||||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
|
||||||
await page.waitForURL('/')
|
|
||||||
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
await expect(page).toHaveURL('/')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows orders table with columns', async ({ page }) => {
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
await expect(page.getByText('Datum')).toBeVisible()
|
|
||||||
await expect(page.getByText('E-post')).toBeVisible()
|
|
||||||
await expect(page.getByText('Regnr')).toBeVisible()
|
|
||||||
await expect(page.getByText('Status')).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows seeded order data', async ({ page }) => {
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
await expect(page.locator('.admin-dashboard__plate').first()).toBeVisible()
|
|
||||||
await expect(page.getByText('DEF456').first()).toBeVisible()
|
|
||||||
await expect(page.getByText('GHI789').first()).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('click row expands letter content', async ({ page }) => {
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
const rows = page.locator('.admin-dashboard__row')
|
|
||||||
await rows.first().click()
|
|
||||||
|
|
||||||
await expect(page.getByText('Brevtext')).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('click expanded row collapses it', async ({ page }) => {
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
const rows = page.locator('.admin-dashboard__row')
|
|
||||||
await rows.first().click()
|
|
||||||
await expect(page.getByText('Brevtext')).toBeVisible()
|
|
||||||
|
|
||||||
await rows.first().click()
|
|
||||||
await expect(page.getByText('Brevtext')).not.toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('status dropdown changes update order status', async ({ page }) => {
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
const selects = page.locator('.admin-dashboard__status-select')
|
|
||||||
await selects.first().selectOption('delivered')
|
|
||||||
|
|
||||||
const updatedSelect = selects.first()
|
|
||||||
await expect(updatedSelect).toHaveValue('delivered')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('admin cannot access admin page without auth', async ({ page }) => {
|
|
||||||
await page.evaluate(() => localStorage.clear())
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/logga-in\?redirect=\/admin/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('expanded row shows tracking input and save button', async ({ page }) => {
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
const rows = page.locator('.admin-dashboard__row')
|
|
||||||
await rows.first().click()
|
|
||||||
|
|
||||||
await expect(page.getByText('Spårnings-ID')).toBeVisible()
|
|
||||||
await expect(page.locator('.admin-dashboard__tracking-input')).toBeVisible()
|
|
||||||
await expect(page.getByRole('button', { name: 'Spara spårning' })).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows PostNord link when trackingId exists', async ({ page }) => {
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
const rows = page.locator('.admin-dashboard__row')
|
|
||||||
await rows.last().click()
|
|
||||||
|
|
||||||
const trackingLink = page.locator('.admin-dashboard__tracking-link')
|
|
||||||
await expect(trackingLink).toBeVisible()
|
|
||||||
await expect(trackingLink).toHaveAttribute('href', /postnord/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('hides PostNord link when trackingId is null', async ({ page }) => {
|
|
||||||
await page.goto('/admin')
|
|
||||||
|
|
||||||
const defRow = page.locator('.admin-dashboard__row', { hasText: 'DEF456' }).first()
|
|
||||||
await defRow.click()
|
|
||||||
|
|
||||||
const trackingLink = page.locator('.admin-dashboard__tracking-link')
|
|
||||||
await expect(trackingLink).not.toBeVisible()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -31,7 +31,7 @@ test.describe('Compose flow', () => {
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: 'Skriv ditt brev' }),
|
page.getByRole('heading', { name: 'Skriv ditt brev' }),
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await expect(page.getByText('ABC123').first()).toBeVisible()
|
await expect(page.getByText('ABC123')).toBeVisible()
|
||||||
await expect(page.getByLabel('Ditt meddelande')).toBeVisible()
|
await expect(page.getByLabel('Ditt meddelande')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ test.describe('Compose flow', () => {
|
||||||
await expect(button).toBeDisabled()
|
await expect(button).toBeDisabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can create order and navigate to payment page', async ({ page }) => {
|
test('can create order and navigate to orders page', async ({ page }) => {
|
||||||
await page.goto('/logga-in')
|
await page.goto('/logga-in')
|
||||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||||
await page.getByLabel('Lösenord').fill('test1234')
|
await page.getByLabel('Lösenord').fill('test1234')
|
||||||
|
|
@ -62,9 +62,10 @@ test.describe('Compose flow', () => {
|
||||||
await expect(button).toBeEnabled()
|
await expect(button).toBeEnabled()
|
||||||
await button.click()
|
await button.click()
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/betalning\//)
|
await expect(page).toHaveURL('/orders')
|
||||||
await expect(page.getByRole('heading', { name: 'Betalning' })).toBeVisible()
|
await expect(
|
||||||
await expect(page.getByText('49 kr')).toBeVisible()
|
page.getByRole('heading', { name: 'Mina beställningar' }),
|
||||||
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('preview shows letter content and GDPR footer', async ({ page }) => {
|
test('preview shows letter content and GDPR footer', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ test.describe('Order history', () => {
|
||||||
await page.goto('/orders')
|
await page.goto('/orders')
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Mina beställningar' })).toBeVisible()
|
await expect(page.getByRole('heading', { name: 'Mina beställningar' })).toBeVisible()
|
||||||
await expect(page.getByText('ABC123').first()).toBeVisible()
|
await expect(page.getByText('ABC123')).toBeVisible()
|
||||||
await expect(page.getByText('DEF456').first()).toBeVisible()
|
await expect(page.getByText('DEF456')).toBeVisible()
|
||||||
await expect(page.getByText('GHI789').first()).toBeVisible()
|
await expect(page.getByText('GHI789')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('shows correct status badges', async ({ page }) => {
|
test('shows correct status badges', async ({ page }) => {
|
||||||
|
|
@ -50,8 +50,8 @@ test.describe('Order history', () => {
|
||||||
await page.goto('/orders')
|
await page.goto('/orders')
|
||||||
|
|
||||||
await expect(page.getByText('Skickat')).toBeVisible()
|
await expect(page.getByText('Skickat')).toBeVisible()
|
||||||
await expect(page.getByText('Väntar på betalning').first()).toBeVisible()
|
await expect(page.getByText('Väntar på betalning')).toBeVisible()
|
||||||
await expect(page.getByText('Levererat').first()).toBeVisible()
|
await expect(page.getByText('Levererat')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('shows tracking links for orders with tracking ID', async ({ page }) => {
|
test('shows tracking links for orders with tracking ID', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test'
|
|
||||||
|
|
||||||
test.describe('Payment redirect', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('/logga-in')
|
|
||||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
|
||||||
await page.getByLabel('Lösenord').fill('test1234')
|
|
||||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
|
||||||
await page.waitForURL('/')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('can navigate to payment page from compose', async ({ page }) => {
|
|
||||||
await page.goto('/compose?plate=ABC123')
|
|
||||||
await page.getByLabel('Ditt meddelande').fill('Hej fin bil!')
|
|
||||||
await page.getByRole('button', { name: 'Skicka brev (49 kr)' }).click()
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/betalning\//)
|
|
||||||
await expect(page.getByRole('heading', { name: 'Betalning' })).toBeVisible()
|
|
||||||
await expect(page.getByText('49 kr')).toBeVisible()
|
|
||||||
await expect(page.getByText('ABC123')).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Betalt button marks order as paid and redirects to orders', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto('/compose?plate=DEF456')
|
|
||||||
await page.getByLabel('Ditt meddelande').fill('Vill köpa din bil.')
|
|
||||||
await page.getByRole('button', { name: 'Skicka brev (49 kr)' }).click()
|
|
||||||
|
|
||||||
await page.waitForURL(/\/betalning\//)
|
|
||||||
await page.getByRole('button', { name: 'Betalt' }).click()
|
|
||||||
|
|
||||||
await expect(page).toHaveURL('/orders')
|
|
||||||
await expect(page.getByText('DEF456').first()).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('payment page requires authentication', async ({ page }) => {
|
|
||||||
await page.evaluate(() => localStorage.clear())
|
|
||||||
await page.goto('/betalning/some-id')
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/logga-in/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows mock payment note', async ({ page }) => {
|
|
||||||
await page.goto('/compose?plate=GHI789')
|
|
||||||
await page.getByLabel('Ditt meddelande').fill('Hej!')
|
|
||||||
await page.getByRole('button', { name: 'Skicka brev (49 kr)' }).click()
|
|
||||||
|
|
||||||
await page.waitForURL(/\/betalning\//)
|
|
||||||
await expect(page.getByText(/mock-betalning/i)).toBeVisible()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
256
frontend/package-lock.json
generated
256
frontend/package-lock.json
generated
|
|
@ -17,7 +17,6 @@
|
||||||
"@rushstack/eslint-patch": "^1.16.1",
|
"@rushstack/eslint-patch": "^1.16.1",
|
||||||
"@types/node": "^24.12.2",
|
"@types/node": "^24.12.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"@vitest/coverage-v8": "^4.1.6",
|
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.7.0",
|
"@vue/eslint-config-typescript": "^14.7.0",
|
||||||
"@vue/test-utils": "^2.4.10",
|
"@vue/test-utils": "^2.4.10",
|
||||||
|
|
@ -146,16 +145,6 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@bcoe/v8-coverage": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bramus/specificity": {
|
"node_modules/@bramus/specificity": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||||
|
|
@ -1314,48 +1303,17 @@
|
||||||
"vue": "^3.2.25"
|
"vue": "^3.2.25"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/coverage-v8": {
|
|
||||||
"version": "4.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.6.tgz",
|
|
||||||
"integrity": "sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@bcoe/v8-coverage": "^1.0.2",
|
|
||||||
"@vitest/utils": "4.1.6",
|
|
||||||
"ast-v8-to-istanbul": "^1.0.0",
|
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
|
||||||
"istanbul-lib-report": "^3.0.1",
|
|
||||||
"istanbul-reports": "^3.2.0",
|
|
||||||
"magicast": "^0.5.2",
|
|
||||||
"obug": "^2.1.1",
|
|
||||||
"std-env": "^4.0.0-rc.1",
|
|
||||||
"tinyrainbow": "^3.1.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/vitest"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@vitest/browser": "4.1.6",
|
|
||||||
"vitest": "4.1.6"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@vitest/browser": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz",
|
||||||
"integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==",
|
"integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.1.0",
|
"@standard-schema/spec": "^1.1.0",
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/spy": "4.1.6",
|
"@vitest/spy": "4.1.5",
|
||||||
"@vitest/utils": "4.1.6",
|
"@vitest/utils": "4.1.5",
|
||||||
"chai": "^6.2.2",
|
"chai": "^6.2.2",
|
||||||
"tinyrainbow": "^3.1.0"
|
"tinyrainbow": "^3.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1364,13 +1322,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz",
|
||||||
"integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==",
|
"integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "4.1.6",
|
"@vitest/spy": "4.1.5",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.21"
|
"magic-string": "^0.30.21"
|
||||||
},
|
},
|
||||||
|
|
@ -1401,9 +1359,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz",
|
||||||
"integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==",
|
"integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1414,13 +1372,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz",
|
||||||
"integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==",
|
"integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "4.1.6",
|
"@vitest/utils": "4.1.5",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|
@ -1428,14 +1386,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz",
|
||||||
"integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==",
|
"integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "4.1.6",
|
"@vitest/pretty-format": "4.1.5",
|
||||||
"@vitest/utils": "4.1.6",
|
"@vitest/utils": "4.1.5",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
|
|
@ -1444,9 +1402,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz",
|
||||||
"integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==",
|
"integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|
@ -1454,13 +1412,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz",
|
||||||
"integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==",
|
"integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "4.1.6",
|
"@vitest/pretty-format": "4.1.5",
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
"tinyrainbow": "^3.1.0"
|
"tinyrainbow": "^3.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1862,28 +1820,6 @@
|
||||||
"url": "https://github.com/sponsors/sxzz"
|
"url": "https://github.com/sponsors/sxzz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ast-v8-to-istanbul": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.31",
|
|
||||||
"estree-walker": "^3.0.3",
|
|
||||||
"js-tokens": "^10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ast-v8-to-istanbul/node_modules/estree-walker": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/estree": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ast-walker-scope": {
|
"node_modules/ast-walker-scope": {
|
||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz",
|
||||||
|
|
@ -2786,16 +2722,6 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-flag": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
|
@ -2815,13 +2741,6 @@
|
||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/html-escaper": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|
@ -2918,45 +2837,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/istanbul-lib-coverage": {
|
|
||||||
"version": "3.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
|
||||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/istanbul-lib-report": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"istanbul-lib-coverage": "^3.0.0",
|
|
||||||
"make-dir": "^4.0.0",
|
|
||||||
"supports-color": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/istanbul-reports": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"html-escaper": "^2.0.0",
|
|
||||||
"istanbul-lib-report": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jackspeak": {
|
"node_modules/jackspeak": {
|
||||||
"version": "3.4.3",
|
"version": "3.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||||
|
|
@ -3015,13 +2895,6 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-tokens": {
|
|
||||||
"version": "10.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
|
||||||
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/jsdom": {
|
"node_modules/jsdom": {
|
||||||
"version": "29.1.1",
|
"version": "29.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
||||||
|
|
@ -3482,34 +3355,6 @@
|
||||||
"url": "https://github.com/sponsors/sxzz"
|
"url": "https://github.com/sponsors/sxzz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magicast": {
|
|
||||||
"version": "0.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
|
|
||||||
"integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.29.3",
|
|
||||||
"@babel/types": "^7.29.0",
|
|
||||||
"source-map-js": "^1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/make-dir": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"semver": "^7.5.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdn-data": {
|
"node_modules/mdn-data": {
|
||||||
"version": "2.27.1",
|
"version": "2.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||||
|
|
@ -4399,19 +4244,6 @@
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supports-color": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-flag": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/symbol-tree": {
|
"node_modules/symbol-tree": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
|
@ -4758,19 +4590,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz",
|
||||||
"integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==",
|
"integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "4.1.6",
|
"@vitest/expect": "4.1.5",
|
||||||
"@vitest/mocker": "4.1.6",
|
"@vitest/mocker": "4.1.5",
|
||||||
"@vitest/pretty-format": "4.1.6",
|
"@vitest/pretty-format": "4.1.5",
|
||||||
"@vitest/runner": "4.1.6",
|
"@vitest/runner": "4.1.5",
|
||||||
"@vitest/snapshot": "4.1.6",
|
"@vitest/snapshot": "4.1.5",
|
||||||
"@vitest/spy": "4.1.6",
|
"@vitest/spy": "4.1.5",
|
||||||
"@vitest/utils": "4.1.6",
|
"@vitest/utils": "4.1.5",
|
||||||
"es-module-lexer": "^2.0.0",
|
"es-module-lexer": "^2.0.0",
|
||||||
"expect-type": "^1.3.0",
|
"expect-type": "^1.3.0",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
|
|
@ -4798,12 +4630,12 @@
|
||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
"@vitest/browser-playwright": "4.1.6",
|
"@vitest/browser-playwright": "4.1.5",
|
||||||
"@vitest/browser-preview": "4.1.6",
|
"@vitest/browser-preview": "4.1.5",
|
||||||
"@vitest/browser-webdriverio": "4.1.6",
|
"@vitest/browser-webdriverio": "4.1.5",
|
||||||
"@vitest/coverage-istanbul": "4.1.6",
|
"@vitest/coverage-istanbul": "4.1.5",
|
||||||
"@vitest/coverage-v8": "4.1.6",
|
"@vitest/coverage-v8": "4.1.5",
|
||||||
"@vitest/ui": "4.1.6",
|
"@vitest/ui": "4.1.5",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*",
|
"jsdom": "*",
|
||||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"format": "prettier --write src/",
|
"format": "prettier --write src/",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:ci": "docker compose -f ../docker-compose.ci.yml up --build --abort-on-container-exit --exit-code-from playwright"
|
"test:e2e:ci": "docker compose -f ../docker-compose.ci.yml up --build --abort-on-container-exit --exit-code-from playwright"
|
||||||
},
|
},
|
||||||
|
|
@ -25,7 +24,6 @@
|
||||||
"@rushstack/eslint-patch": "^1.16.1",
|
"@rushstack/eslint-patch": "^1.16.1",
|
||||||
"@types/node": "^24.12.2",
|
"@types/node": "^24.12.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"@vitest/coverage-v8": "^4.1.6",
|
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.7.0",
|
"@vue/eslint-config-typescript": "^14.7.0",
|
||||||
"@vue/test-utils": "^2.4.10",
|
"@vue/test-utils": "^2.4.10",
|
||||||
|
|
|
||||||
|
|
@ -1,306 +0,0 @@
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
|
||||||
import { createPinia } from 'pinia'
|
|
||||||
import AdminPage from '@/pages/AdminPage.vue'
|
|
||||||
|
|
||||||
function mockFetchResponse(status: number, body: unknown) {
|
|
||||||
return Promise.resolve({
|
|
||||||
ok: status >= 200 && status < 300,
|
|
||||||
status,
|
|
||||||
json: () => Promise.resolve(body),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTestRouter() {
|
|
||||||
return createRouter({
|
|
||||||
history: createMemoryHistory(),
|
|
||||||
routes: [
|
|
||||||
{ path: '/admin', name: 'admin', component: AdminPage },
|
|
||||||
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function mountPage() {
|
|
||||||
const router = createTestRouter()
|
|
||||||
const pinia = createPinia()
|
|
||||||
router.push('/admin')
|
|
||||||
return {
|
|
||||||
router,
|
|
||||||
wrapper: mount(AdminPage, {
|
|
||||||
global: { plugins: [router, pinia] },
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockOrders = [
|
|
||||||
{
|
|
||||||
id: 'c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
|
|
||||||
email: 'test@bilhalsning.se',
|
|
||||||
plate: 'ABC123',
|
|
||||||
letterText: 'Hej fin bil!',
|
|
||||||
status: 'sent',
|
|
||||||
trackingId: 'PN123456789',
|
|
||||||
amountPaid: 49.0,
|
|
||||||
createdAt: '2026-05-11T12:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12',
|
|
||||||
email: 'user@example.com',
|
|
||||||
plate: 'XYZ789',
|
|
||||||
letterText: 'Vill köpa din bil.',
|
|
||||||
status: 'pending_payment',
|
|
||||||
trackingId: null,
|
|
||||||
amountPaid: null,
|
|
||||||
createdAt: '2026-05-14T13:00:00Z',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
describe('AdminDashboard', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
localStorage.clear()
|
|
||||||
globalThis.fetch = vi.fn()
|
|
||||||
vi.mocked(globalThis.fetch).mockResolvedValue(
|
|
||||||
mockFetchResponse(200, mockOrders),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders heading and subtitle', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(wrapper.text()).toContain('Administration')
|
|
||||||
expect(wrapper.text()).toContain(
|
|
||||||
'Hantera beställningar, mallar och användare',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows loading state initially', async () => {
|
|
||||||
globalThis.fetch = vi.fn().mockImplementation(() => new Promise(() => {}))
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
expect(wrapper.text()).toContain('Laddar beställningar...')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fetches orders from API on mount', async () => {
|
|
||||||
mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
||||||
'/api/admin/orders',
|
|
||||||
expect.objectContaining({ headers: expect.any(Object) }),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders table with all columns', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(wrapper.text()).toContain('Datum')
|
|
||||||
expect(wrapper.text()).toContain('E-post')
|
|
||||||
expect(wrapper.text()).toContain('Regnr')
|
|
||||||
expect(wrapper.text()).toContain('Status')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders order data in rows', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(wrapper.text()).toContain('test@bilhalsning.se')
|
|
||||||
expect(wrapper.text()).toContain('ABC123')
|
|
||||||
expect(wrapper.text()).toContain('user@example.com')
|
|
||||||
expect(wrapper.text()).toContain('XYZ789')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows empty state when no orders', async () => {
|
|
||||||
vi.mocked(globalThis.fetch).mockResolvedValue(mockFetchResponse(200, []))
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(wrapper.text()).toContain('Inga beställningar ännu')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows error state on API failure', async () => {
|
|
||||||
vi.mocked(globalThis.fetch).mockResolvedValue(
|
|
||||||
mockFetchResponse(500, { message: 'Internal server error' }),
|
|
||||||
)
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(wrapper.text()).toContain('Kunde inte hämta beställningar')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('expands row on click to show letter content', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const rows = wrapper.findAll('.admin-dashboard__row')
|
|
||||||
expect(rows.length).toBe(2)
|
|
||||||
|
|
||||||
await rows[0].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('Hej fin bil!')
|
|
||||||
expect(wrapper.text()).toContain('Brevtext')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('collapses row on second click', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const rows = wrapper.findAll('.admin-dashboard__row')
|
|
||||||
await rows[0].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(wrapper.text()).toContain('Hej fin bil!')
|
|
||||||
|
|
||||||
await rows[0].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(wrapper.text()).not.toContain('Hej fin bil!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('only expands one row at a time', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const rows = wrapper.findAll('.admin-dashboard__row')
|
|
||||||
await rows[0].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(wrapper.text()).toContain('Hej fin bil!')
|
|
||||||
|
|
||||||
await rows[1].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
expect(wrapper.text()).not.toContain('Hej fin bil!')
|
|
||||||
expect(wrapper.text()).toContain('Vill köpa din bil.')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders status dropdowns', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const selects = wrapper.findAll('.admin-dashboard__status-select')
|
|
||||||
expect(selects.length).toBe(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fires status update API on dropdown change', async () => {
|
|
||||||
vi.mocked(globalThis.fetch)
|
|
||||||
.mockResolvedValueOnce(mockFetchResponse(200, mockOrders))
|
|
||||||
.mockResolvedValueOnce(
|
|
||||||
mockFetchResponse(200, { ...mockOrders[0], status: 'paid' }),
|
|
||||||
)
|
|
||||||
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const selects = wrapper.findAll('.admin-dashboard__status-select')
|
|
||||||
await selects[0].trigger('change')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
||||||
'/api/admin/orders/c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11/status',
|
|
||||||
expect.objectContaining({
|
|
||||||
method: 'PATCH',
|
|
||||||
body: '{"status":"sent"}',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows status error on failed update', async () => {
|
|
||||||
vi.mocked(globalThis.fetch)
|
|
||||||
.mockResolvedValueOnce(mockFetchResponse(200, mockOrders))
|
|
||||||
.mockResolvedValueOnce(
|
|
||||||
mockFetchResponse(500, { message: 'Server error' }),
|
|
||||||
)
|
|
||||||
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const selects = wrapper.findAll('.admin-dashboard__status-select')
|
|
||||||
await selects[0].trigger('change')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('Kunde inte uppdatera status')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('formats dates in Swedish locale', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('2026')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows tracking input in expanded row', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const rows = wrapper.findAll('.admin-dashboard__row')
|
|
||||||
await rows[0].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
expect(wrapper.find('.admin-dashboard__tracking').exists()).toBe(true)
|
|
||||||
expect(wrapper.find('.admin-dashboard__tracking-input').exists()).toBe(true)
|
|
||||||
expect(wrapper.find('.admin-dashboard__tracking-save').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows tracking link when trackingId is set', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const rows = wrapper.findAll('.admin-dashboard__row')
|
|
||||||
await rows[0].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const link = wrapper.find('.admin-dashboard__tracking-link')
|
|
||||||
expect(link.exists()).toBe(true)
|
|
||||||
expect(link.attributes('href')).toContain('postnord')
|
|
||||||
expect(link.attributes('target')).toBe('_blank')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('hides tracking link when trackingId is null', async () => {
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const rows = wrapper.findAll('.admin-dashboard__row')
|
|
||||||
await rows[1].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const link = wrapper.find('.admin-dashboard__tracking-link')
|
|
||||||
expect(link.exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fires PATCH on tracking save button click', async () => {
|
|
||||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
||||||
mockFetchResponse(200, mockOrders),
|
|
||||||
)
|
|
||||||
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const rows = wrapper.findAll('.admin-dashboard__row')
|
|
||||||
await rows[1].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
await wrapper.find('.admin-dashboard__tracking-save').trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
||||||
'/api/admin/orders/c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12',
|
|
||||||
expect.objectContaining({
|
|
||||||
method: 'PATCH',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows tracking error on failed save', async () => {
|
|
||||||
vi.mocked(globalThis.fetch)
|
|
||||||
.mockResolvedValueOnce(mockFetchResponse(200, mockOrders))
|
|
||||||
.mockResolvedValueOnce(
|
|
||||||
mockFetchResponse(500, { message: 'Server error' }),
|
|
||||||
)
|
|
||||||
|
|
||||||
const { wrapper } = mountPage()
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
const rows = wrapper.findAll('.admin-dashboard__row')
|
|
||||||
await rows[1].trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
await wrapper.find('.admin-dashboard__tracking-save').trigger('click')
|
|
||||||
await new Promise((r) => setTimeout(r, 50))
|
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('Kunde inte spara spårnings-ID')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { mount } from '@vue/test-utils'
|
||||||
import { createPinia, setActivePinia } from 'pinia'
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
import ComposePage from '@/pages/ComposePage.vue'
|
import ComposePage from '@/pages/ComposePage.vue'
|
||||||
import PaymentRedirect from '@/pages/PaymentRedirect.vue'
|
|
||||||
|
|
||||||
vi.mock('@/api/orders', () => ({
|
vi.mock('@/api/orders', () => ({
|
||||||
createOrder: vi.fn(),
|
createOrder: vi.fn(),
|
||||||
|
|
@ -32,11 +31,6 @@ function createTestRouter() {
|
||||||
name: 'orders',
|
name: 'orders',
|
||||||
component: { template: '<div>Orders</div>' },
|
component: { template: '<div>Orders</div>' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/betalning/:orderId',
|
|
||||||
name: 'payment',
|
|
||||||
component: PaymentRedirect,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -128,13 +122,12 @@ describe('ComposePage', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('navigates to payment on success', async () => {
|
it('navigates to /orders on success', async () => {
|
||||||
mockCreateOrder.mockResolvedValue({
|
mockCreateOrder.mockResolvedValue({
|
||||||
id: 'order-1',
|
id: 'order-1',
|
||||||
plate: 'ABC123',
|
plate: 'ABC123',
|
||||||
status: 'pending_payment',
|
status: 'pending_payment',
|
||||||
trackingId: null,
|
trackingId: null,
|
||||||
amountPaid: null,
|
|
||||||
createdAt: '2025-01-01T00:00:00Z',
|
createdAt: '2025-01-01T00:00:00Z',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -145,8 +138,7 @@ describe('ComposePage', () => {
|
||||||
await button.trigger('submit')
|
await button.trigger('submit')
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(router.currentRoute.value.name).toBe('payment')
|
expect(router.currentRoute.value.name).toBe('orders')
|
||||||
expect(router.currentRoute.value.params.orderId).toBe('order-1')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { createPinia, setActivePinia } from 'pinia'
|
|
||||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
|
||||||
import PaymentRedirect from '@/pages/PaymentRedirect.vue'
|
|
||||||
import OrdersPage from '@/pages/OrdersPage.vue'
|
|
||||||
|
|
||||||
vi.mock('@/api/payment', () => ({
|
|
||||||
payOrder: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
import { payOrder } from '@/api/payment'
|
|
||||||
const mockPayOrder = vi.mocked(payOrder)
|
|
||||||
|
|
||||||
function createTestRouter() {
|
|
||||||
return createRouter({
|
|
||||||
history: createMemoryHistory(),
|
|
||||||
routes: [
|
|
||||||
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
|
||||||
{
|
|
||||||
path: '/betalning/:orderId',
|
|
||||||
name: 'payment',
|
|
||||||
component: PaymentRedirect,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/orders',
|
|
||||||
name: 'orders',
|
|
||||||
component: OrdersPage,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function mountPage(orderId = 'order-1', plate = 'ABC123') {
|
|
||||||
const pinia = createPinia()
|
|
||||||
setActivePinia(pinia)
|
|
||||||
|
|
||||||
const router = createTestRouter()
|
|
||||||
await router.push({
|
|
||||||
name: 'payment',
|
|
||||||
params: { orderId },
|
|
||||||
query: { plate },
|
|
||||||
})
|
|
||||||
await router.isReady()
|
|
||||||
|
|
||||||
const wrapper = mount(PaymentRedirect, {
|
|
||||||
global: { plugins: [router, pinia] },
|
|
||||||
})
|
|
||||||
|
|
||||||
return { wrapper, router }
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('PaymentRedirect', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders heading and amount', async () => {
|
|
||||||
const { wrapper } = await mountPage()
|
|
||||||
expect(wrapper.text()).toContain('Betalning')
|
|
||||||
expect(wrapper.text()).toContain('49 kr')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows plate from query', async () => {
|
|
||||||
const { wrapper } = await mountPage('order-1', 'ABC123')
|
|
||||||
expect(wrapper.text()).toContain('ABC123')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows Betalt button', async () => {
|
|
||||||
const { wrapper } = await mountPage()
|
|
||||||
const button = wrapper.find('.payment__button')
|
|
||||||
expect(button.exists()).toBe(true)
|
|
||||||
expect(button.text()).toBe('Betalt')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows mock payment note', async () => {
|
|
||||||
const { wrapper } = await mountPage()
|
|
||||||
expect(wrapper.text()).toContain('mock-betalning')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls payOrder on button click', async () => {
|
|
||||||
mockPayOrder.mockResolvedValue({
|
|
||||||
id: 'order-1',
|
|
||||||
plate: 'ABC123',
|
|
||||||
status: 'paid',
|
|
||||||
trackingId: null,
|
|
||||||
amountPaid: 49.0,
|
|
||||||
createdAt: '2025-01-01T00:00:00Z',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { wrapper } = await mountPage()
|
|
||||||
await wrapper.find('.payment__button').trigger('click')
|
|
||||||
|
|
||||||
expect(mockPayOrder).toHaveBeenCalledWith('order-1')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('navigates to orders on success', async () => {
|
|
||||||
mockPayOrder.mockResolvedValue({
|
|
||||||
id: 'order-1',
|
|
||||||
plate: 'ABC123',
|
|
||||||
status: 'paid',
|
|
||||||
trackingId: null,
|
|
||||||
amountPaid: 49.0,
|
|
||||||
createdAt: '2025-01-01T00:00:00Z',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { wrapper, router } = await mountPage()
|
|
||||||
await wrapper.find('.payment__button').trigger('click')
|
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
|
||||||
expect(router.currentRoute.value.name).toBe('orders')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows error on payment failure', async () => {
|
|
||||||
mockPayOrder.mockRejectedValue(new Error('Network error'))
|
|
||||||
|
|
||||||
const { wrapper } = await mountPage()
|
|
||||||
await wrapper.find('.payment__button').trigger('click')
|
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
|
||||||
expect(wrapper.text()).toContain('Kunde inte genomföra betalningen')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('disables button while paying', async () => {
|
|
||||||
mockPayOrder.mockImplementation(() => new Promise(() => {}))
|
|
||||||
|
|
||||||
const { wrapper } = await mountPage()
|
|
||||||
const button = wrapper.find('.payment__button')
|
|
||||||
await button.trigger('click')
|
|
||||||
|
|
||||||
expect(button.attributes('disabled')).toBeDefined()
|
|
||||||
expect(button.text()).toBe('Bearbetar...')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { request } from './client'
|
|
||||||
|
|
||||||
export interface AdminOrder {
|
|
||||||
id: string
|
|
||||||
email: string
|
|
||||||
plate: string
|
|
||||||
letterText: string
|
|
||||||
status: string
|
|
||||||
trackingId: string | null
|
|
||||||
amountPaid: number | null
|
|
||||||
createdAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchAllOrders(): Promise<AdminOrder[]> {
|
|
||||||
return request<AdminOrder[]>('/admin/orders')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateOrderStatus(
|
|
||||||
orderId: string,
|
|
||||||
status: string,
|
|
||||||
): Promise<AdminOrder> {
|
|
||||||
return request<AdminOrder>(`/admin/orders/${orderId}/status`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify({ status }),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTracking(
|
|
||||||
orderId: string,
|
|
||||||
trackingId: string | null,
|
|
||||||
): Promise<AdminOrder> {
|
|
||||||
return request<AdminOrder>(`/admin/orders/${orderId}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify({ trackingId }),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,6 @@ export interface Order {
|
||||||
plate: string
|
plate: string
|
||||||
status: string
|
status: string
|
||||||
trackingId: string | null
|
trackingId: string | null
|
||||||
amountPaid: number | null
|
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { request } from './client'
|
|
||||||
import type { Order } from './orders'
|
|
||||||
|
|
||||||
export function payOrder(orderId: string): Promise<Order> {
|
|
||||||
return request<Order>(`/payment/${orderId}/pay`, {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,467 +1,28 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
import { ref, onMounted, reactive } from 'vue'
|
|
||||||
import {
|
|
||||||
fetchAllOrders,
|
|
||||||
updateOrderStatus,
|
|
||||||
updateTracking,
|
|
||||||
type AdminOrder,
|
|
||||||
} from '@/api/admin'
|
|
||||||
|
|
||||||
const orders = ref<AdminOrder[]>([])
|
|
||||||
const expandedOrderId = ref<string | null>(null)
|
|
||||||
const loading = ref(true)
|
|
||||||
const error = ref('')
|
|
||||||
const statusError = ref('')
|
|
||||||
const trackingError = ref('')
|
|
||||||
const trackingInputValues = reactive<Record<string, string>>({})
|
|
||||||
|
|
||||||
const statusLabels: Record<string, string> = {
|
|
||||||
pending_payment: 'Väntar på betalning',
|
|
||||||
paid: 'Betalad',
|
|
||||||
lookup_started: 'Hanteras',
|
|
||||||
sent: 'Skickat',
|
|
||||||
delivered: 'Levererat',
|
|
||||||
failed: 'Misslyckad',
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusClasses: Record<string, string> = {
|
|
||||||
pending_payment: 'badge--gray',
|
|
||||||
paid: 'badge--blue',
|
|
||||||
lookup_started: 'badge--blue',
|
|
||||||
sent: 'badge--green',
|
|
||||||
delivered: 'badge--green',
|
|
||||||
failed: 'badge--red',
|
|
||||||
}
|
|
||||||
|
|
||||||
const allStatuses = [
|
|
||||||
'pending_payment',
|
|
||||||
'paid',
|
|
||||||
'lookup_started',
|
|
||||||
'sent',
|
|
||||||
'delivered',
|
|
||||||
'failed',
|
|
||||||
]
|
|
||||||
|
|
||||||
function formatDate(iso: string): string {
|
|
||||||
return new Date(iso).toLocaleDateString('sv-SE', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleExpand(orderId: string) {
|
|
||||||
if (expandedOrderId.value === orderId) {
|
|
||||||
expandedOrderId.value = null
|
|
||||||
} else {
|
|
||||||
expandedOrderId.value = orderId
|
|
||||||
const order = orders.value.find((o) => o.id === orderId)
|
|
||||||
if (order && !(orderId in trackingInputValues)) {
|
|
||||||
trackingInputValues[orderId] = order.trackingId ?? ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleStatusChange(orderId: string, newStatus: string) {
|
|
||||||
const order = orders.value.find((o) => o.id === orderId)
|
|
||||||
if (!order) return
|
|
||||||
|
|
||||||
const previousStatus = order.status
|
|
||||||
order.status = newStatus
|
|
||||||
statusError.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateOrderStatus(orderId, newStatus)
|
|
||||||
} catch {
|
|
||||||
order.status = previousStatus
|
|
||||||
statusError.value = 'Kunde inte uppdatera status. Försök igen.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleTrackingSave(orderId: string) {
|
|
||||||
const newTrackingId = trackingInputValues[orderId]?.trim() || null
|
|
||||||
const order = orders.value.find((o) => o.id === orderId)
|
|
||||||
if (!order) return
|
|
||||||
|
|
||||||
const previousTrackingId = order.trackingId
|
|
||||||
order.trackingId = newTrackingId
|
|
||||||
trackingError.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateTracking(orderId, newTrackingId)
|
|
||||||
} catch {
|
|
||||||
order.trackingId = previousTrackingId
|
|
||||||
trackingError.value = 'Kunde inte spara spårnings-ID. Försök igen.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
orders.value = await fetchAllOrders()
|
|
||||||
} catch {
|
|
||||||
error.value = 'Kunde inte hämta beställningar. Försök igen senare.'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="admin-dashboard">
|
<div class="admin">
|
||||||
<h1 class="admin-dashboard__title">Administration</h1>
|
<h1 class="admin__title">Administration</h1>
|
||||||
<p class="admin-dashboard__subtitle">
|
<p class="admin__subtitle">Hantera beställningar, mallar och användare.</p>
|
||||||
Hantera beställningar, mallar och användare.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p v-if="loading" class="admin-dashboard__loading">
|
|
||||||
Laddar beställningar...
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p v-else-if="error" class="admin-dashboard__error">{{ error }}</p>
|
|
||||||
|
|
||||||
<p v-else-if="orders.length === 0" class="admin-dashboard__empty">
|
|
||||||
Inga beställningar ännu.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div v-else class="admin-dashboard__table-wrapper">
|
|
||||||
<p v-if="statusError" class="admin-dashboard__status-error">
|
|
||||||
{{ statusError }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<table class="admin-dashboard__table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>E-post</th>
|
|
||||||
<th>Regnr</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template v-for="order in orders" :key="order.id">
|
|
||||||
<tr
|
|
||||||
class="admin-dashboard__row"
|
|
||||||
:class="{
|
|
||||||
'admin-dashboard__row--expanded': expandedOrderId === order.id,
|
|
||||||
}"
|
|
||||||
@click="toggleExpand(order.id)"
|
|
||||||
>
|
|
||||||
<td>{{ formatDate(order.createdAt) }}</td>
|
|
||||||
<td>{{ order.email }}</td>
|
|
||||||
<td class="admin-dashboard__plate">{{ order.plate }}</td>
|
|
||||||
<td>
|
|
||||||
<select
|
|
||||||
class="admin-dashboard__status-select"
|
|
||||||
:class="statusClasses[order.status] || 'badge--gray'"
|
|
||||||
:value="order.status"
|
|
||||||
@change="
|
|
||||||
handleStatusChange(
|
|
||||||
order.id,
|
|
||||||
($event.target as HTMLSelectElement).value,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<option v-for="s in allStatuses" :key="s" :value="s">
|
|
||||||
{{ statusLabels[s] }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td class="admin-dashboard__expand">
|
|
||||||
<span class="admin-dashboard__chevron">
|
|
||||||
{{ expandedOrderId === order.id ? '▼' : '▶' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
v-if="expandedOrderId === order.id"
|
|
||||||
class="admin-dashboard__expanded-row"
|
|
||||||
>
|
|
||||||
<td :colspan="5">
|
|
||||||
<div class="admin-dashboard__letter">
|
|
||||||
<div class="admin-dashboard__letter-label">Brevtext</div>
|
|
||||||
<div class="admin-dashboard__letter-text">
|
|
||||||
{{ order.letterText }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-dashboard__tracking">
|
|
||||||
<div class="admin-dashboard__tracking-header">
|
|
||||||
<span class="admin-dashboard__tracking-label"
|
|
||||||
>Spårnings-ID</span
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
v-if="order.trackingId"
|
|
||||||
class="admin-dashboard__tracking-link"
|
|
||||||
:href="`https://www.postnord.se/verktyg/spara/?id=${order.trackingId}`"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
Spåra hos PostNord ↗
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p v-if="trackingError" class="admin-dashboard__status-error">
|
|
||||||
{{ trackingError }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="admin-dashboard__tracking-input-row">
|
|
||||||
<input
|
|
||||||
class="admin-dashboard__tracking-input"
|
|
||||||
type="text"
|
|
||||||
:value="
|
|
||||||
trackingInputValues[order.id] ?? order.trackingId ?? ''
|
|
||||||
"
|
|
||||||
placeholder="PN..."
|
|
||||||
@input="
|
|
||||||
trackingInputValues[order.id] = (
|
|
||||||
$event.target as HTMLInputElement
|
|
||||||
).value
|
|
||||||
"
|
|
||||||
@click.stop
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="admin-dashboard__tracking-save"
|
|
||||||
@click.stop="handleTrackingSave(order.id)"
|
|
||||||
>
|
|
||||||
Spara spårning
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.admin-dashboard {
|
.admin {
|
||||||
max-width: 64rem;
|
max-width: 48rem;
|
||||||
margin: 3rem auto 0;
|
margin: 3rem auto 0;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard__title {
|
.admin__title {
|
||||||
margin: 0 0 0.25rem 0;
|
margin: 0 0 0.25rem 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: #1a202c;
|
color: #1a202c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard__subtitle {
|
.admin__subtitle {
|
||||||
margin: 0 0 1.5rem 0;
|
margin: 0;
|
||||||
color: #718096;
|
color: #718096;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard__loading,
|
|
||||||
.admin-dashboard__error,
|
|
||||||
.admin-dashboard__empty {
|
|
||||||
margin: 2rem 0;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__loading {
|
|
||||||
color: #718096;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__error {
|
|
||||||
background: #fff5f5;
|
|
||||||
border: 1px solid #fed7d7;
|
|
||||||
color: #c53030;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__empty {
|
|
||||||
background: #f7fafc;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
color: #718096;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__status-error {
|
|
||||||
margin: 0 0 0.75rem 0;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
background: #fff5f5;
|
|
||||||
border: 1px solid #fed7d7;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
color: #c53030;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__table thead {
|
|
||||||
background: #f7fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__table th {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #718096;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
border-bottom: 2px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__row {
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
transition: background 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__row:hover {
|
|
||||||
background: #f7fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__row--expanded {
|
|
||||||
background: #ebf8ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__row td {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
color: #4a5568;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__plate {
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: #1a202c !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__status-select {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4a5568;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__status-select:focus {
|
|
||||||
border-color: #4299e1;
|
|
||||||
box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__expand {
|
|
||||||
text-align: center;
|
|
||||||
width: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__chevron {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
color: #a0aec0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__expanded-row td {
|
|
||||||
padding: 0;
|
|
||||||
background: #f7fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__letter {
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
border-top: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__letter-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #a0aec0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__letter-text {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #4a5568;
|
|
||||||
line-height: 1.6;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking {
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
border-top: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #a0aec0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking-link {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: #4299e1;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking-input-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: #4a5568;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking-input:focus {
|
|
||||||
border-color: #4299e1;
|
|
||||||
box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking-save {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
background: #4299e1;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-dashboard__tracking-save:hover {
|
|
||||||
background: #3182ce;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,8 @@ async function handleSubmit() {
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const order = await createOrder(plate.value, letterText.value)
|
await createOrder(plate.value, letterText.value)
|
||||||
await router.push({
|
await router.push({ name: 'orders' })
|
||||||
name: 'payment',
|
|
||||||
params: { orderId: order.id },
|
|
||||||
query: { plate: plate.value },
|
|
||||||
})
|
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage.value = 'Kunde inte skapa beställningen. Försök igen senare.'
|
errorMessage.value = 'Kunde inte skapa beställningen. Försök igen senare.'
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
|
||||||
import { payOrder } from '@/api/payment'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const orderId = route.params.orderId as string
|
|
||||||
const paying = ref(false)
|
|
||||||
const error = ref('')
|
|
||||||
|
|
||||||
async function handlePay() {
|
|
||||||
paying.value = true
|
|
||||||
error.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
await payOrder(orderId)
|
|
||||||
await router.push({ name: 'orders' })
|
|
||||||
} catch {
|
|
||||||
error.value = 'Kunde inte genomföra betalningen. Försök igen.'
|
|
||||||
} finally {
|
|
||||||
paying.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="payment">
|
|
||||||
<h1 class="payment__title">Betalning</h1>
|
|
||||||
<p class="payment__subtitle">
|
|
||||||
Registreringsnummer: <strong>{{ route.query.plate || '—' }}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="payment__card">
|
|
||||||
<div class="payment__amount-row">
|
|
||||||
<span class="payment__label">Att betala</span>
|
|
||||||
<span class="payment__amount">49 kr</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p v-if="error" class="payment__error">{{ error }}</p>
|
|
||||||
|
|
||||||
<button class="payment__button" :disabled="paying" @click="handlePay">
|
|
||||||
{{ paying ? 'Bearbetar...' : 'Betalt' }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p class="payment__note">
|
|
||||||
Detta är en mock-betalning. I framtiden skickas du till Stripe.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.payment {
|
|
||||||
max-width: 28rem;
|
|
||||||
margin: 3rem auto 0;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__title {
|
|
||||||
margin: 0 0 0.25rem 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #1a202c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__subtitle {
|
|
||||||
margin: 0 0 1.5rem 0;
|
|
||||||
color: #718096;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__amount-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
padding-bottom: 1.25rem;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__label {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #718096;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__amount {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1a202c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__error {
|
|
||||||
margin: 0 0 0.75rem 0;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
background: #fff5f5;
|
|
||||||
border: 1px solid #fed7d7;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
color: #c53030;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background: #48bb78;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__button:hover:not(:disabled) {
|
|
||||||
background: #38a169;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__button:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment__note {
|
|
||||||
margin: 0.75rem 0 0 0;
|
|
||||||
color: #a0aec0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -7,7 +7,6 @@ import RegisterPage from '@/pages/RegisterPage.vue'
|
||||||
import LoginPage from '@/pages/LoginPage.vue'
|
import LoginPage from '@/pages/LoginPage.vue'
|
||||||
import OrdersPage from '@/pages/OrdersPage.vue'
|
import OrdersPage from '@/pages/OrdersPage.vue'
|
||||||
import AdminPage from '@/pages/AdminPage.vue'
|
import AdminPage from '@/pages/AdminPage.vue'
|
||||||
import PaymentRedirect from '@/pages/PaymentRedirect.vue'
|
|
||||||
import { useAuthStore } from '@/stores/authStore'
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
import { getActivePinia } from 'pinia'
|
import { getActivePinia } from 'pinia'
|
||||||
|
|
||||||
|
|
@ -37,12 +36,6 @@ const router = createRouter({
|
||||||
component: AdminPage,
|
component: AdminPage,
|
||||||
meta: { requiresAuth: true, requiresAdmin: true },
|
meta: { requiresAuth: true, requiresAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/betalning/:orderId',
|
|
||||||
name: 'payment',
|
|
||||||
component: PaymentRedirect,
|
|
||||||
meta: { requiresAuth: true },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/registrera',
|
path: '/registrera',
|
||||||
name: 'register',
|
name: 'register',
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,5 @@ export default defineConfig({
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: ['src/__tests__/setup.ts'],
|
setupFiles: ['src/__tests__/setup.ts'],
|
||||||
exclude: ['e2e/**', 'node_modules/**'],
|
exclude: ['e2e/**', 'node_modules/**'],
|
||||||
coverage: {
|
|
||||||
provider: 'v8',
|
|
||||||
reporter: ['text', 'html', 'lcov', 'json'],
|
|
||||||
reportsDirectory: './coverage',
|
|
||||||
thresholds: {
|
|
||||||
lines: 70,
|
|
||||||
branches: 60,
|
|
||||||
functions: 70,
|
|
||||||
statements: 70,
|
|
||||||
},
|
|
||||||
exclude: ['src/__tests__/**', 'e2e/**'],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue