Skip to content

Commit 2dc32d4

Browse files
authored
Merge pull request #57 from DamnDamnDamnM3/feature/dashboard
feat: 新增統計 Dashboard
2 parents 1a84602 + 8b9a7d0 commit 2dc32d4

13 files changed

Lines changed: 567 additions & 1 deletion

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package com.vehicle.management.api.controller;
2+
3+
import com.vehicle.management.api.dto.MonthlyStatsResponse;
4+
import com.vehicle.management.api.dto.StatsOverviewResponse;
5+
import com.vehicle.management.api.dto.UserActivityResponse;
6+
import com.vehicle.management.api.dto.VehicleUtilizationResponse;
7+
import com.vehicle.management.domain.model.BorrowingRequest;
8+
import com.vehicle.management.domain.model.User;
9+
import com.vehicle.management.domain.model.Vehicle;
10+
import com.vehicle.management.domain.model.VehicleStatus;
11+
import com.vehicle.management.repository.IBorrowingRepository;
12+
import com.vehicle.management.repository.IUserRepository;
13+
import com.vehicle.management.repository.IVehicleRepository;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.web.bind.annotation.GetMapping;
16+
import org.springframework.web.bind.annotation.RequestMapping;
17+
import org.springframework.web.bind.annotation.RestController;
18+
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
import java.time.ZoneId;
22+
import java.time.ZonedDateTime;
23+
import java.time.format.DateTimeFormatter;
24+
import java.util.*;
25+
import java.util.stream.Collectors;
26+
27+
@RestController
28+
@RequestMapping("/api/stats")
29+
public class StatsController {
30+
31+
private final IBorrowingRepository borrowingRepo;
32+
private final IVehicleRepository vehicleRepo;
33+
private final IUserRepository userRepo;
34+
35+
public StatsController(IBorrowingRepository borrowingRepo,
36+
IVehicleRepository vehicleRepo,
37+
IUserRepository userRepo) {
38+
this.borrowingRepo = borrowingRepo;
39+
this.vehicleRepo = vehicleRepo;
40+
this.userRepo = userRepo;
41+
}
42+
43+
/**
44+
* GET /api/stats/overview
45+
* 回傳系統總覽 KPI:車輛數、可用車輛、待審核、使用中、總使用者數。
46+
*/
47+
@GetMapping("/overview")
48+
public ResponseEntity<StatsOverviewResponse> overview() {
49+
List<Vehicle> vehicles = vehicleRepo.findAll();
50+
List<BorrowingRequest> borrowings = borrowingRepo.findAll();
51+
List<User> users = userRepo.findAll();
52+
53+
long totalVehicles = vehicles.size();
54+
long availableVehicles = vehicles.stream()
55+
.filter(v -> v.getStatus() == VehicleStatus.AVAILABLE)
56+
.count();
57+
long pendingRequests = borrowings.stream()
58+
.filter(b -> "PENDING".equals(b.getStateName()))
59+
.count();
60+
long activeUses = borrowings.stream()
61+
.filter(b -> "IN_USE".equals(b.getStateName()))
62+
.count();
63+
long totalUsers = users.size();
64+
65+
return ResponseEntity.ok(new StatsOverviewResponse(
66+
totalVehicles, availableVehicles, pendingRequests, activeUses, totalUsers));
67+
}
68+
69+
/**
70+
* GET /api/stats/monthly
71+
* 回傳近 12 個月每月借用申請數量(以 createdAt 為基準)。
72+
*/
73+
@GetMapping("/monthly")
74+
public ResponseEntity<List<MonthlyStatsResponse>> monthly() {
75+
List<BorrowingRequest> borrowings = borrowingRepo.findAll();
76+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM").withZone(ZoneId.systemDefault());
77+
78+
// 建立近 12 個月的月份清單(含零值)
79+
ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault());
80+
List<String> months = new ArrayList<>();
81+
for (int i = 11; i >= 0; i--) {
82+
months.add(now.minusMonths(i).format(formatter));
83+
}
84+
85+
// 依月份統計
86+
Map<String, Long> countMap = borrowings.stream()
87+
.collect(Collectors.groupingBy(
88+
b -> formatter.format(b.getCreatedAt()),
89+
Collectors.counting()
90+
));
91+
92+
List<MonthlyStatsResponse> result = months.stream()
93+
.map(m -> new MonthlyStatsResponse(m, countMap.getOrDefault(m, 0L).intValue()))
94+
.collect(Collectors.toList());
95+
96+
return ResponseEntity.ok(result);
97+
}
98+
99+
/**
100+
* GET /api/stats/vehicle-utilization
101+
* 回傳每輛車的使用次數與總天數(只計 RETURNED 與 IN_USE 的申請)。
102+
*/
103+
@GetMapping("/vehicle-utilization")
104+
public ResponseEntity<List<VehicleUtilizationResponse>> vehicleUtilization() {
105+
List<BorrowingRequest> borrowings = borrowingRepo.findAll();
106+
List<Vehicle> vehicles = vehicleRepo.findAll();
107+
108+
// 建立 vehicleId → Vehicle 查找表
109+
Map<UUID, Vehicle> vehicleMap = vehicles.stream()
110+
.collect(Collectors.toMap(Vehicle::getId, v -> v));
111+
112+
// 只計算 RETURNED 或 IN_USE 的借用
113+
List<BorrowingRequest> active = borrowings.stream()
114+
.filter(b -> "RETURNED".equals(b.getStateName()) || "IN_USE".equals(b.getStateName()))
115+
.collect(Collectors.toList());
116+
117+
// 依 vehicleId 分組
118+
Map<UUID, List<BorrowingRequest>> grouped = active.stream()
119+
.collect(Collectors.groupingBy(BorrowingRequest::getVehicleId));
120+
121+
List<VehicleUtilizationResponse> result = grouped.entrySet().stream()
122+
.filter(e -> vehicleMap.containsKey(e.getKey()))
123+
.map(e -> {
124+
UUID vehicleId = e.getKey();
125+
Vehicle vehicle = vehicleMap.get(vehicleId);
126+
List<BorrowingRequest> reqs = e.getValue();
127+
int usageCount = reqs.size();
128+
long totalDays = reqs.stream()
129+
.mapToLong(b -> Duration.between(b.getPeriodStart(), b.getPeriodEnd()).toDays())
130+
.sum();
131+
return new VehicleUtilizationResponse(
132+
vehicleId, vehicle.getPlate(), vehicle.getModel(), usageCount, totalDays);
133+
})
134+
.sorted(Comparator.comparingInt(VehicleUtilizationResponse::usageCount).reversed())
135+
.collect(Collectors.toList());
136+
137+
return ResponseEntity.ok(result);
138+
}
139+
140+
/**
141+
* GET /api/stats/user-activity
142+
* 回傳借用申請數前 10 名的使用者。
143+
*/
144+
@GetMapping("/user-activity")
145+
public ResponseEntity<List<UserActivityResponse>> userActivity() {
146+
List<BorrowingRequest> borrowings = borrowingRepo.findAll();
147+
List<User> users = userRepo.findAll();
148+
149+
// 建立 userId → User 查找表
150+
Map<UUID, User> userMap = users.stream()
151+
.collect(Collectors.toMap(User::getId, u -> u));
152+
153+
// 依 userId 統計申請數
154+
Map<UUID, Long> countMap = borrowings.stream()
155+
.collect(Collectors.groupingBy(BorrowingRequest::getUserId, Collectors.counting()));
156+
157+
List<UserActivityResponse> result = countMap.entrySet().stream()
158+
.filter(e -> userMap.containsKey(e.getKey()))
159+
.map(e -> {
160+
User user = userMap.get(e.getKey());
161+
return new UserActivityResponse(e.getKey(), user.getName(), e.getValue().intValue());
162+
})
163+
.sorted(Comparator.comparingInt(UserActivityResponse::requestCount).reversed())
164+
.limit(10)
165+
.collect(Collectors.toList());
166+
167+
return ResponseEntity.ok(result);
168+
}
169+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.vehicle.management.api.dto;
2+
3+
public record MonthlyStatsResponse(
4+
String month,
5+
int count
6+
) {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.vehicle.management.api.dto;
2+
3+
public record StatsOverviewResponse(
4+
long totalVehicles,
5+
long availableVehicles,
6+
long pendingRequests,
7+
long activeUses,
8+
long totalUsers
9+
) {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.vehicle.management.api.dto;
2+
3+
import java.util.UUID;
4+
5+
public record UserActivityResponse(
6+
UUID userId,
7+
String name,
8+
int requestCount
9+
) {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.vehicle.management.api.dto;
2+
3+
import java.util.UUID;
4+
5+
public record VehicleUtilizationResponse(
6+
UUID vehicleId,
7+
String plate,
8+
String model,
9+
int usageCount,
10+
long totalDays
11+
) {}

backend/src/main/java/com/vehicle/management/service/BorrowingService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ public List<BorrowingRequest> findConflicts(UUID vehicleId, Instant start, Insta
232232

233233
/**
234234
* 取得指定時段內的借車申請(月曆視圖用)。
235+
*
236+
* @param start 時段開始時間
237+
* @param end 時段結束時間
238+
* @return 時段內的申請清單(不含已拒絕)
235239
*/
236240
public List<BorrowingRequest> listInRange(Instant start, Instant end) {
237241
return borrowingRepo.findInRange(start, end);

frontend/package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"dependencies": {
1111
"axios": "^1.7.7",
12+
"chart.js": "^4.5.1",
1213
"pinia": "^2.2.2",
1314
"vue": "^3.5.11",
1415
"vue-router": "^4.4.5"

frontend/src/App.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<span class="brand">🚗 車輛管理系統</span>
55
<div class="nav-links">
66
<template v-if="auth.isAdmin">
7+
<router-link to="/admin/dashboard">統計總覽</router-link>
78
<router-link to="/admin/review">審核申請</router-link>
89
<router-link to="/admin/vehicles">車輛管理</router-link>
910
<router-link to="/admin/maintenance">保養管理</router-link>

frontend/src/api/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ client.interceptors.response.use(
2424
},
2525
)
2626

27+
export { client }
2728
export default client

0 commit comments

Comments
 (0)