From 4e02d3197ce42cdabf438caae50a33aa4f547a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B4=20Quang=20=C4=90=E1=BB=A9c?= Date: Tue, 2 Sep 2025 00:41:32 +0700 Subject: [PATCH] fix AI layout --- .../handle/error-handler.service.ts | 1 + src/app/core/models/api-response.ts | 1 + src/app/core/models/code.model.ts | 1 + src/app/core/models/exercise.model.ts | 2 + .../core/services/api-service/auth.service.ts | 7 ++ .../services/config-service/api.enpoints.ts | 1 + .../set-password-modal.component.html | 41 ++++++ .../set-password-modal.component.scss | 118 ++++++++++++++++++ .../set-password-modal.component.ts | 64 ++++++++++ src/app/features/auth/pages/login/login.ts | 1 + .../pages/oauth-callback/oauth-callback.ts | 20 ++- .../exercise-code-details.component.ts | 1 + .../exercise-details.component.ts | 1 + .../list-exercise/list-exercise.component.ts | 4 +- .../transaction-history.component.ts | 2 +- .../app-layout/app-layout.component.scss | 6 + .../components/my-shared/header/header.html | 7 ++ .../components/my-shared/header/header.ts | 18 ++- .../header/profile-menu.component.html | 89 +++---------- .../header/profile-menu.component.ts | 15 ++- .../variable-state/variable.selectors.ts | 5 + src/app/shared/utils/mapData.ts | 1 + 22 files changed, 324 insertions(+), 82 deletions(-) create mode 100644 src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.html create mode 100644 src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.scss create mode 100644 src/app/features/auth/components/modal/set-password-modal/set-password-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 a5a9bd4d..6b4f0607 100644 --- a/src/app/core/interceptors/handle/error-handler.service.ts +++ b/src/app/core/interceptors/handle/error-handler.service.ts @@ -26,6 +26,7 @@ export class ErrorHandlerService { cancelText: 'Hủy', onConfirm: () => { this.router.navigate(['/auth/identity/login']); + localStorage.removeItem('token'); }, onCancel: () => { sendNotification(this.store, errorStatus, errorMessage, 'error'); diff --git a/src/app/core/models/api-response.ts b/src/app/core/models/api-response.ts index 50d966ad..96ee52c4 100644 --- a/src/app/core/models/api-response.ts +++ b/src/app/core/models/api-response.ts @@ -25,6 +25,7 @@ export type loginResponse = { authenticated: boolean; enabled: boolean; active: boolean; + needPasswordSetup: boolean; }; export interface IPaginationResponse { diff --git a/src/app/core/models/code.model.ts b/src/app/core/models/code.model.ts index 1516be19..414eaf85 100644 --- a/src/app/core/models/code.model.ts +++ b/src/app/core/models/code.model.ts @@ -36,6 +36,7 @@ export type ExerciseCodeResponse = { endTime: string; // ISO datetime duration: number; // minutes allowDiscussionId: string; + purchased: boolean; resourceIds: string[]; tags: string[]; allowAiQuestion: boolean; diff --git a/src/app/core/models/exercise.model.ts b/src/app/core/models/exercise.model.ts index 4472d4b2..2bc6a95c 100644 --- a/src/app/core/models/exercise.model.ts +++ b/src/app/core/models/exercise.model.ts @@ -23,6 +23,7 @@ export type ExerciseItem = { freeForOrg: boolean; tags: Set; createdAt: string; + purchased: boolean; }; export type UserBasicInfo = { @@ -101,6 +102,7 @@ export interface ExerciseQuiz { endTime: string; duration: number; allowDiscussionId: string; + purchased: boolean; resourceIds: string[]; tags: string[]; allowAiQuestion: boolean; diff --git a/src/app/core/services/api-service/auth.service.ts b/src/app/core/services/api-service/auth.service.ts index 692decff..dd120bf3 100644 --- a/src/app/core/services/api-service/auth.service.ts +++ b/src/app/core/services/api-service/auth.service.ts @@ -31,6 +31,13 @@ export class AuthService { ); } + createInitialPassword(password: string) { + return this.api.post>( + API_CONFIG.ENDPOINTS.POST.CREATE_FIRST_PASSWORD, + { password } + ); + } + requestForgotPassword(email: string) { return this.api.post>( API_CONFIG.ENDPOINTS.POST.REQUEST_FORGOT_PASSWORD, diff --git a/src/app/core/services/config-service/api.enpoints.ts b/src/app/core/services/config-service/api.enpoints.ts index e303ac63..7c0bca24 100644 --- a/src/app/core/services/config-service/api.enpoints.ts +++ b/src/app/core/services/config-service/api.enpoints.ts @@ -148,6 +148,7 @@ export const API_CONFIG = { LOGIN: '/identity/auth/login', REGISTER: '/identity/auth/register', LOGOUT: '/identity/auth/logout', + CREATE_FIRST_PASSWORD: '/identity/auth/user/create-password', REQUEST_FORGOT_PASSWORD: '/identity/auth/forgot-password/request', RESET_PASSWORD: `/identity/auth/forgot-password/reset`, REFRESH_TOKEN: '/identity/auth/refresh', diff --git a/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.html b/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.html new file mode 100644 index 00000000..a603f4c7 --- /dev/null +++ b/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.html @@ -0,0 +1,41 @@ + + + + diff --git a/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.scss b/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.scss new file mode 100644 index 00000000..2de781dc --- /dev/null +++ b/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.scss @@ -0,0 +1,118 @@ +/* set-password-modal.component.scss */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + animation: fadeIn 0.3s ease forwards; + z-index: 1000; +} + +.modal-wrapper { + position: fixed; + inset: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background: #fff; + border-radius: 12px; + padding: 24px; + width: 400px; + max-width: 90%; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + animation: slideIn 0.3s ease forwards; +} + +h2 { + margin-bottom: 16px; + font-size: 20px; + text-align: center; +} + +.form-group { + margin-bottom: 16px; + display: flex; + flex-direction: column; + + label { + font-size: 14px; + margin-bottom: 6px; + } + + input { + padding: 10px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + + &:focus { + outline: none; + border-color: #3f51b5; + box-shadow: 0 0 0 2px rgba(63, 81, 181, 0.2); + } + } +} + +.error-message { + color: #d32f2f; + font-size: 13px; + margin-bottom: 10px; +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 10px; + + .btn { + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + border: none; + font-size: 14px; + transition: all 0.2s ease; + + &.cancel { + background: #e0e0e0; + &:hover { + background: #d5d5d5; + } + } + + &.confirm { + background: #3f51b5; + color: white; + &:hover { + background: #303f9f; + } + &:disabled { + background: #9fa8da; + cursor: not-allowed; + } + } + } +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} 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 new file mode 100644 index 00000000..29bd4886 --- /dev/null +++ b/src/app/features/auth/components/modal/set-password-modal/set-password-modal.component.ts @@ -0,0 +1,64 @@ +// set-password-modal.component.ts +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { AuthService } from '../../../../../core/services/api-service/auth.service'; + +@Component({ + selector: 'app-set-password-modal', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './set-password-modal.component.html', + styleUrls: ['./set-password-modal.component.scss'], +}) +export class SetPasswordModalComponent { + @Input() isOpen = false; + @Output() closed = new EventEmitter(); + + password = ''; + confirmPassword = ''; + isLoading = false; + errorMessage = ''; + + constructor(private authService: AuthService) {} + + close() { + this.closed.emit(); + this.resetForm(); + this.isOpen = !this.isOpen; + } + + resetForm() { + this.password = ''; + this.confirmPassword = ''; + this.errorMessage = ''; + this.isLoading = false; + } + + onSubmit() { + if (!this.password || !this.confirmPassword) { + this.errorMessage = 'Vui lòng nhập đầy đủ thông tin'; + return; + } + if (this.password.length < 6) { + this.errorMessage = 'Mật khẩu phải có ít nhất 6 ký tự'; + return; + } + if (this.password !== this.confirmPassword) { + this.errorMessage = 'Mật khẩu nhập lại không khớp'; + return; + } + + this.isLoading = true; + this.authService.createInitialPassword(this.password).subscribe({ + next: () => { + this.isLoading = false; + this.close(); + // Có thể bắn event success hoặc toast ở đây + }, + error: () => { + this.isLoading = false; + }, + }); + } +} diff --git a/src/app/features/auth/pages/login/login.ts b/src/app/features/auth/pages/login/login.ts index 97aa35ea..e5b33bdd 100644 --- a/src/app/features/auth/pages/login/login.ts +++ b/src/app/features/auth/pages/login/login.ts @@ -70,6 +70,7 @@ export class Login { authenticated: false, enabled: false, active: false, + needPasswordSetup: false, }; constructor( diff --git a/src/app/features/auth/pages/oauth-callback/oauth-callback.ts b/src/app/features/auth/pages/oauth-callback/oauth-callback.ts index 6e7d9763..0a21a7e5 100644 --- a/src/app/features/auth/pages/oauth-callback/oauth-callback.ts +++ b/src/app/features/auth/pages/oauth-callback/oauth-callback.ts @@ -4,7 +4,11 @@ import { Router, ActivatedRoute } from '@angular/router'; import { AuthService } from '../../../../core/services/api-service/auth.service'; import { LoadingOverlayComponent } from '../../../../shared/components/fxdonad-shared/loading-overlay/loading-overlay.component'; import { Store } from '@ngrx/store'; -import { sendNotification } from '../../../../shared/utils/notification'; +import { + openModalNotification, + sendNotification, +} from '../../../../shared/utils/notification'; +import { setVariable } from '../../../../shared/store/variable-state/variable.actions'; @Component({ selector: 'app-oauth-callback', @@ -38,6 +42,20 @@ export class OauthCallbackComponent { if (res.code === 20000) { this.router.navigate(['/exercise/exercise-layout/list']); localStorage.setItem('token', res.result.accessToken); + localStorage.setItem('refreshToken', res.result.refreshToken); + + if (!res.result.needPasswordSetup) { + openModalNotification( + this.store, + 'Cảnh báo', + 'Tài khoản của bạn chưa có mật khẩu, hãy cài đặt nó sớm nhất có thể để tránh rủi ro.', + 'Đồng ý', + 'hủy' + ); + this.store.dispatch( + setVariable({ key: 'needPasswordSetup', value: true }) + ); + } } else { this.router.navigate(['/auth/identity/login']); } 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 b47d6f54..52cb1944 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 @@ -57,6 +57,7 @@ export class ExerciseCodeDetailsComponent { active: false, cost: 0, freeForOrg: false, + purchased: false, visibility: false, startTime: '', endTime: '', 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 3714daab..7da48952 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 @@ -123,6 +123,7 @@ export class ExerciseDetailsComponent implements OnInit { freeForOrg: false, visibility: false, startTime: '', + purchased: false, endTime: '', duration: 0, allowDiscussionId: 'chưa có', diff --git a/src/app/features/excercise/exercise-pages/list-exercise/list-exercise.component.ts b/src/app/features/excercise/exercise-pages/list-exercise/list-exercise.component.ts index fc118add..fae0a9a1 100644 --- a/src/app/features/excercise/exercise-pages/list-exercise/list-exercise.component.ts +++ b/src/app/features/excercise/exercise-pages/list-exercise/list-exercise.component.ts @@ -238,7 +238,9 @@ export class ListExerciseComponent implements OnInit { next: (res) => { this.pageIndex = 1; this.searchData = ''; - this.fetchData(); + setTimeout(() => { + this.fetchData(); + }, 2000); this.showModalCreate = false; sendNotification(this.store, 'Thành công', res.message, 'success'); this.store.dispatch(clearLoading()); diff --git a/src/app/features/service-payment/pages/transaction-history/transaction-history.component.ts b/src/app/features/service-payment/pages/transaction-history/transaction-history.component.ts index f2328f4d..edde2b51 100644 --- a/src/app/features/service-payment/pages/transaction-history/transaction-history.component.ts +++ b/src/app/features/service-payment/pages/transaction-history/transaction-history.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs'; +import { BehaviorSubject, Observable, of } from 'rxjs'; import { switchMap, map, startWith, catchError } from 'rxjs/operators'; import { ApiResponse, diff --git a/src/app/layouts/layout-pages/app-layout/app-layout.component.scss b/src/app/layouts/layout-pages/app-layout/app-layout.component.scss index 9d959db5..e1149f7f 100644 --- a/src/app/layouts/layout-pages/app-layout/app-layout.component.scss +++ b/src/app/layouts/layout-pages/app-layout/app-layout.component.scss @@ -26,6 +26,12 @@ display: flex; justify-content: end; z-index: 3; + // Bỏ sự kiện cho toàn container + pointer-events: none; + + .chat-box { + pointer-events: auto; // Chỉ phần này thao tác được + } } } diff --git a/src/app/shared/components/my-shared/header/header.html b/src/app/shared/components/my-shared/header/header.html index c02a0004..a944e7a6 100644 --- a/src/app/shared/components/my-shared/header/header.html +++ b/src/app/shared/components/my-shared/header/header.html @@ -77,5 +77,12 @@ + + diff --git a/src/app/shared/components/my-shared/header/header.ts b/src/app/shared/components/my-shared/header/header.ts index a415d73b..1daaabfd 100644 --- a/src/app/shared/components/my-shared/header/header.ts +++ b/src/app/shared/components/my-shared/header/header.ts @@ -3,23 +3,27 @@ import { NgIf } from '@angular/common'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { ProfileMenuComponent } from './profile-menu.component'; - import { decodeJWT } from '../../../utils/stringProcess'; - import { ToggleSwitch } from '../../fxdonad-shared/toggle-switch/toggle-switch'; import { ThemeService } from '../../../../styles/theme-service/theme.service'; -import { ProvinceService } from '../../../../core/services/api-service/province.service'; import { ProfileService } from '../../../../core/services/api-service/profile.service'; import { Observable } from 'rxjs'; 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'; + @Component({ selector: 'app-header', templateUrl: './header.html', styleUrls: ['./header.scss'], standalone: true, - imports: [NgIf, ProfileMenuComponent, ToggleSwitch], + imports: [ + NgIf, + ProfileMenuComponent, + ToggleSwitch, + SetPasswordModalComponent, + ], }) export class HeaderComponent { needReloadAvatar$: Observable; @@ -33,6 +37,8 @@ export class HeaderComponent { role: string = ''; avatarUrl: string = ''; avatarDefault = avatarUrlDefault; + setPassword = false; + needCreateNewPass = false; constructor( private router: Router, @@ -136,4 +142,8 @@ export class HeaderComponent { this.showProfileMenu = false; }, 300); } + + openSetPassword($event: boolean) { + this.setPassword = $event; + } } diff --git a/src/app/shared/components/my-shared/header/profile-menu.component.html b/src/app/shared/components/my-shared/header/profile-menu.component.html index 525efe7b..18596316 100644 --- a/src/app/shared/components/my-shared/header/profile-menu.component.html +++ b/src/app/shared/components/my-shared/header/profile-menu.component.html @@ -3,90 +3,35 @@ [class.visible]="isVisible" [class.fadeout]="!isVisible" > - - + diff --git a/src/app/shared/components/my-shared/header/profile-menu.component.ts b/src/app/shared/components/my-shared/header/profile-menu.component.ts index 65d6c5ab..b3aa1f31 100644 --- a/src/app/shared/components/my-shared/header/profile-menu.component.ts +++ b/src/app/shared/components/my-shared/header/profile-menu.component.ts @@ -7,7 +7,6 @@ import { HostListener, } from '@angular/core'; import { AuthService } from '../../../../core/services/api-service/auth.service'; -import { sendNotification } from '../../../utils/notification'; import { Store } from '@ngrx/store'; import { Router } from '@angular/router'; import { @@ -15,22 +14,28 @@ import { setLoading, } from '../../../store/loading-state/loading.action'; import { ChangePasswordComponent } from '../../../../features/auth/components/modal/change-password/change-password.component'; +import { Observable } from 'rxjs'; +import { selectVariable } from '../../../store/variable-state/variable.selectors'; +import { CommonModule } from '@angular/common'; +import { setVariable } from '../../../store/variable-state/variable.actions'; @Component({ selector: 'app-profile-menu', templateUrl: './profile-menu.component.html', styleUrls: ['./profile-menu.component.scss'], standalone: true, - imports: [ChangePasswordComponent], + imports: [ChangePasswordComponent, CommonModule], }) export class ProfileMenuComponent { @Input() isVisible: boolean = false; + @Input() needSetNewPass: boolean = false; @Output() closeMenu = new EventEmitter(); + @Output() openSetPassword = new EventEmitter(); status = ''; loading = false; - isOpenChangePassword = false; + isPasswordModalOpen: boolean = false; constructor( private elementRef: ElementRef, @@ -59,6 +64,10 @@ export class ProfileMenuComponent { this.isVisible = false; } + openSetPasswordModal() { + this.openSetPassword.emit(true); + } + onCloseModal($event: boolean) { this.isOpenChangePassword = $event; this.closeMenu.emit(); diff --git a/src/app/shared/store/variable-state/variable.selectors.ts b/src/app/shared/store/variable-state/variable.selectors.ts index b5a7f280..34eaee05 100644 --- a/src/app/shared/store/variable-state/variable.selectors.ts +++ b/src/app/shared/store/variable-state/variable.selectors.ts @@ -7,3 +7,8 @@ export const selectAppState = createFeatureSelector('variable'); // Selector generic: lấy giá trị theo key export const selectVariable = (key: string) => createSelector(selectAppState, (state: VariableState) => state[key]); + +// export const selectVariable = (key: string) => +// createSelector(selectAppState, (state: VariableState) => +// state[key] !== undefined ? state[key] : false +// ); diff --git a/src/app/shared/utils/mapData.ts b/src/app/shared/utils/mapData.ts index 30b1edca..d3f0898d 100644 --- a/src/app/shared/utils/mapData.ts +++ b/src/app/shared/utils/mapData.ts @@ -147,6 +147,7 @@ export function mapToExerciseQuiz(ex: ExerciseCodeResponse): ExerciseQuiz { cost: ex.cost, freeForOrg: ex.freeForOrg, visibility: ex.visibility, + purchased: ex.purchased, startTime: ex.startTime, endTime: ex.endTime, duration: ex.duration,