Skip to content

Commit 59c97bb

Browse files
authored
Merge pull request #199 from CapstoneProjectCMC/feature/sap-service-and-payment
Feature/sap service and payment
2 parents 6635120 + 1797a20 commit 59c97bb

File tree

22 files changed

+934
-26
lines changed

22 files changed

+934
-26
lines changed

README.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,22 +165,18 @@ Dự án Angular này áp dụng nhiều **design pattern chuẩn** để đảm
165165
- **Guards** (`AuthGuard`, `RoleGuard`) áp dụng strategy để quyết định quyền truy cập.
166166
- **Environments** (`dev`, `staging`, `prod`) chọn cấu hình phù hợp theo môi trường.
167167

168-
### 6. Facade Pattern
169-
- **Router Manager** gom logic routing vào 1 chỗ.
170-
- **NgRx Facade** (nếu sử dụng) để tách component khỏi chi tiết state management.
171-
172-
### 7. Template Pattern
168+
### 6. Template Pattern
173169
- Các **layout** (`header`, `sidebar`, `footer`) định nghĩa khung sẵn, feature module nhúng nội dung vào khu vực content.
174170

175-
### 8. Smart & Dumb Components (Container/Presenter)
171+
### 7. Smart & Dumb Components (Container/Presenter)
176172
- Component **Smart** xử lý logic, data fetching, và state.
177173
- Component **Dumb** nhận `@Input()` và emit `@Output()`, chỉ chịu trách nhiệm hiển thị.
178174

179-
### 9. Decorator Pattern
175+
### 8. Decorator Pattern
180176
- Angular decorators: `@Component`, `@Directive`, `@Pipe`, `@Injectable`.
181177
- Cho phép mở rộng chức năng mà không sửa code gốc.
182178

183-
### 10. DTO Pattern
179+
### 9. DTO Pattern
184180
- Các **model** trong `core/models` quản lý dữ liệu giữa API và component, đảm bảo type safety.
185181

186182
---

src/app/app.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ export const routes: Routes = [
138138
(m) => m.ServiceAndPaymentModule
139139
),
140140
},
141+
{
142+
path: 'codecampus-statistics',
143+
loadChildren: () =>
144+
import('./features/statistics/statistics.module').then(
145+
(m) => m.StatisticsModule
146+
),
147+
},
141148
{
142149
path: 'organization',
143150
loadChildren: () =>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export type ExerciseStatisticsResponse = {
2+
exerciseId: string;
3+
title: string;
4+
exerciseType: 'QUIZ' | 'CODING';
5+
visibility: boolean;
6+
orgId: string | null;
7+
assignedCount: number;
8+
completedCount: number;
9+
completionRate: number;
10+
submissionCount: number;
11+
passedCount: number;
12+
passRate: number;
13+
avgScore: number;
14+
lastSubmissionAt: string;
15+
};
16+
17+
export type SummaryStatisticsAdmin = {
18+
totalExercises: number;
19+
totalVisibleExercises: number;
20+
totalQuiz: number;
21+
totalCoding: number;
22+
totalAssignments: number;
23+
totalCompletedAssignments: number;
24+
totalSubmissions: number;
25+
totalPassedSubmissions: number;
26+
};

src/app/core/router-manager/horizontal-menu.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function getNavHorizontalItems(roles: string[]): SidebarItem[] {
3434
},
3535
{
3636
id: 'statistics',
37-
path: '/statistics',
37+
path: '/codecampus-statistics/admin-exercise-statistics',
3838
label: 'Thống kê',
3939
icon: 'fas fa-chart-bar',
4040
isVisible: !roles.includes(auth_lv2[0]),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { SidebarItem } from '../../models/data-handle';
2+
3+
export function sidebarStatisticsRouter(roles: string[]): SidebarItem[] {
4+
const auth_lv2 = ['ADMIN'];
5+
6+
return [
7+
{
8+
id: 'list-exericse-satistics',
9+
path: '/codecampus-statistics/admin-exercise-statistics',
10+
label: 'Thống kê bài tập',
11+
icon: 'fa-solid fa-file-contract',
12+
isVisible: !roles.includes(auth_lv2[0]),
13+
},
14+
{
15+
id: 'chart-exercise-statistics',
16+
path: '/codecampus-statistics/admin-chart-exercise-statistics',
17+
label: 'Biểu đồ thống kê',
18+
icon: 'fa-solid fa-chart-pie',
19+
isVisible: !roles.includes(auth_lv2[0]),
20+
},
21+
];
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Injectable } from '@angular/core';
2+
import { ApiMethod } from '../config-service/api.methods';
3+
import { ApiResponse, IPaginationResponse } from '../../models/api-response';
4+
import {
5+
ExerciseStatisticsResponse,
6+
SummaryStatisticsAdmin,
7+
} from '../../models/statistics.model';
8+
import { API_CONFIG } from '../config-service/api.enpoints';
9+
10+
@Injectable({
11+
providedIn: 'root',
12+
})
13+
export class StatisticsService {
14+
constructor(private api: ApiMethod) {}
15+
16+
getAdminExerciseStats(page: number, size: number) {
17+
return this.api.get<
18+
ApiResponse<IPaginationResponse<ExerciseStatisticsResponse[]>>
19+
>(API_CONFIG.ENDPOINTS.GET.GET_EXERCISE_STATISTICS_ADMIN(page, size));
20+
}
21+
22+
getAdminSummary() {
23+
return this.api.get<ApiResponse<SummaryStatisticsAdmin>>(
24+
API_CONFIG.ENDPOINTS.GET.GET_SUMMARY_STATISTICS_ADMIN
25+
);
26+
}
27+
}

src/app/core/services/config-service/api.enpoints.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ export const API_CONFIG = {
214214
) =>
215215
`/notification/my?page=${page}&size=${size}&readStatus=${readStatus}`,
216216
GET_COUNT_MY_UNREAD: '/notification/my/unread-count',
217+
218+
GET_EXERCISE_STATISTICS_ADMIN: (page: number, size: number) =>
219+
`/submission/stats/admin/exercises?page=${page}&size=${size}`,
220+
GET_SUMMARY_STATISTICS_ADMIN: '/submission/stats/admin/summary',
217221
},
218222
POST: {
219223
LOGIN: '/identity/auth/login',
@@ -288,7 +292,7 @@ export const API_CONFIG = {
288292
ADD_ADMIN: '/identity/admin',
289293
ADD_STUDENT: '/identity/teacher',
290294
ADD_TEACHER: '/identity/user',
291-
MARK_AS_READ_NOTIFICATION: '/my/mark-read',
295+
MARK_AS_READ_NOTIFICATION: '/notification/my/mark-read',
292296
},
293297
PUT: {
294298
EDIT_FILE: (id: string) => `/file/api/FileDocument/edit/${id}`,

src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ <h2>{{ exercise.title }}</h2>
2828
(click)="toggleMainDropdown()"
2929
>
3030
<i class="fa fa-edit"></i>
31-
<span>Sửa bài tập</span>
31+
<!-- <span>Sửa bài tập</span> -->
3232
</button>
3333
@if (isMainDropdownOpen) {
3434
<div class="edit-dropdown">
@@ -49,7 +49,7 @@ <h2>{{ exercise.title }}</h2>
4949
(click)="openConfirmDelete()"
5050
>
5151
<i class="fa fa-trash"></i>
52-
<span>Xóa bài tập</span>
52+
<!-- <span>Xóa bài tập</span> -->
5353
</button>
5454
</div>
5555
}
@@ -158,7 +158,7 @@ <h2>{{ exercise.title }}</h2>
158158
<div class="stats">
159159
<span title="Số câu hỏi"
160160
><i class="fa-regular fa-question-circle"></i>
161-
{{ exercise.quizDetail?.totalElements ?? 0 }}</span
161+
{{ exercise.quizDetail?.totalElements ?? 0 }} Câu</span
162162
>
163163
<span title="Thời lượng"
164164
><i class="fa-regular fa-hourglass-half"></i>
@@ -199,6 +199,8 @@ <h2>{{ exercise.title }}</h2>
199199
@for (q of exercise.quizDetail?.questions; track q; let i = $index) {
200200
<div class="question-item">
201201
<div class="question-header">
202+
<div class="question-number-index">Câu hỏi số {{ i + 1 }}</div>
203+
202204
<div class="btn-control">
203205
@if (isActionActive) {
204206
<button

src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
2525
padding: 20px 18px 18px 18px;
2626
max-width: 400px;
27-
min-width: 260px;
27+
min-width: 300px;
2828
.header {
2929
display: flex;
3030
// flex-direction: column;
@@ -341,6 +341,10 @@
341341
}
342342
}
343343
}
344+
345+
.question-number-index {
346+
font-weight: 700;
347+
}
344348
}
345349
.confirm-modal-overlay {
346350
position: fixed;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<div class="stats-container">
2+
<header class="stats-header">
3+
<h1>Bảng thống kê bài tập</h1>
4+
<p>Tổng quan về hiệu suất và tương tác của các bài tập.</p>
5+
</header>
6+
7+
@if (isLoading) {
8+
<div class="loading-overlay">
9+
<div class="spinner"></div>
10+
<p>Đang tải dữ liệu...</p>
11+
</div>
12+
} @else if (error && !isLoading) {
13+
<div class="error-message">
14+
<p>{{ error }}</p>
15+
<button class="btn-retry" (click)="loadStats()">Thử lại</button>
16+
</div>
17+
} @else {
18+
<div class="stats-content">
19+
@if (statsData.length > 0) {
20+
<div class="table-wrapper">
21+
<table class="stats-table">
22+
<thead>
23+
<tr>
24+
<th>Tiêu đề</th>
25+
<th>Loại</th>
26+
<th>Trạng thái</th>
27+
<th>Lượt hoàn thành</th>
28+
<th>Tỷ lệ hoàn thành</th>
29+
<th>Lượt nộp bài</th>
30+
<th>Tỷ lệ qua</th>
31+
<th>Điểm TB</th>
32+
<th>Lần nộp cuối</th>
33+
</tr>
34+
</thead>
35+
<tbody>
36+
@for (stat of statsData; track $index) {
37+
<tr>
38+
<td class="cell-title" [title]="stat.title">
39+
{{ stat.title }}
40+
</td>
41+
<td>
42+
<span
43+
class="badge"
44+
[class.badge-quiz]="stat.exerciseType === 'QUIZ'"
45+
[class.badge-coding]="stat.exerciseType === 'CODING'"
46+
>
47+
{{ stat.exerciseType }}
48+
</span>
49+
</td>
50+
<td>{{ stat.visibility ? "Công khai" : "Riêng tư" }}</td>
51+
<td>{{ stat.completedCount }} / {{ stat.assignedCount }}</td>
52+
<td>
53+
<div
54+
class="progress-bar-container"
55+
[title]="stat.completionRate | percent : '1.0-2'"
56+
>
57+
<div
58+
class="progress-bar-fill"
59+
[style.width.%]="stat.completionRate * 100"
60+
></div>
61+
</div>
62+
</td>
63+
<td>{{ stat.passedCount }} / {{ stat.submissionCount }}</td>
64+
<td>
65+
<div
66+
class="progress-bar-container"
67+
[title]="stat.passRate | percent : '0.0-1'"
68+
>
69+
<div
70+
class="progress-bar-fill success"
71+
[style.width.%]="stat.passRate * 100"
72+
></div>
73+
</div>
74+
</td>
75+
<td>{{ stat.avgScore | number : "1.1-2" }}</td>
76+
<td>{{ stat.lastSubmissionAt | date : "dd/MM/yyyy HH:mm" }}</td>
77+
</tr>
78+
}
79+
</tbody>
80+
</table>
81+
</div>
82+
83+
@if (totalPages > 1) {
84+
<nav class="pagination-controls" aria-label="Page navigation">
85+
<button
86+
(click)="onPageChange(currentPage - 1)"
87+
[disabled]="currentPage === 1"
88+
>
89+
&laquo;
90+
</button>
91+
@for (page of getPagesArray(); track page) {
92+
<button
93+
(click)="onPageChange(page)"
94+
[class.active]="page === currentPage"
95+
>
96+
{{ page }}
97+
</button>
98+
}
99+
<button
100+
(click)="onPageChange(currentPage + 1)"
101+
[disabled]="currentPage === totalPages"
102+
>
103+
&raquo;
104+
</button>
105+
</nav>
106+
} } @else {
107+
<div class="no-data-message">
108+
<p>Không có dữ liệu thống kê để hiển thị.</p>
109+
</div>
110+
}
111+
</div>
112+
}
113+
</div>

0 commit comments

Comments
 (0)