From 61ca103666ea0580a8d4076f2c160434cdedc5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B4=20Quang=20=C4=90=E1=BB=A9c?= Date: Sun, 7 Sep 2025 15:47:06 +0700 Subject: [PATCH 1/3] fix some flows org --- src/app/app.routes.ts | 2 - src/app/core/models/comment.models.ts | 1 + src/app/core/models/socket-data.model.ts | 8 + .../core/router-manager/horizontal-menu.ts | 6 +- .../vetical-menu-dynamic/org-vertical-menu.ts | 15 + .../api-service/notification-list.service.ts | 6 + .../services/config-service/api.enpoints.ts | 1 + .../port-socket.ts | 1 + .../socketConnection.service.ts | 0 .../notification-socket.service.ts | 29 ++ .../pages/chat/chat.component.ts | 4 +- .../update-exercise.component.html | 274 +++++++++--------- .../exercise-details.component.html | 6 +- .../post/pages/post-list/post-list.ts | 18 +- .../comment/comment.component.html | 124 ++++---- .../components/my-shared/header/header.html | 6 +- .../components/my-shared/header/header.scss | 2 +- .../components/my-shared/header/header.ts | 39 ++- src/app/shared/utils/mapData.ts | 2 +- 19 files changed, 316 insertions(+), 228 deletions(-) create mode 100644 src/app/core/models/socket-data.model.ts rename src/app/core/services/{socket-service => config-socket}/port-socket.ts (65%) rename src/app/core/services/{socket-service => config-socket}/socketConnection.service.ts (100%) create mode 100644 src/app/core/services/socket-service/notification-socket.service.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 21670258..35b64a26 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -144,8 +144,6 @@ export const routes: Routes = [ import('./features/organization/organization.module').then( (m) => m.OrganizationModule ), - data: { roles: ['ADMIN'] }, - canActivate: [RoleGuard], }, ], }, diff --git a/src/app/core/models/comment.models.ts b/src/app/core/models/comment.models.ts index 1bc5fe5b..c32c0005 100644 --- a/src/app/core/models/comment.models.ts +++ b/src/app/core/models/comment.models.ts @@ -32,6 +32,7 @@ export interface CommentResponse { content: string; replies?: CommentResponse[] | []; // đệ quy user: User; + createdAt: string; } export interface AddCommentResponse { diff --git a/src/app/core/models/socket-data.model.ts b/src/app/core/models/socket-data.model.ts new file mode 100644 index 00000000..b05034f5 --- /dev/null +++ b/src/app/core/models/socket-data.model.ts @@ -0,0 +1,8 @@ +export interface NotificationEvent { + channel: string; + recipient: string; + templateCode: string; + param: Record; + subject: string; + body: string; +} diff --git a/src/app/core/router-manager/horizontal-menu.ts b/src/app/core/router-manager/horizontal-menu.ts index c0a792d4..c589483c 100644 --- a/src/app/core/router-manager/horizontal-menu.ts +++ b/src/app/core/router-manager/horizontal-menu.ts @@ -53,10 +53,12 @@ export function getNavHorizontalItems(roles: string[]): SidebarItem[] { }, { id: 'organization ', - path: '/organization/orgs-list', + path: roles.includes('ADMIN') + ? '/organization/orgs-list' + : '/organization/org-list-post', label: 'Tổ chức', icon: 'fa-solid fa-building-user', - isVisible: !roles.includes(auth_lv2[0]), + // isVisible: !roles.includes(auth_lv2[0]), }, ]; } diff --git a/src/app/core/router-manager/vetical-menu-dynamic/org-vertical-menu.ts b/src/app/core/router-manager/vetical-menu-dynamic/org-vertical-menu.ts index a00787b2..2fb4fb60 100644 --- a/src/app/core/router-manager/vetical-menu-dynamic/org-vertical-menu.ts +++ b/src/app/core/router-manager/vetical-menu-dynamic/org-vertical-menu.ts @@ -1,12 +1,27 @@ import { SidebarItem } from '../../models/data-handle'; export function sidebarOrgRouter(roles: string[]): SidebarItem[] { + const auth_lv2 = ['ADMIN']; + return [ { id: 'list-orgs', path: '/organization/orgs-list', label: 'Danh sách tổ chức', icon: 'fa-solid fa-tasks', + isVisible: !roles.includes(auth_lv2[0]), + }, + { + id: 'list-posts-org', + path: '/organization/org-list-post', + label: 'Danh sách bài viết', + icon: 'fa-solid fa-newspaper', + }, + { + id: 'list-exercise-org', + path: '/organization/org-list-exercise', + label: 'Danh sách bài tập', + icon: 'fa-solid fa-book', }, ]; } diff --git a/src/app/core/services/api-service/notification-list.service.ts b/src/app/core/services/api-service/notification-list.service.ts index 758cbbb6..84fd8dda 100644 --- a/src/app/core/services/api-service/notification-list.service.ts +++ b/src/app/core/services/api-service/notification-list.service.ts @@ -31,4 +31,10 @@ export class NotificationListService { Ids ); } + + countMyUnread() { + return this.api.get>( + API_CONFIG.ENDPOINTS.GET.GET_COUNT_MY_UNREAD + ); + } } diff --git a/src/app/core/services/config-service/api.enpoints.ts b/src/app/core/services/config-service/api.enpoints.ts index 1aff9d7b..a99a04bd 100644 --- a/src/app/core/services/config-service/api.enpoints.ts +++ b/src/app/core/services/config-service/api.enpoints.ts @@ -207,6 +207,7 @@ export const API_CONFIG = { readStatus: ReadStatusNotice ) => `/notification/my?page=${page}&size=${size}&readStatus=${readStatus}`, + GET_COUNT_MY_UNREAD: '/notification/my/unread-count', }, POST: { LOGIN: '/identity/auth/login', diff --git a/src/app/core/services/socket-service/port-socket.ts b/src/app/core/services/config-socket/port-socket.ts similarity index 65% rename from src/app/core/services/socket-service/port-socket.ts rename to src/app/core/services/config-socket/port-socket.ts index 19b4121e..1ff21d66 100644 --- a/src/app/core/services/socket-service/port-socket.ts +++ b/src/app/core/services/config-socket/port-socket.ts @@ -1,2 +1,3 @@ export const CODE_COMPILER_SOCKET = 'http://localhost:4098'; export const CONVERSATION_CHAT_SOCKET = 'http://localhost:4099'; +export const NOTIFICATION_SOCKET_PORT = 'http://localhost:4101'; diff --git a/src/app/core/services/socket-service/socketConnection.service.ts b/src/app/core/services/config-socket/socketConnection.service.ts similarity index 100% rename from src/app/core/services/socket-service/socketConnection.service.ts rename to src/app/core/services/config-socket/socketConnection.service.ts diff --git a/src/app/core/services/socket-service/notification-socket.service.ts b/src/app/core/services/socket-service/notification-socket.service.ts new file mode 100644 index 00000000..a64f18b0 --- /dev/null +++ b/src/app/core/services/socket-service/notification-socket.service.ts @@ -0,0 +1,29 @@ +// notification.service.ts +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { SocketConnectionService } from '../config-socket/socketConnection.service'; +import { NOTIFICATION_SOCKET_PORT } from '../config-socket/port-socket'; + +export interface NotificationEvent { + channel: string; + recipient: string; + templateCode: string; + param: Record; + subject: string; + body: string; +} + +@Injectable({ providedIn: 'root' }) +export class NotificationService { + private readonly url = `${NOTIFICATION_SOCKET_PORT}?token=${localStorage.getItem( + 'token' + )}`; + + constructor(private socketService: SocketConnectionService) { + this.socketService.connect(this.url); + } + + listenNotifications(): Observable { + return this.socketService.on(this.url, 'notification'); + } +} diff --git a/src/app/features/conversation-chat/pages/chat/chat.component.ts b/src/app/features/conversation-chat/pages/chat/chat.component.ts index 6133c21e..b630d3c7 100644 --- a/src/app/features/conversation-chat/pages/chat/chat.component.ts +++ b/src/app/features/conversation-chat/pages/chat/chat.component.ts @@ -9,10 +9,10 @@ import { } from '@angular/core'; import { Subscription } from 'rxjs'; import { ChatService } from '../../../../core/services/api-service/chat-conversation.service'; -import { SocketConnectionService } from '../../../../core/services/socket-service/socketConnection.service'; +import { SocketConnectionService } from '../../../../core/services/config-socket/socketConnection.service'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { CONVERSATION_CHAT_SOCKET } from '../../../../core/services/socket-service/port-socket'; +import { CONVERSATION_CHAT_SOCKET } from '../../../../core/services/config-socket/port-socket'; import { Conversation, ConversationEvent, diff --git a/src/app/features/excercise/exercise-modal/update-exercise/update-exercise.component.html b/src/app/features/excercise/exercise-modal/update-exercise/update-exercise.component.html index ad1bcc04..738f4fd1 100644 --- a/src/app/features/excercise/exercise-modal/update-exercise/update-exercise.component.html +++ b/src/app/features/excercise/exercise-modal/update-exercise/update-exercise.component.html @@ -2,14 +2,14 @@ class="modal-overlay" [class.open]="isOpen" (click)="onOverlayClick($event)" - > +> + }} + + } + + - +
+ + + @if ( exerciseForm.get('cost')?.touched && + exerciseForm.get('cost')?.invalid ) { +
Chi phí là bắt buộc và phải >= 0
+ }
+ - -
-
-
- - -
+ +
-
- - -
+ +
+
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
+ +
+ + +
-
+ -
- - -
+
+ + +
-
- -
-
+
+ +
+
- -
-
- - - + + + + + + diff --git a/src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.html b/src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.html index 06981985..426a6300 100644 --- a/src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.html +++ b/src/app/features/excercise/exercise-pages/exercise-details/exercise-details.component.html @@ -171,12 +171,12 @@

{{ exercise.title }}

- @if (!isBought && exercise.cost > 0) { + @if ((!isBought && exercise.cost > 0) && !isActionActive) { - } @if ( hasQuestions && canStartDoing && (exercise.cost <= 0 ? true : - isBought) ) { + } @if ( (hasQuestions && canStartDoing && (exercise.cost <= 0 ? true : + isBought)) || isActionActive ) { diff --git a/src/app/features/post/pages/post-list/post-list.ts b/src/app/features/post/pages/post-list/post-list.ts index b6ac2d82..52b87acf 100644 --- a/src/app/features/post/pages/post-list/post-list.ts +++ b/src/app/features/post/pages/post-list/post-list.ts @@ -20,7 +20,10 @@ import { mapPostdatatoPostCardInfo } from '../../../../shared/utils/mapData'; import { LottieComponent, provideLottieOptions } from 'ngx-lottie'; import { ScrollEndDirective } from '../../../../shared/directives/scroll-end.directive'; import { BtnType1Component } from '../../../../shared/components/fxdonad-shared/ui-verser-io/btn-type1/btn-type1.component'; -import { openModalNotification } from '../../../../shared/utils/notification'; +import { + openModalNotification, + sendNotification, +} from '../../../../shared/utils/notification'; @Component({ selector: 'app-post-list', @@ -36,8 +39,8 @@ import { openModalNotification } from '../../../../shared/utils/notification'; TrendingComponent, LottieComponent, ScrollEndDirective, - BtnType1Component -], + BtnType1Component, + ], providers: [provideLottieOptions({ player: () => import('lottie-web') })], schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -248,7 +251,12 @@ export class PostListComponent { this.postservice.unSavePost(postId).subscribe({ next: () => { post.isSaved = false; // ✅ cập nhật lại trạng thái - console.log(`Unsave thành công post: ${postId}`); + sendNotification( + this.store, + 'Đã hủy lưu', + 'Bạn đã hủy lưu bài viết này', + 'success' + ); }, error: (err) => { console.error('Unsave thất bại', err); @@ -259,7 +267,7 @@ export class PostListComponent { this.postservice.savePost(postId).subscribe({ next: () => { post.isSaved = true; - console.log(`Save thành công post: ${postId}`); + sendNotification(this.store, 'Đã lưu', 'Bài viết đã lưu', 'success'); }, error: (err) => { console.error('Save thất bại', err); diff --git a/src/app/shared/components/fxdonad-shared/comment/comment.component.html b/src/app/shared/components/fxdonad-shared/comment/comment.component.html index 224a9c43..ce02b08b 100644 --- a/src/app/shared/components/fxdonad-shared/comment/comment.component.html +++ b/src/app/shared/components/fxdonad-shared/comment/comment.component.html @@ -4,81 +4,75 @@

Đã tải {{ totalCommentsCount }} Bình luận

@if (authenticated) { -
- User Avatar - - -
+
+ User Avatar + + +
}
@for (comment of comments; track comment) { -
- -
- -
-
- {{ comment.user.username }} - {{ getTimeAgo(comment.createdAt) }} -

{{ comment.content }}

- -
- Phản hồi ({{ comment.replies?.length }}) -
-
+
+ +
+ +
+
+ {{ comment.user.username }} + {{ + comment.createdAt ? getTimeAgo(comment.createdAt) : "Vừa xong" + }} +

{{ comment.content }}

+ +
+ Phản hồi ({{ comment.replies?.length }})
- - @if (authenticated) { -
-
- - - -
+
+
+ + @if (authenticated) { +
+
+ + + +
+ } + +
+ @for (reply of comment.replies; track reply) { +
+ @if (!isLastReply(comment, reply.id)) { +
} - -
- @for (reply of comment.replies; track reply) { -
- @if (!isLastReply(comment, reply.id)) { -
- } -
-
- -
-
- {{ reply.user.username }} - {{ getTimeAgo(reply.createdAt) }} -
-

{{ reply.content }}

-
-
+
+
+ +
+
+ {{ reply.user.username }} + {{ getTimeAgo(reply.createdAt) }}
- } +

{{ reply.content }}

+
- } -
- - @if (isLoading) { -
-

Đang tải thêm bình luận...

+ }
+
}
+ + @if (isLoading) { +
+

Đang tải thêm bình luận...

+
+ } +
diff --git a/src/app/shared/components/my-shared/header/header.html b/src/app/shared/components/my-shared/header/header.html index f016dd35..8d647443 100644 --- a/src/app/shared/components/my-shared/header/header.html +++ b/src/app/shared/components/my-shared/header/header.html @@ -41,7 +41,11 @@
-
+
diff --git a/src/app/shared/components/my-shared/header/header.scss b/src/app/shared/components/my-shared/header/header.scss index fe1b45fb..7a6f5391 100644 --- a/src/app/shared/components/my-shared/header/header.scss +++ b/src/app/shared/components/my-shared/header/header.scss @@ -175,7 +175,7 @@ } /*notifications number with before*/ .notification::before { - content: "1"; + content: attr(data-count); color: white; font-size: 10px; width: 16px; diff --git a/src/app/shared/components/my-shared/header/header.ts b/src/app/shared/components/my-shared/header/header.ts index 415f4515..60537cc5 100644 --- a/src/app/shared/components/my-shared/header/header.ts +++ b/src/app/shared/components/my-shared/header/header.ts @@ -1,4 +1,4 @@ -import { Component, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; @@ -13,6 +13,11 @@ 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'; +import { + NotificationEvent, + NotificationService, +} from '../../../../core/services/socket-service/notification-socket.service'; +import { NotificationListService } from '../../../../core/services/api-service/notification-list.service'; @Component({ selector: 'app-header', @@ -30,7 +35,7 @@ export class HeaderComponent { needReloadAvatar$: Observable; isDarkMode: boolean = false; - notificationCount: number = 10; + notificationCount: number = 0; isLoggedIn: boolean = false; showProfileMenu = false; isMenuVisible = false; @@ -45,7 +50,9 @@ export class HeaderComponent { private router: Router, private profileService: ProfileService, private themeService: ThemeService, - private store: Store + private store: Store, + private notificationService: NotificationService, + private notificationListService: NotificationListService ) { this.needReloadAvatar$ = this.store.select( selectVariable('reloadAvatarHeader') @@ -77,6 +84,21 @@ export class HeaderComponent { this.needCreateNewPass = JSON.parse( localStorage.getItem('needPasswordSetup') || 'false' ); + + // 👇 Đăng ký lắng nghe notification từ socket + this.notificationService + .listenNotifications() + .subscribe((event: NotificationEvent) => { + console.log('Header nhận notification:', event); + + // Tăng counter + this.notificationCount++; + + // Nếu muốn push vào modal hoặc show toast + // this.notifications.unshift(event); + }); + + this.getCountNotice(); } organizations = [ @@ -124,6 +146,17 @@ export class HeaderComponent { }); } + getCountNotice() { + this.notificationListService.countMyUnread().subscribe({ + next: (res) => { + this.notificationCount = res.result; + }, + error(err) { + console.log(err); + }, + }); + } + toggleProfileMenu() { if (this.showProfileMenu) { this.isMenuVisible = false; diff --git a/src/app/shared/utils/mapData.ts b/src/app/shared/utils/mapData.ts index d3f0898d..e955ee94 100644 --- a/src/app/shared/utils/mapData.ts +++ b/src/app/shared/utils/mapData.ts @@ -190,7 +190,7 @@ export function mapCommentToFilmResponse( parentId: comment.parentCommentId ?? null, content: comment.content, isDeactivated: false, // default vì trong Comment không có field này - createdAt: new Date().toISOString(), // hoặc gán giá trị mặc định + createdAt: comment.createdAt, // hoặc gán giá trị mặc định updatedAt: new Date().toISOString(), // tùy use case user: { id: comment.user.userId, From 34f70be6cb0523fa046e2e8333327bfca2232362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B4=20Quang=20=C4=90=E1=BB=A9c?= Date: Sun, 7 Sep 2025 17:07:29 +0700 Subject: [PATCH 2/3] fix some fields request API --- .../csv/identity_users_import_template.xlsx | Bin 53814 -> 53742 bytes src/app/core/models/post.models.ts | 4 +- .../api-service/organization.service.ts | 2 +- .../core/services/api-service/post.service.ts | 26 +- .../services/config-service/api.methods.ts | 10 +- .../details-organization.component.ts | 6 +- .../post/pages/post-create/post-create.html | 356 +++++++++--------- .../post/pages/post-create/post-create.ts | 96 +++-- 8 files changed, 247 insertions(+), 253 deletions(-) diff --git a/public/csv/identity_users_import_template.xlsx b/public/csv/identity_users_import_template.xlsx index 0c9c8a5d030c99afc7bd5bc58373aad50807b142..dfd05e8f6175cfd3026999c1bdbe7dba0416766a 100644 GIT binary patch delta 10188 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 Zk6_T1myuDQpk{hKsCsIun8cE{{vUx~v&8@a diff --git a/src/app/core/models/post.models.ts b/src/app/core/models/post.models.ts index 938fb2ee..8dd3da33 100644 --- a/src/app/core/models/post.models.ts +++ b/src/app/core/models/post.models.ts @@ -71,7 +71,7 @@ export interface postData { } export type PostType = 'Global' | 'Private' | 'Org'; export interface FileDocument { - file?: File; + file?: File[]; category?: string; // BE mong muốn STRING description?: string; tags?: string[]; // mảng -> sẽ append tags[0], tags[1]... @@ -102,7 +102,7 @@ export interface CreatePostRequest { hashtag?: string; fileDocument?: { - file?: File; + files?: File[]; category?: string; description?: string; tags?: string[]; diff --git a/src/app/core/services/api-service/organization.service.ts b/src/app/core/services/api-service/organization.service.ts index 339c5ddd..6bbaaa22 100644 --- a/src/app/core/services/api-service/organization.service.ts +++ b/src/app/core/services/api-service/organization.service.ts @@ -54,7 +54,7 @@ export class OrganizationService { return this.api.patchWithFormData>( API_CONFIG.ENDPOINTS.PATCH.EDIT_ORG(orgId), otherData, - logo + { logo } ); } diff --git a/src/app/core/services/api-service/post.service.ts b/src/app/core/services/api-service/post.service.ts index bc54943e..5085de60 100644 --- a/src/app/core/services/api-service/post.service.ts +++ b/src/app/core/services/api-service/post.service.ts @@ -94,16 +94,19 @@ export class PostService { createPost(data: CreatePostRequest) { const { fileDocument, ...otherData } = data; - // Chuẩn bị files và form data - const files: { [fieldName: string]: File } = {}; - if (fileDocument?.file) { - files['fileDocument.file'] = fileDocument.file; //tên key cần khớp backend + // Gom các field còn lại vào data (lọc undefined/null) + const formDataData: Record = {}; + for (const [key, value] of Object.entries(otherData)) { + if (value !== undefined && value !== null) { + formDataData[key] = value; + } } - // Gom các field còn lại vào data - const formDataData: Record = { - ...otherData, - }; + // Chuẩn bị files + const files: { [fieldName: string]: File[] } = {}; + if (fileDocument?.files) { + files['fileDocument.files'] = fileDocument.files; // key cần khớp backend + } if (fileDocument) { const fd: Record = { @@ -115,19 +118,18 @@ export class PostService { 'fileDocument.orgId': fileDocument.orgId, }; - // Chỉ append field nào có giá trị khác undefined for (const [key, value] of Object.entries(fd)) { - if (value !== undefined) { + if (value !== undefined && value !== null) { formDataData[key] = value; } } } - // Gọi method postWithFormData + // Gọi API return this.api.postWithFormData>( API_CONFIG.ENDPOINTS.POST.ADD_POST, formDataData, - files + Object.keys(files).length ? files : undefined // chỉ gửi nếu có file ); } diff --git a/src/app/core/services/config-service/api.methods.ts b/src/app/core/services/config-service/api.methods.ts index e155b55d..eaa43dd0 100644 --- a/src/app/core/services/config-service/api.methods.ts +++ b/src/app/core/services/config-service/api.methods.ts @@ -200,7 +200,7 @@ export class ApiMethod { patchWithFormData( endpoint: string, data?: Record, - files?: File | { [fieldName: string]: File | File[] }, + files?: File | { [fieldName: string]: File | File[] | undefined }, apiType: 'MAIN_API' | 'SECONDARY_API' = 'MAIN_API' ): Observable { const url = `${API_CONFIG.BASE_URLS[apiType]}${endpoint}`; @@ -221,8 +221,12 @@ export class ApiMethod { Object.keys(files).forEach((fieldName) => { const fileItem = files[fieldName]; if (Array.isArray(fileItem)) { - fileItem.forEach((file) => formData.append(fieldName, file)); - } else { + fileItem.forEach((file) => { + if (file !== undefined && file !== null) { + formData.append(fieldName, file); + } + }); + } else if (fileItem !== undefined && fileItem !== null) { formData.append(fieldName, fileItem); } }); diff --git a/src/app/features/organization/organization-component/details-organization/details-organization.component.ts b/src/app/features/organization/organization-component/details-organization/details-organization.component.ts index 342c9a1f..2798c39d 100644 --- a/src/app/features/organization/organization-component/details-organization/details-organization.component.ts +++ b/src/app/features/organization/organization-component/details-organization/details-organization.component.ts @@ -97,9 +97,9 @@ export class DetailsOrganizationComponent implements OnInit { } onFileChange(event: any) { - const file = event.target.files[0]; - if (file) { - this.editForm.logo = file; + const logo = event.target.files[0]; + if (logo) { + this.editForm.logo = logo; } } diff --git a/src/app/features/post/pages/post-create/post-create.html b/src/app/features/post/pages/post-create/post-create.html index 151ebf09..596c316b 100644 --- a/src/app/features/post/pages/post-create/post-create.html +++ b/src/app/features/post/pages/post-create/post-create.html @@ -81,190 +81,182 @@ name="visibility" [(ngModel)]="post.postType" value="Org" - /> - Nội bộ - - - -
+ /> + Nội bộ + + + +
+
+ +
+ Bình luận +
+ + +
+
+
+ +
+ +
+
+ Hủy + Lưu Nháp + Đăng Bài +
+
+
+ @if (drafts.length > 0) { +
+
+

Bản nháp đã lưu

+ {{ drafts.length }} bản nháp +
+
    + @for (draft of drafts; track draft) { +
  • +
    +
    + {{ draft.title }} + + {{ draft.timestamp | date:'short' }} +
    +
    +
    + +
    +
  • + } +
+
+ } -
- Bình luận -
- - -
-
-
+
+
+
-
- @if (drafts.length > 0) { -
-
-

Bản nháp đã lưu

- {{ drafts.length }} bản nháp -
-
    - @for (draft of drafts; track draft) { -
  • -
    -
    - {{ draft.title }} - - {{ draft.timestamp | date:'short' }} - -
    -
    -
    - -
    -
  • - } -
-
- } + + + + + + + Chọn file + + + @if (filePreviews) { +
+
+ + + -
-
-
+
- - - - - - - Chọn file - - - @if (filePreview) { -
-
- @if (isImageFile) { - Xem trước ảnh - } - @if (isVideoFile) { - - } - -
-
- -
-
- } -
-
- + } + + + diff --git a/src/app/features/post/pages/post-create/post-create.ts b/src/app/features/post/pages/post-create/post-create.ts index 137119ec..e1e7782f 100644 --- a/src/app/features/post/pages/post-create/post-create.ts +++ b/src/app/features/post/pages/post-create/post-create.ts @@ -48,8 +48,8 @@ export interface Draft { DropdownButtonComponent, ButtonComponent, FormsModule, - CommonModule -], + CommonModule, + ], }) export class PostCreatePageComponent { @ViewChild('linkInput') linkInput!: ElementRef; @@ -99,8 +99,15 @@ export class PostCreatePageComponent { ) {} // ===== File (1 ảnh) ===== - selectedFile: File | null = null; - filePreview: string | null = null; + // ===== File (nhiều ảnh/video) ===== + selectedFiles: File[] = []; + filePreviews: { + url: string; + isImage: boolean; + isVideo: boolean; + name: string; + }[] = []; + isImageFile: boolean = false; isVideoFile: boolean = false; @@ -108,48 +115,33 @@ export class PostCreatePageComponent { this.loadAllDrafts(); } - onFileSelected(event: Event) { + onFilesSelected(event: Event) { const input = event.target as HTMLInputElement; if (input.files && input.files.length > 0) { - const file = input.files[0]; - - // Reset tất cả các cờ - this.selectedFile = null; - this.filePreview = null; - this.isImageFile = false; - this.isVideoFile = false; - - if (file.type.startsWith('image/')) { - this.selectedFile = file; - this.isImageFile = true; - } else if (file.type.startsWith('video/')) { - this.selectedFile = file; - this.isVideoFile = true; - } else { - // Nếu không phải ảnh hoặc video, không làm gì cả - return; - } - - // Đọc file để tạo URL xem trước - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target?.result) { - this.filePreview = e.target.result as string; - // Kích hoạt phát hiện thay đổi để cập nhật UI - this.cdr.detectChanges(); - } - }; - reader.readAsDataURL(file); + this.selectedFiles = Array.from(input.files); + this.filePreviews = []; + + this.selectedFiles.forEach((file) => { + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target?.result) { + this.filePreviews.push({ + url: e.target.result as string, + isImage: file.type.startsWith('image/'), + isVideo: file.type.startsWith('video/'), + name: file.name, + }); + this.cdr.detectChanges(); + } + }; + reader.readAsDataURL(file); + }); } } - removeFile() { - // Đổi tên từ removeImage thành removeFile - this.selectedFile = null; - this.filePreview = null; - this.isImageFile = false; - this.isVideoFile = false; - this.post.fileDocument = null; // Xóa luôn mô tả khi remove file + removeFile(index: number) { + this.selectedFiles.splice(index, 1); + this.filePreviews.splice(index, 1); } // ===== Input handlers ===== @@ -297,9 +289,9 @@ export class PostCreatePageComponent { this.post.isPublic = this.post.postType !== 'Private'; // Gắn file vào fileDocument - if (this.selectedFile) { + if (this.selectedFiles) { if (!this.post.fileDocument) this.post.fileDocument = {}; - this.post.fileDocument.file = this.selectedFile; + this.post.fileDocument.file = this.selectedFiles; } this.store.dispatch( @@ -309,16 +301,20 @@ export class PostCreatePageComponent { // Debug nhanh const payload: CreatePostRequest = { title: this.post.title, - content: this.post.content, - isPublic: this.post.isPublic, + content: this.htmlToMd.convert(this.editorContent || ''), + isPublic: this.post.postType !== 'Private', allowComment: this.post.allowComment ?? false, postType: this.post.postType ?? 'Global', hashtag: this.post.hashtag, fileDocument: { - file: this.post.fileDocument?.file, + files: this.selectedFiles, // Gửi nhiều file description: this.post.fileDocument?.description, - isLectureVideo: this.isVideoFile, // Sử dụng cờ isVideoFile - isTextBook: this.isImageFile, // Sử dụng cờ isImageFile + tags: this.post.fileDocument?.tags, + isLectureVideo: this.selectedFiles.some((f) => + f.type.startsWith('video/') + ), + isTextBook: this.selectedFiles.some((f) => f.type.startsWith('image/')), + orgId: this.post.fileDocument?.orgId, }, }; @@ -386,8 +382,8 @@ export class PostCreatePageComponent { fileDocument: null, }; this.editorContent = ''; - this.selectedFile = null; - this.filePreview = null; + this.selectedFiles = []; + this.filePreviews = []; this.isImageFile = false; this.isVideoFile = false; this.selectedOptions = {}; From dd745f064b3b236cc6fd2ea6c8f43acb42d4987b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B4=20Quang=20=C4=90=E1=BB=A9c?= Date: Mon, 8 Sep 2025 17:24:55 +0700 Subject: [PATCH 3/3] add first config cancel call api in qr-payment component --- .../config/ignoreApiHandleError.ts | 2 ++ .../notification-socket.service.ts | 9 +++++- .../pages/user-list/user-list.html | 5 +-- .../pages/user-list/user-list.ts | 2 +- .../pages/org-blocks/org-blocks.component.ts | 7 ++++- .../pages/qr-payment/qr-payment.component.ts | 31 +++++++++++++------ .../components/my-shared/header/header.ts | 15 ++++----- 7 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/app/core/interceptors/config/ignoreApiHandleError.ts b/src/app/core/interceptors/config/ignoreApiHandleError.ts index 1213e3ea..b797426a 100644 --- a/src/app/core/interceptors/config/ignoreApiHandleError.ts +++ b/src/app/core/interceptors/config/ignoreApiHandleError.ts @@ -4,6 +4,8 @@ export const IGNORE_ERROR_NOTIFICATION_URLS = [ '/submission/exercise/quiz/', // Bỏ qua tất cả endpoint chứa pattern này '/profile/user/', '/submission/exercise/coding/', + '/notification/', + '/ai/chat/', // Ví dụ pattern regex: /\/submission\/exercise\/quiz\/\d+(\?.*)?$/ // Thêm các endpoint khác nếu cần ]; diff --git a/src/app/core/services/socket-service/notification-socket.service.ts b/src/app/core/services/socket-service/notification-socket.service.ts index a64f18b0..8b9adff5 100644 --- a/src/app/core/services/socket-service/notification-socket.service.ts +++ b/src/app/core/services/socket-service/notification-socket.service.ts @@ -14,7 +14,7 @@ export interface NotificationEvent { } @Injectable({ providedIn: 'root' }) -export class NotificationService { +export class NotificationSocketService { private readonly url = `${NOTIFICATION_SOCKET_PORT}?token=${localStorage.getItem( 'token' )}`; @@ -26,4 +26,11 @@ export class NotificationService { listenNotifications(): Observable { return this.socketService.on(this.url, 'notification'); } + + listenNoticeCount(): Observable<{ unread: number }> { + return this.socketService.on<{ unread: number }>( + this.url, + 'notification-unread' + ); + } } 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 45d2ddf9..4651e419 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 @@ -21,11 +21,12 @@ [disabled]="false" [multiSelect]="true" [isDisplayCheckbox]="false" + [removeSingleSelected]="true" (onSelect)="handleSelect(filterRoleKey, $event)" [isOpen]="activeDropdown === filterRoleKey" (toggle)="toggleDropdown(filterRoleKey)" [isDisplaySelectedOpptionLabels]="true" - [isButtonControl]="true" + [isButtonControl]="false" [isSearchable]="true" [minHeight]="true" [needIndexColor]="true" @@ -132,7 +133,7 @@ [amountDataPerPage]="itemsPerPage" [needNo]="true" [needDelete]="true" - [needEdit]="true" + [needEdit]="false" [needViewResult]="false" [needswitch]="true" [idSelect]="'userId'" 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 00565b9a..0f361e64 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 @@ -118,7 +118,7 @@ export class UserListComponent { // Pagination pageIndex: number = 1; - itemsPerPage: number = 8; + itemsPerPage: number = 10; totalDatas: number = 0; sortBy: EnumType['sort'] = 'CREATED_AT'; asc: boolean = false; diff --git a/src/app/features/organization/pages/org-blocks/org-blocks.component.ts b/src/app/features/organization/pages/org-blocks/org-blocks.component.ts index 210720a5..b5ac22ff 100644 --- a/src/app/features/organization/pages/org-blocks/org-blocks.component.ts +++ b/src/app/features/organization/pages/org-blocks/org-blocks.component.ts @@ -86,7 +86,12 @@ export class OrgBlocksComponent implements OnInit { next: (res) => { const result = res.result; this.totalPages = result.totalPages; - this.blocks = loadMore ? [...this.blocks, ...result.data] : result.data; + this.blocks = loadMore + ? [ + ...this.blocks, + ...result.data.filter((data) => data.code !== 'UNASSIGNED'), + ] + : result.data.filter((data) => data.code !== 'UNASSIGNED'); this.isLoading = false; }, error: () => (this.isLoading = false), diff --git a/src/app/features/service-payment/pages/qr-payment/qr-payment.component.ts b/src/app/features/service-payment/pages/qr-payment/qr-payment.component.ts index 9a21bc32..f86228a8 100644 --- a/src/app/features/service-payment/pages/qr-payment/qr-payment.component.ts +++ b/src/app/features/service-payment/pages/qr-payment/qr-payment.component.ts @@ -1,4 +1,10 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { + Component, + OnInit, + OnDestroy, + inject, + DestroyRef, +} from '@angular/core'; import { Observable, Subscription, interval, of } from 'rxjs'; import { Store } from '@ngrx/store'; @@ -20,6 +26,7 @@ import { } from '../../validate/qr-payment.utils'; import { decodeJWT } from '../../../../shared/utils/stringProcess'; import { ModalNoticeService } from '../../../../shared/store/modal-notice-state/modal-notice.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-qr-payment', @@ -29,6 +36,8 @@ import { ModalNoticeService } from '../../../../shared/store/modal-notice-state/ styleUrls: ['./qr-payment.component.scss'], }) export class QrPaymentComponent implements OnInit, OnDestroy { + private destroyRef = inject(DestroyRef); //ngắt request khi thoát trang (thêm 2 dòng, dòng này và dòng pipe trong service) + amount: number | null = null; // Số tiền VNĐ người dùng nhập maxAmount: number = 20000000; qrTimeOut: boolean = true; @@ -90,15 +99,19 @@ export class QrPaymentComponent implements OnInit, OnDestroy { this.countdownInterval?.unsubscribe(); } + //có ngắt request khi thoát trang, (còn lại là pipe dưới để ngắt request) fetchCurrentMoney() { - this.paymentService.getMyWallet().subscribe({ - next: (res) => { - this.currentGp = res.result.balance; - }, - error(err) { - console.log(err); - }, - }); + this.paymentService + .getMyWallet() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (res) => { + this.currentGp = res.result.balance; + }, + error(err) { + console.log(err); + }, + }); } paymentFailed() { diff --git a/src/app/shared/components/my-shared/header/header.ts b/src/app/shared/components/my-shared/header/header.ts index 60537cc5..52ad29ef 100644 --- a/src/app/shared/components/my-shared/header/header.ts +++ b/src/app/shared/components/my-shared/header/header.ts @@ -13,10 +13,7 @@ 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'; -import { - NotificationEvent, - NotificationService, -} from '../../../../core/services/socket-service/notification-socket.service'; +import { NotificationSocketService } from '../../../../core/services/socket-service/notification-socket.service'; import { NotificationListService } from '../../../../core/services/api-service/notification-list.service'; @Component({ @@ -51,7 +48,7 @@ export class HeaderComponent { private profileService: ProfileService, private themeService: ThemeService, private store: Store, - private notificationService: NotificationService, + private notificationService: NotificationSocketService, private notificationListService: NotificationListService ) { this.needReloadAvatar$ = this.store.select( @@ -87,12 +84,12 @@ export class HeaderComponent { // 👇 Đăng ký lắng nghe notification từ socket this.notificationService - .listenNotifications() - .subscribe((event: NotificationEvent) => { - console.log('Header nhận notification:', event); + .listenNoticeCount() + .subscribe((event: { unread: number }) => { + console.log('Header nhận count notification:', event.unread); // Tăng counter - this.notificationCount++; + this.notificationCount = event.unread; // Nếu muốn push vào modal hoặc show toast // this.notifications.unshift(event);