From 5f05a8991f3a6ce4f3800133e855840539cb84d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B4=20Quang=20=C4=90=E1=BB=A9c?= Date: Thu, 4 Sep 2025 07:11:47 +0700 Subject: [PATCH] fix some bug --- .../handle/error-handler.service.ts | 2 +- src/app/core/models/notice.model.ts | 18 ++ src/app/core/models/user.models.ts | 17 ++ .../api-service/notification-list.service.ts | 34 +++ .../core/services/api-service/user.service.ts | 18 ++ .../services/config-service/api.enpoints.ts | 13 +- .../create-user-modal.component.html | 182 +++++++++++++ .../create-user-modal.component.scss | 230 ++++++++++++++++ .../create-user-modal.component.ts | 230 ++++++++++++++++ .../pages/user-list/user-list.html | 252 +++++++++--------- .../pages/user-list/user-list.ts | 96 ++++--- .../set-password-modal.component.ts | 10 +- .../exercise-code-details.component.ts | 2 +- .../exercise-details.component.ts | 2 +- .../saved-posts-list.component.ts | 9 +- .../qr-payment/qr-payment.component.html | 157 ++++++----- .../components/my-shared/header/header.html | 147 +++++----- .../components/my-shared/header/header.ts | 11 +- .../notification-modal.component.html | 35 +++ .../notification-modal.component.scss | 122 +++++++++ .../notification-modal.component.ts | 98 +++++++ 21 files changed, 1342 insertions(+), 343 deletions(-) create mode 100644 src/app/core/models/notice.model.ts create mode 100644 src/app/core/services/api-service/notification-list.service.ts create mode 100644 src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.html create mode 100644 src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.scss create mode 100644 src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.ts create mode 100644 src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.html create mode 100644 src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.scss create mode 100644 src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.ts diff --git a/src/app/core/interceptors/handle/error-handler.service.ts b/src/app/core/interceptors/handle/error-handler.service.ts index bdf80487..0866ee1a 100644 --- a/src/app/core/interceptors/handle/error-handler.service.ts +++ b/src/app/core/interceptors/handle/error-handler.service.ts @@ -45,7 +45,7 @@ export class ErrorHandlerService { sendNotification( this.store, 'Hết tiền!', - 'Bạn cần nạp tiền vào tài khoản của mình trước nhé!', + 'Bạn cần nạp tiền vào tài khoản của mình trước để kích hoạt ví nhé!', 'error' ); } else { diff --git a/src/app/core/models/notice.model.ts b/src/app/core/models/notice.model.ts new file mode 100644 index 00000000..1d9bd6ed --- /dev/null +++ b/src/app/core/models/notice.model.ts @@ -0,0 +1,18 @@ +export type ReadStatusNotice = 'ALL' | 'READ' | 'UNREAD'; + +export type DeliveryStatus = 'PENDING' | 'SENT' | 'FAILED'; + +export type GetAllNoticeResponse = { + id: string; + recipient: string; + channel: ReadStatusNotice; // "SOCKET" | "EMAIL" | "ALL"... + templateCode: string; + subject: string; + body: string; + param: { class: string; exercise: string }; + readStatus: string; + readAt: null; + deliveryStatus: DeliveryStatus; //// PENDING | SENT | FAILED + deliveredAt: string; + createdAt: string; +}; diff --git a/src/app/core/models/user.models.ts b/src/app/core/models/user.models.ts index 7ef70af3..39d2840a 100644 --- a/src/app/core/models/user.models.ts +++ b/src/app/core/models/user.models.ts @@ -77,3 +77,20 @@ export type RequestForgotPasswordResponse = { email: string; message: string; }; + +export type CreateAccoutByAdmin = { + username: string; + email: string; + password: string; + firstName: string; + lastName: string; + dob: string; + bio: string; + gender: boolean; + displayName: string; + education: number; + links: string[]; + city: string; + organizationId: string; + organizationMemberRole: 'ADMIN' | 'TEACHER' | 'STUDENT'; +}; diff --git a/src/app/core/services/api-service/notification-list.service.ts b/src/app/core/services/api-service/notification-list.service.ts new file mode 100644 index 00000000..758cbbb6 --- /dev/null +++ b/src/app/core/services/api-service/notification-list.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; + +import { ApiMethod } from '../config-service/api.methods'; +import { ApiResponse, IPaginationResponse } from '../../models/api-response'; +import { API_CONFIG } from '../config-service/api.enpoints'; +import { + GetAllNoticeResponse, + ReadStatusNotice, +} from '../../models/notice.model'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationListService { + constructor(private api: ApiMethod) {} + getAllMyNotification( + page: number, + size: number, + statusRead: ReadStatusNotice + ) { + return this.api.get< + ApiResponse> + >( + API_CONFIG.ENDPOINTS.GET.GET_ALL_MY_NOTIFICATIONS(page, size, statusRead) + ); + } + + markAsReadNotification(Ids: string[]) { + return this.api.post>( + API_CONFIG.ENDPOINTS.POST.MARK_AS_READ_NOTIFICATION, + Ids + ); + } +} diff --git a/src/app/core/services/api-service/user.service.ts b/src/app/core/services/api-service/user.service.ts index af3f9494..f0414634 100644 --- a/src/app/core/services/api-service/user.service.ts +++ b/src/app/core/services/api-service/user.service.ts @@ -3,6 +3,7 @@ import { ApiMethod } from '../config-service/api.methods'; import { EnumType } from '../../models/data-handle'; import { ApiResponse, IPaginationResponse } from '../../models/api-response'; import { + CreateAccoutByAdmin, SearchingUser, SearchUserProfileResponse, User, @@ -45,4 +46,21 @@ export class UserService { {} ); } + + createAccountUser( + role: 'ADMIN' | 'STUDENT' | 'TEACHER', + data: CreateAccoutByAdmin + ) { + let enpoint = ''; + + if (role === 'ADMIN') { + enpoint = API_CONFIG.ENDPOINTS.POST.ADD_ADMIN; + } else if (role === 'STUDENT') { + enpoint = API_CONFIG.ENDPOINTS.POST.ADD_STUDENT; + } else if (role === 'TEACHER') { + enpoint = API_CONFIG.ENDPOINTS.POST.ADD_TEACHER; + } + + return this.api.post>(enpoint, data); + } } diff --git a/src/app/core/services/config-service/api.enpoints.ts b/src/app/core/services/config-service/api.enpoints.ts index ffc5742f..13f3dfaa 100644 --- a/src/app/core/services/config-service/api.enpoints.ts +++ b/src/app/core/services/config-service/api.enpoints.ts @@ -1,5 +1,6 @@ // import { environment } from '../../../../environments/environment'; import { EnumType } from '../../models/data-handle'; +import { ReadStatusNotice } from '../../models/notice.model'; import { FilterOrgs, ParamGetAllBlockOfOrg, @@ -200,12 +201,18 @@ export const API_CONFIG = { data: { membersPage: number; membersSize: number; activeOnly: boolean } ) => `/org/block/${blockId}?membersPage=${data.membersPage}&membersSize=${data.membersSize}&activeOnly=${data.activeOnly}`, + GET_ALL_MY_NOTIFICATIONS: ( + page: number, + size: number, + readStatus: ReadStatusNotice + ) => + `/notification/my?page=${page}&size=${size}&readStatus=${readStatus}`, }, POST: { LOGIN: '/identity/auth/login', REGISTER: '/identity/auth/register', LOGOUT: '/identity/auth/logout', - CREATE_FIRST_PASSWORD: '/identity/auth/user/create-password', + CREATE_FIRST_PASSWORD: '/identity/user/create-password', REQUEST_FORGOT_PASSWORD: '/identity/auth/forgot-password/request', RESET_PASSWORD: `/identity/auth/forgot-password/reset`, REFRESH_TOKEN: '/identity/auth/refresh', @@ -271,6 +278,10 @@ export const API_CONFIG = { BULK_ADD_TO_BLOCK: (blockId: string) => `/org/block/${blockId}/members:bulk`, IMPORT_EXCEL_ADD_MEMBER: '/identity/users/import', + ADD_ADMIN: '/identity/admin', + ADD_STUDENT: '/identity/teacher', + ADD_TEACHER: '/identity/user', + MARK_AS_READ_NOTIFICATION: '/my/mark-read', }, PUT: { EDIT_FILE: (id: string) => `/file/api/FileDocument/edit/${id}`, diff --git a/src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.html b/src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.html new file mode 100644 index 00000000..57f48847 --- /dev/null +++ b/src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.html @@ -0,0 +1,182 @@ + diff --git a/src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.scss b/src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.scss new file mode 100644 index 00000000..ada451e4 --- /dev/null +++ b/src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.scss @@ -0,0 +1,230 @@ +// Biến màu sắc và animation +:host { + --primary-color: #007bff; + --border-color: #dee2e6; + --text-color: #212529; + --background-color: #fff; + --overlay-color: rgba(0, 0, 0, 0.5); + --transition-speed: 0.3s; +} + +// Lớp phủ nền +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: var(--overlay-color); + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + visibility: hidden; + transition: opacity var(--transition-speed) ease, + visibility var(--transition-speed) ease; + z-index: 1000; + + &.active { + opacity: 1; + visibility: visible; + } +} + +// Nội dung modal +.modal-content { + background-color: var(--background-color); + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + width: 90%; + max-width: 600px; + transform: translateY(30px); + opacity: 0; + transition: transform var(--transition-speed) ease, + opacity var(--transition-speed) ease; + display: flex; + flex-direction: column; + + &.active { + transform: translateY(0); + opacity: 1; + } +} + +// Header +.modal-header { + padding: 16px 24px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + + h2 { + margin: 0; + font-size: 1.25rem; + } + + .close-btn { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #6c757d; + &:hover { + color: var(--text-color); + } + } +} + +// Body và Form +.modal-body { + padding: 24px; + overflow-y: auto; + max-height: 70vh; +} + +.steps-container { + overflow: hidden; + position: relative; + min-height: 350px; // Đảm bảo chiều cao không bị giật khi chuyển step +} + +.form-step { + position: absolute; + width: 100%; + opacity: 0; + visibility: hidden; + transform: translateX(100%); + transition: all var(--transition-speed) ease-in-out; + + &.active { + position: relative; + opacity: 1; + visibility: visible; + transform: translateX(0); + } +} + +// Các element trong form +.form-group { + margin-bottom: 1.2rem; + + label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + } + + input, + select, + textarea { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 1rem; + transition: border-color var(--transition-speed) ease, + box-shadow var(--transition-speed) ease; + box-sizing: border-box; + + &:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + } + } +} + +.form-grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +// Dropdown tìm kiếm tổ chức +.org-search-group { + position: relative; +} + +.org-dropdown { + position: absolute; + width: 100%; + background: var(--background-color); + border: 1px solid var(--border-color); + border-radius: 4px; + margin-top: 4px; + max-height: 200px; + overflow-y: auto; + z-index: 1001; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .dropdown-item { + padding: 12px; + cursor: pointer; + &:hover { + background-color: #f8f9fa; + } + &.loading, + &.not-found { + font-style: italic; + color: #6c757d; + cursor: default; + } + } +} + +// Footer và Buttons +.modal-footer { + padding: 16px 24px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.btn { + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: background-color var(--transition-speed) ease; + + &.btn-primary { + background-color: var(--primary-color); + color: white; + &:hover { + background-color: #0056b3; + } + &:disabled { + background-color: #6c757d; + cursor: not-allowed; + } + } + + &.btn-secondary { + background-color: #e9ecef; + color: var(--text-color); + &:hover { + background-color: #d3d9df; + } + } +} + +// Spinner animation +.spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.ts b/src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.ts new file mode 100644 index 00000000..76f8335b --- /dev/null +++ b/src/app/features/admin/user-management/modal/create-user-modal/create-user-modal.component.ts @@ -0,0 +1,230 @@ +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { Subject } from 'rxjs'; +import { + debounceTime, + distinctUntilChanged, + switchMap, + takeUntil, +} from 'rxjs/operators'; +import { OrganizationResponse } from '../../../../../core/models/organization.model'; +import { UserService } from '../../../../../core/services/api-service/user.service'; +import { OrganizationService } from '../../../../../core/services/api-service/organization.service'; +import { CreateAccoutByAdmin } from '../../../../../core/models/user.models'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-create-user-modal', + templateUrl: './create-user-modal.component.html', + styleUrls: ['./create-user-modal.component.scss'], + imports: [CommonModule, FormsModule, ReactiveFormsModule], +}) +export class CreateUserModalComponent implements OnInit, OnDestroy { + // --- Inputs & Outputs --- + @Input() isOpen = false; + @Output() close = new EventEmitter(); + @Output() userCreated = new EventEmitter(); + + // --- State Management --- + currentStep = 1; + isSubmitting = false; + + // --- Organization Search --- + organizationSearch$ = new Subject(); + isSearchingOrgs = false; + searchedOrganizations: OrganizationResponse[] = []; + selectedOrganizationName: string | null = null; + showOrgDropdown = false; + + educationOptions = [ + { value: 0, label: 'Không tiết lộ' }, + { value: 1, label: 'Tiểu học' }, // Lớp 1 - 5 + { value: 2, label: 'Trung học cơ sở' }, // Lớp 6 - 9 + { value: 3, label: 'Trung học phổ thông' }, // Lớp 10 - 12 + { value: 4, label: 'Trung cấp' }, + { value: 5, label: 'Cao đẳng' }, + { value: 6, label: 'Đại học' }, + { value: 7, label: 'Cao học / Thạc sĩ' }, + { value: 8, label: 'Tiến sĩ' }, + { value: 9, label: 'Khác' }, + ]; + + // --- Forms --- + createUserForm: FormGroup; + private destroy$ = new Subject(); + + constructor( + private fb: FormBuilder, + private userService: UserService, // Inject API service + private orgService: OrganizationService + ) { + this.createUserForm = this.fb.group({ + // Step 1 + role: ['STUDENT', Validators.required], + firstName: ['', Validators.required], + lastName: ['', Validators.required], + displayName: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + username: ['', [Validators.required, Validators.minLength(4)]], + password: ['', [Validators.required, Validators.minLength(8)]], + dob: ['', Validators.required], + gender: [true, Validators.required], + + // Step 2 + bio: [''], + education: [0], + links: [''], + city: [''], + organizationId: [''], + organizationMemberRole: ['STUDENT', Validators.required], + }); + } + + ngOnInit(): void { + this.handleOrganizationSearch(); + } + + handleOrganizationSearch(): void { + this.organizationSearch$ + .pipe( + debounceTime(400), + distinctUntilChanged(), + switchMap((query) => { + if (!query || query.length < 2) { + this.searchedOrganizations = []; + this.showOrgDropdown = false; + return []; + } + this.isSearchingOrgs = true; + this.showOrgDropdown = true; + // Gọi API tìm kiếm + return this.orgService.searchOrgsFilter(1, 10, { q: query }); + }), + takeUntil(this.destroy$) + ) + .subscribe((response) => { + this.isSearchingOrgs = false; + this.searchedOrganizations = response.result.data; // Giả sử API trả về cấu trúc này + }); + } + + onOrgSearchInput(event: Event): void { + const query = (event.target as HTMLInputElement).value; + this.organizationSearch$.next(query); + } + + selectOrganization(org: OrganizationResponse): void { + this.selectedOrganizationName = org.name; + this.createUserForm.get('organizationId')?.setValue(org.id); + this.showOrgDropdown = false; + this.searchedOrganizations = []; + } + + // --- Step Navigation --- + nextStep(): void { + if (this.currentStep === 1) { + // Logic validate các trường của step 1 nếu cần + this.currentStep = 2; + } + } + + prevStep(): void { + if (this.currentStep === 2) { + this.currentStep = 1; + } + } + + // --- Actions --- + closeModal(): void { + // this.resetForm(); + this.close.emit(); + } + + onSubmit(): void { + if (this.createUserForm.invalid) { + this.createUserForm.markAllAsTouched(); + return; + } + + this.isSubmitting = true; + + const formValue = this.createUserForm.value; + + // Tách role (chỉ để gọi API) + const role: 'ADMIN' | 'STUDENT' | 'TEACHER' = formValue.role; + + if (formValue.dob && !formValue.dob.endsWith('T00:00:00Z')) { + formValue.dob = formValue.dob + 'T00:00:00Z'; + } + + // Đảm bảo đúng kiểu CreateAccoutByAdmin + const data: CreateAccoutByAdmin = { + username: formValue.username, + email: formValue.email, + password: formValue.password, + firstName: formValue.firstName, + lastName: formValue.lastName, + dob: formValue.dob, + bio: formValue.bio, + gender: formValue.gender === true || formValue.gender === 'true', + displayName: formValue.displayName, + education: Number(formValue.education) || 0, + links: formValue.links.split(','), + city: formValue.city, + organizationId: formValue.organizationId, + organizationMemberRole: formValue.organizationMemberRole, + }; + + this.userService + .createAccountUser(role, data) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + console.log('User created successfully!'); + this.isSubmitting = false; + this.userCreated.emit(); + this.closeModal(); + }, + error: (err) => { + console.error('Failed to create user:', err); + this.isSubmitting = false; + }, + }); + } + + resetForm(): void { + this.createUserForm.reset({ + ...this.createUserForm.value, // giữ lại role + gender: false, + organizationMemberRole: this.createUserForm.value.role || 'STUDENT', + }); + this.currentStep = 1; + this.isSubmitting = false; + this.selectedOrganizationName = null; + this.searchedOrganizations = []; + this.showOrgDropdown = false; + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + // Helper để truy cập controls trong template + get f() { + return this.createUserForm.controls; + } +} diff --git a/src/app/features/admin/user-management/pages/user-list/user-list.html b/src/app/features/admin/user-management/pages/user-list/user-list.html index 73cb3ee2..3c90a8fc 100644 --- a/src/app/features/admin/user-management/pages/user-list/user-list.html +++ b/src/app/features/admin/user-management/pages/user-list/user-list.html @@ -56,7 +56,7 @@ [width]="'50px'" [height]="'50px'" variant="solid" - > + > + > - - - - - - - - - - - - - - - + - - - - - @if (!isLoading) { - - - -
- @if (row.avatarUrl!=null) { - avatar - } - - {{ row.displayName }} - -
-
- - {{ row.dob }} - - - - {{ row.role === 0 ? 'Học sinh' : row.role === 1 ? 'Admin tổ chức' : - row.role === 2 ? 'Giáo viên' : 'Khác' }} - - - - - - {{ row.active === true?'Active' : 'Blocked' }} - - -
- } - @if (isLoading ) { -
- @if (isLoading) { - - } -
- } + /> + + + + + + + + + + + + + + + + + + - - @if (openedUser) { -
+ + +
+ @if (row.avatarUrl!=null) { + avatar + } + -
-
- - -
+ {{ row.displayName }} +
+
+ + {{ row.dob }} + + + + {{ row.role === 0 ? 'Học sinh' : row.role === 1 ? 'Admin tổ chức' : + row.role === 2 ? 'Giáo viên' : 'Khác' }} + + + + + + {{ row.active === true?'Active' : 'Blocked' }} + + + + } @if (isLoading ) { +
+ @if (isLoading) { + } +
+ } +
- + + @if (openedUser) { +
+
+
+ +
+
+ } + + + + + diff --git a/src/app/features/admin/user-management/pages/user-list/user-list.ts b/src/app/features/admin/user-management/pages/user-list/user-list.ts index 336a273f..4e05adc1 100644 --- a/src/app/features/admin/user-management/pages/user-list/user-list.ts +++ b/src/app/features/admin/user-management/pages/user-list/user-list.ts @@ -21,6 +21,7 @@ import { import { EnumType } from '../../../../../core/models/data-handle'; import { UserService } from '../../../../../core/services/api-service/user.service'; import { clearLoading } from '../../../../../shared/store/loading-state/loading.action'; +import { CreateUserModalComponent } from '../../modal/create-user-modal/create-user-modal.component'; @Component({ selector: 'app-user-list', @@ -34,8 +35,9 @@ import { clearLoading } from '../../../../../shared/store/loading-state/loading. InputComponent, ButtonComponent, DropdownButtonComponent, - SkeletonLoadingComponent -], + SkeletonLoadingComponent, + CreateUserModalComponent, + ], standalone: true, }) export class UserListComponent { @@ -43,6 +45,7 @@ export class UserListComponent { headers = userHeaders; sidebarData = sidebarData; + isOpenCreateUser = false; // UI State isCollapsed = false; @@ -61,47 +64,47 @@ export class UserListComponent { // Data listId = 'user-list-2024-06-09'; // hoặc số, hoặc uuid, hoặc lấy từ backend ListUser: SearchUserProfileResponse[] = []; - dataJson = `[ - { - "id": 1, - "displayName": "nguyenvana", - "avatarUrl": "https://randomuser.me/api/portraits/men/1.jpg", - "backgroundUrl": "https://images.unsplash.com/photo-1506744038136-46273834b3fb", - "dob": "2020-01-01T00:00:00.000Z", - "role": 0, - "status": 1, - "org": "Trường Đại học ABC", - "links": [ - { "type": "facebook", "url": "https://facebook.com/nguyenvana" }, - { "type": "github", "url": "https://github.com/nguyenvana" } - ], - "followers": 120000000000000, - "following": 80, - "bio": "Yêu thích lập trình, thích chia sẻ kiến thức.", - "firstname": "Nguyen", - "lastname": "Van A", - "education": "Đại học ABC", - "gender": "Nam" - }, - { - "id": 2, - "displayName": "tranthib", - "avatarUrl": "https://randomuser.me/api/portraits/women/2.jpg", - "backgroundUrl": "https://images.unsplash.com/photo-1465101046530-73398c7f28ca", - "dob": "2020-01-01T00:00:00.000Z", - "role": 2, - "status": 0, - "org": "Trường Đại học XYZ", - "links": [], - "followers": 200, - "following": 150, - "bio": "Giáo viên Toán, đam mê dạy học.", - "firstname": "Tran", - "lastname": "Thi B", - "education": "Đại học XYZ", - "gender": "Nữ" - } - ]`; + + // { + // "id": 1, + // "displayName": "nguyenvana", + // "avatarUrl": "https://randomuser.me/api/portraits/men/1.jpg", + // "backgroundUrl": "https://images.unsplash.com/photo-1506744038136-46273834b3fb", + // "dob": "2020-01-01T00:00:00.000Z", + // "role": 0, + // "status": 1, + // "org": "Trường Đại học ABC", + // "links": [ + // { "type": "facebook", "url": "https://facebook.com/nguyenvana" }, + // { "type": "github", "url": "https://github.com/nguyenvana" } + // ], + // "followers": 120000000000000, + // "following": 80, + // "bio": "Yêu thích lập trình, thích chia sẻ kiến thức.", + // "firstname": "Nguyen", + // "lastname": "Van A", + // "education": "Đại học ABC", + // "gender": "Nam" + // }, + // { + // "id": 2, + // "displayName": "tranthib", + // "avatarUrl": "https://randomuser.me/api/portraits/women/2.jpg", + // "backgroundUrl": "https://images.unsplash.com/photo-1465101046530-73398c7f28ca", + // "dob": "2020-01-01T00:00:00.000Z", + // "role": 2, + // "status": 0, + // "org": "Trường Đại học XYZ", + // "links": [], + // "followers": 200, + // "following": 150, + // "bio": "Giáo viên Toán, đam mê dạy học.", + // "firstname": "Tran", + // "lastname": "Thi B", + // "education": "Đại học XYZ", + // "gender": "Nữ" + // } + // ]`; // Pagination pageIndex: number = 1; @@ -139,6 +142,11 @@ export class UserListComponent { this.fetchDataListUser(); } + reloadData() { + this.pageIndex = 1; + this.fetchDataListUser(); + } + // Data fetch fetchDataListUser() { this.isLoading = true; @@ -180,7 +188,7 @@ export class UserListComponent { }; handleAdd = () => { - console.log('Add button clicked, listId:', this.listId); + this.isOpenCreateUser = !this.isOpenCreateUser; }; handleInputChange(value: string | number): void { diff --git a/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.ts b/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.ts index 8aec9a3c..02579931 100644 --- a/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.ts +++ b/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.ts @@ -3,6 +3,8 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AuthService } from '../../../../../core/services/api-service/auth.service'; +import { sendNotification } from '../../../../../shared/utils/notification'; +import { Store } from '@ngrx/store'; @Component({ selector: 'app-set-password-modal', @@ -20,7 +22,7 @@ export class SetPasswordModalComponent { isLoading = false; errorMessage = ''; - constructor(private authService: AuthService) {} + constructor(private authService: AuthService, private store: Store) {} close() { this.closed.emit(); @@ -55,6 +57,12 @@ export class SetPasswordModalComponent { this.isLoading = false; this.close(); // Có thể bắn event success hoặc toast ở đây + sendNotification( + this.store, + 'Thành công', + 'Đã đặt mật khẩu', + 'success' + ); }, error: () => { this.isLoading = false; diff --git a/src/app/features/excercise/exercise-pages/exercise-code-details/exercise-code-details.component.ts b/src/app/features/excercise/exercise-pages/exercise-code-details/exercise-code-details.component.ts index cc63a0bb..0d497fa1 100644 --- a/src/app/features/excercise/exercise-pages/exercise-code-details/exercise-code-details.component.ts +++ b/src/app/features/excercise/exercise-pages/exercise-code-details/exercise-code-details.component.ts @@ -141,7 +141,7 @@ export class ExerciseCodeDetailsComponent { referenceId: uuidv4(), currency: 'VNĐ', itemId: this.exerciseId, - itemType: this.exercise?.exerciseType, + itemType: 'EXERCISE', itemPrice: this.exercise?.cost, itemName: this.exercise?.title, }; diff --git a/src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.ts b/src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.ts index f8a85aa6..b249200a 100644 --- a/src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.ts +++ b/src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.ts @@ -189,7 +189,7 @@ export class ExerciseDetailsComponent implements OnInit { referenceId: uuidv4(), currency: 'VNĐ', itemId: this.exerciseId, - itemType: this.exercise.exerciseType, + itemType: 'EXERCISE', itemPrice: this.exercise.cost, itemName: this.exercise.title, }; diff --git a/src/app/features/post/pages/saved-posts-list/saved-posts-list.component.ts b/src/app/features/post/pages/saved-posts-list/saved-posts-list.component.ts index f2eced29..f623d34a 100644 --- a/src/app/features/post/pages/saved-posts-list/saved-posts-list.component.ts +++ b/src/app/features/post/pages/saved-posts-list/saved-posts-list.component.ts @@ -16,8 +16,8 @@ import { LottieComponent } from 'ngx-lottie'; }) export class SavedPostsListComponent implements OnInit { savedPosts: SavedPostResponse[] = []; - currentPage = 0; - totalPages = 0; + currentPage = 1; + totalPages = 1; pageSize = 10; loading = false; @@ -30,10 +30,7 @@ export class SavedPostsListComponent implements OnInit { } loadSavedPosts(): void { - if ( - this.loading || - (this.totalPages && this.currentPage >= this.totalPages) - ) + if (this.loading || (this.totalPages && this.currentPage > this.totalPages)) return; this.loading = true; diff --git a/src/app/features/service-payment/pages/qr-payment/qr-payment.component.html b/src/app/features/service-payment/pages/qr-payment/qr-payment.component.html index bf5db04f..c06d2a87 100644 --- a/src/app/features/service-payment/pages/qr-payment/qr-payment.component.html +++ b/src/app/features/service-payment/pages/qr-payment/qr-payment.component.html @@ -5,92 +5,85 @@

Nạp điểm tiêu dùng

-
- -
VNĐ
-
- @if (amountInWords) { -
- {{ amountInWords }} -
- } -
+ > +
+ +
VNĐ
+
+ @if (amountInWords) { +
+ {{ amountInWords }} +
+ } + - + -
- Lưu ý: Không được thay đổi nội dung và hạn mức tiền tự động của giao dịch - bằng QR này. Chúng tôi sẽ không chịu trách nhiệm nếu bạn thay đổi nó khi - chuyển khoản. -
- +
+ Lưu ý: Không được thay đổi nội dung và hạn mức tiền tự động của giao dịch + bằng QR này. Chúng tôi sẽ không chịu trách nhiệm nếu bạn thay đổi nó khi + chuyển khoản. +
+ - -
-
- @if (qrUrl) { - QR Code - } - @if (!qrUrl) { - QR Code - } - - @if (!transactionSuccess && qrUrl !== qrUrlPlaceHolder) { -

- Giao dịch hết hạn sau: {{ formatTime(countdown) }}s -

- } + +
+
+ @if (qrUrl) { + QR Code + } @if (!qrUrl) { + QR Code + } + + @if (!transactionSuccess && qrUrl !== qrUrlPlaceHolder) { +

+ Giao dịch hết hạn sau: {{ formatTime(countdown) }}s +

+ } +
+
+

Thông tin thanh toán

+ {{ + transactionSuccess ? "(✔Thanh toán thành công)" : "(❌Chưa thanh toán)" + }} +
+
+ + {{ matchedTransaction?.['Mã GD'] || '--' }} +
+
+ + {{ matchedTransaction?.['Mã tham chiếu'] || '--' }} +
+
+ + {{ amount || 0 | formatView }} VNĐ +
+
+ + Nạp tiền
-
-

Thông tin thanh toán

- {{ - transactionSuccess ? "(✔Thanh toán thành công)" : "(❌Chưa thanh toán)" - }} -
-
- - {{ matchedTransaction?.['Mã GD'] || '--' }} -
-
- - {{ matchedTransaction?.['Mã tham chiếu'] || '--' }} -
-
- - {{ amount || 0 | formatView }} VNĐ -
-
- - Nạp tiền -
-
- - - {{ currentGp | formatView }} VNĐ -
-
+
+ + + {{ currentGp | formatView }} Điểm
+
+
diff --git a/src/app/shared/components/my-shared/header/header.html b/src/app/shared/components/my-shared/header/header.html index 0c8b56c2..f016dd35 100644 --- a/src/app/shared/components/my-shared/header/header.html +++ b/src/app/shared/components/my-shared/header/header.html @@ -7,86 +7,77 @@ class="header-logo-icon" src="landing-assets/Logo.svg" alt="logo" - /> -
-

CodeCampus

- Học tập và kết nối cộng đồng -
- -
- + /> +
+

CodeCampus

+ Học tập và kết nối cộng đồng +
+
+ - @if (!isLoggedIn) { -
- - Đăng nhập - Tạo tài khoản -
- } - @if (isLoggedIn) { -
-
- - -
- -
-
-
-
-
-
- avatar -
-
- } - + @if (!isLoggedIn) { +
+ + Đăng nhập + Tạo tài khoản +
+ } @if (isLoggedIn) { +
+
+ + +
+ +
+
+
- @if (showProfileMenu) { - - } +
+ avatar +
+
+ } + + + +@if (showProfileMenu) { + +} + + - + + diff --git a/src/app/shared/components/my-shared/header/header.ts b/src/app/shared/components/my-shared/header/header.ts index 48e5b58b..415f4515 100644 --- a/src/app/shared/components/my-shared/header/header.ts +++ b/src/app/shared/components/my-shared/header/header.ts @@ -12,6 +12,7 @@ import { selectVariable } from '../../../store/variable-state/variable.selectors import { resetVariable } from '../../../store/variable-state/variable.actions'; import { avatarUrlDefault } from '../../../../core/constants/value.constant'; import { SetPasswordModalComponent } from '../../../../features/auth/components/modal/set-password-modal/set-password-modal.component'; +import { NotificationModalComponent } from './notification-modal/notification-modal.component'; @Component({ selector: 'app-header', @@ -21,8 +22,9 @@ import { SetPasswordModalComponent } from '../../../../features/auth/components/ imports: [ ProfileMenuComponent, ToggleSwitch, - SetPasswordModalComponent -], + SetPasswordModalComponent, + NotificationModalComponent, + ], }) export class HeaderComponent { needReloadAvatar$: Observable; @@ -37,6 +39,7 @@ export class HeaderComponent { avatarDefault = avatarUrlDefault; setPassword = false; needCreateNewPass = false; + showNotificationModal = false; constructor( private router: Router, @@ -89,6 +92,10 @@ export class HeaderComponent { console.log(this.selectedOptions); } + toggleNotification() { + this.showNotificationModal = !this.showNotificationModal; + } + goToLogin() { this.router.navigate(['/auth/identity/login']); } diff --git a/src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.html b/src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.html new file mode 100644 index 00000000..f0cd1933 --- /dev/null +++ b/src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.html @@ -0,0 +1,35 @@ + + + diff --git a/src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.scss b/src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.scss new file mode 100644 index 00000000..be000a7c --- /dev/null +++ b/src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.scss @@ -0,0 +1,122 @@ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 1000; +} + +.modal-container { + position: fixed; + top: 60px; + right: 20px; + width: 400px; + max-height: 80vh; + background: #fff; + border-radius: 12px; + overflow: hidden; + z-index: 1001; + display: flex; + flex-direction: column; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid #eee; + background: #f9f9f9; + + h3 { + margin: 0; + font-size: 16px; + font-weight: bold; + } + + .mark-all { + font-size: 12px; + background: transparent; + border: none; + cursor: pointer; + color: #007bff; + } + + .close-btn { + cursor: pointer; + font-size: 18px; + margin-left: 8px; + } +} + +.modal-body { + padding: 12px; + overflow-y: auto; + flex: 1; + + .loading, + .empty { + text-align: center; + padding: 20px; + color: #777; + } + + .notification-list { + list-style: none; + margin: 0; + padding: 0; + + li { + padding: 10px; + border-bottom: 1px solid #eee; + cursor: pointer; + transition: background 0.2s ease; + + &.unread { + background: #eef6ff; + font-weight: 500; + } + + &:hover { + background: #f7f7f7; + } + + h4 { + margin: 0 0 4px; + font-size: 14px; + } + + p { + margin: 0 0 4px; + font-size: 13px; + color: #444; + } + + small { + font-size: 12px; + color: #888; + } + } + } + + .load-more { + text-align: center; + margin-top: 10px; + + button { + padding: 6px 12px; + font-size: 13px; + border: 1px solid #007bff; + background: #fff; + color: #007bff; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #007bff; + color: #fff; + } + } + } +} diff --git a/src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.ts b/src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.ts new file mode 100644 index 00000000..42022a52 --- /dev/null +++ b/src/app/shared/components/my-shared/header/notification-modal/notification-modal.component.ts @@ -0,0 +1,98 @@ +// notification-modal.component.ts +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { GetAllNoticeResponse } from '../../../../../core/models/notice.model'; +import { NotificationListService } from '../../../../../core/services/api-service/notification-list.service'; +import { animate, style, transition, trigger } from '@angular/animations'; + +@Component({ + selector: 'app-notification-modal', + standalone: true, + imports: [CommonModule], + templateUrl: './notification-modal.component.html', + styleUrls: ['./notification-modal.component.scss'], + animations: [ + trigger('fadeSlide', [ + transition(':enter', [ + style({ opacity: 0, transform: 'translateY(-20px)' }), + animate( + '250ms ease-out', + style({ opacity: 1, transform: 'translateY(0)' }) + ), + ]), + transition(':leave', [ + animate( + '200ms ease-in', + style({ opacity: 0, transform: 'translateY(-20px)' }) + ), + ]), + ]), + ], +}) +export class NotificationModalComponent { + @Input() isOpen = false; + @Output() closed = new EventEmitter(); + + notifications: GetAllNoticeResponse[] = []; + isLoading = false; + page = 1; + size = 10; + totalPages = 1; + + constructor(private notificationService: NotificationListService) {} + + ngOnInit() { + if (this.isOpen) { + this.loadNotifications(); + } + } + + ngOnChanges() { + if (this.isOpen) { + this.loadNotifications(true); + } + } + + loadNotifications(reset = false) { + if (reset) { + this.page = 1; + this.notifications = []; + } + this.isLoading = true; + this.notificationService + .getAllMyNotification(this.page, this.size, 'ALL') + .subscribe({ + next: (res) => { + this.notifications.push(...res.result.data); + this.totalPages = res.result.totalPages; + this.isLoading = false; + }, + error: () => (this.isLoading = false), + }); + } + + markAsRead(id: string) { + this.notificationService.markAsReadNotification([id]).subscribe(() => { + const item = this.notifications.find((n) => n.id === id); + if (item) item.readStatus = 'READ'; + }); + } + + markAllAsRead() { + const ids = this.notifications.map((n) => n.id); + this.notificationService.markAsReadNotification(ids).subscribe(() => { + this.notifications.forEach((n) => (n.readStatus = 'READ')); + }); + } + + loadMore() { + if (this.page < this.totalPages) { + this.page++; + this.loadNotifications(); + } + } + + closeModal() { + this.closed.emit(); + } +}