diff --git a/README.md b/README.md index 659771dd..4f272ecf 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ CodeCampus là một nền tảng trực tuyến được thiết kế để h ## Yêu cầu hệ thống - Node.js (phiên bản LTS mới nhất) - npm (được cài đặt cùng với Node.js) -- Angular CLI (phiên bản 19+) +- Angular CLI (phiên bản 20.2.1) ## Cấu trúc dự án (Tổng quan & Minh họa) diff --git a/src/app/core/services/api-service/user.service.ts b/src/app/core/services/api-service/user.service.ts index f0414634..08ea2a6f 100644 --- a/src/app/core/services/api-service/user.service.ts +++ b/src/app/core/services/api-service/user.service.ts @@ -63,4 +63,10 @@ export class UserService { return this.api.post>(enpoint, data); } + + deleteUserAccount(accountId: string) { + return this.api.delete>( + API_CONFIG.ENDPOINTS.DELETE.DELETE_USER_ACCOUNT(accountId) + ); + } } diff --git a/src/app/core/services/config-service/api.enpoints.ts b/src/app/core/services/config-service/api.enpoints.ts index 13f3dfaa..1aff9d7b 100644 --- a/src/app/core/services/config-service/api.enpoints.ts +++ b/src/app/core/services/config-service/api.enpoints.ts @@ -327,6 +327,7 @@ export const API_CONFIG = { DELETE_BLOCK: (blockId: string) => `/org/block/${blockId}`, REMOVE_MEMBER_FROM_BLOCK: (blockId: string, memberId: string) => `/org/block/${blockId}/member/${memberId}`, + DELETE_USER_ACCOUNT: (userId: string) => `/identity/user/${userId}`, }, }, HEADERS: { 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 index ada451e4..9b1f6c22 100644 --- 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 @@ -24,9 +24,11 @@ transition: opacity var(--transition-speed) ease, visibility var(--transition-speed) ease; z-index: 1000; + pointer-events: none; &.active { opacity: 1; + pointer-events: auto; visibility: visible; } } @@ -44,10 +46,12 @@ opacity var(--transition-speed) ease; display: flex; flex-direction: column; + pointer-events: none; &.active { transform: translateY(0); opacity: 1; + pointer-events: auto; } } 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 3c90a8fc..45d2ddf9 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 @@ -51,7 +51,7 @@ >
- - + --> + + + + + @@ -136,16 +149,34 @@ avatar + } @else { +
+ {{ row.username?.charAt(0) }} +
} - {{ row.displayName }} + {{ row.displayName ? row.displayName : 'Chưa đặt tên' }}
@@ -190,9 +221,9 @@ } diff --git a/src/app/features/admin/user-management/pages/user-list/user-list.scss b/src/app/features/admin/user-management/pages/user-list/user-list.scss index 8a678106..7bb7f7c9 100644 --- a/src/app/features/admin/user-management/pages/user-list/user-list.scss +++ b/src/app/features/admin/user-management/pages/user-list/user-list.scss @@ -57,6 +57,34 @@ } } } + + .btn { + padding: 6px 12px; + border-radius: 6px; + border: none; + cursor: pointer; + transition: all 0.3s ease; + + &.primary { + background: var(--button-color); + color: var(--reverse-color-text); + &:hover { + background: oklch(from var(--button-color) calc(l * 0.8) c h); + } + display: flex; + gap: 4px; + } + &.danger { + background: #dc3545; + color: #fff; + &:hover { + background: #a71d2a; + } + } + &.other { + padding: 8px; + } + } } } } 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 4e05adc1..00565b9a 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 @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, ElementRef, ViewChild } from '@angular/core'; import { NgClass } from '@angular/common'; import { Store } from '@ngrx/store'; @@ -20,8 +20,17 @@ import { } from '../../../../../core/models/user.models'; 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 { + clearLoading, + setLoading, +} from '../../../../../shared/store/loading-state/loading.action'; import { CreateUserModalComponent } from '../../modal/create-user-modal/create-user-modal.component'; +import { + openModalNotification, + sendNotification, +} from '../../../../../shared/utils/notification'; +import { OrganizationService } from '../../../../../core/services/api-service/organization.service'; +import { ImportMemberResponse } from '../../../../../core/models/organization.model'; @Component({ selector: 'app-user-list', @@ -41,6 +50,7 @@ import { CreateUserModalComponent } from '../../modal/create-user-modal/create-u standalone: true, }) export class UserListComponent { + @ViewChild('fileInput') fileInput!: ElementRef; private debounceTimer: ReturnType | null = null; headers = userHeaders; @@ -109,9 +119,11 @@ export class UserListComponent { // Pagination pageIndex: number = 1; itemsPerPage: number = 8; + totalDatas: number = 0; sortBy: EnumType['sort'] = 'CREATED_AT'; asc: boolean = false; hasMore = true; + importResult: ImportMemberResponse | null = null; // Loading isLoading = false; @@ -121,7 +133,11 @@ export class UserListComponent { role: { value: string; label: string }[] = []; status: { value: string; label: string }[] = []; - constructor(private userService: UserService, private store: Store) { + constructor( + private userService: UserService, + private store: Store, + private orgService: OrganizationService + ) { // Mock data for role this.role = [ { value: 'STUDENT', label: 'Học sinh' }, @@ -137,6 +153,10 @@ export class UserListComponent { ]; } + triggerFileInput() { + this.fileInput.nativeElement.click(); + } + // Lifecycle ngOnInit(): void { this.fetchDataListUser(); @@ -164,7 +184,9 @@ export class UserListComponent { .subscribe({ next: (res) => { this.ListUser = res.result.data; - if (this.ListUser.length < this.itemsPerPage) { + this.totalDatas = res.result.totalElements; + this.pageIndex = res.result.currentPage; + if (res.result.currentPage >= res.result.totalPages) { this.hasMore = false; } this.isLoading = false; @@ -180,7 +202,8 @@ export class UserListComponent { // Handlers handlePageChange(page: number) { - console.log('chuyển trang'); + this.pageIndex = page; + this.fetchDataListUser(); } handleImport = () => { @@ -271,4 +294,66 @@ export class UserListComponent { return values.join(', '); } + + openModalDelete = (id: string | number) => { + openModalNotification( + this.store, + 'Xóa người dùng', + 'Bạn có chắc chắn xóa người dùng này?', + 'Đồng ý', + 'Hủy', + () => this.deleteUser(id.toString()) + ); + }; + + deleteUser(userId: string) { + return this.userService.deleteUserAccount(userId).subscribe({ + next: () => { + sendNotification(this.store, 'Đã xóa', 'Đã xóa người dùng', 'success'); + }, + error: (err) => { + console.log(err); + }, + }); + } + + downloadTemplate() { + const link = document.createElement('a'); + link.href = '/csv/identity_users_import_template.xlsx'; + link.download = 'identity_users_import_template.xlsx'; + link.click(); + } + + // Khi chọn file import + onImportExcel(event: Event) { + const file = (event.target as HTMLInputElement).files?.[0]; + if (!file) return; + + Promise.resolve().then(() => { + this.store.dispatch( + setLoading({ isLoading: true, content: 'Đang thêm test case...' }) + ); + }); + + this.orgService.importMemberExcel(file).subscribe({ + next: (res) => { + this.importResult = res.result; + sendNotification( + this.store, + 'Import thành công', + `Import hoàn tất:\nTổng: ${res.result.total} \nTạo mới: ${res.result.created} \nBỏ qua: ${res.result.skipped}\nLỗi: ${res.result.errors.length}`, + 'success' + ); + this.store.dispatch(clearLoading()); + }, + error: (err) => { + alert('Import thất bại!'); + console.error(err); + this.store.dispatch(clearLoading()); + }, + }); + + // Reset input để chọn lại cùng 1 file lần sau + (event.target as HTMLInputElement).value = ''; + } } diff --git a/src/app/features/organization/pages/organization-management/organization-management.component.ts b/src/app/features/organization/pages/organization-management/organization-management.component.ts index f00bfd68..ca664240 100644 --- a/src/app/features/organization/pages/organization-management/organization-management.component.ts +++ b/src/app/features/organization/pages/organization-management/organization-management.component.ts @@ -24,6 +24,10 @@ import { OrganizationCreateModalComponent } from '../../organization-component/o import { Router } from '@angular/router'; import { sendNotification } from '../../../../shared/utils/notification'; import { Store } from '@ngrx/store'; +import { + clearLoading, + setLoading, +} from '../../../../shared/store/loading-state/loading.action'; @Component({ selector: 'app-organization-management', @@ -148,6 +152,12 @@ export class OrganizationManagementComponent implements OnInit, OnDestroy { const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return; + Promise.resolve().then(() => { + this.store.dispatch( + setLoading({ isLoading: true, content: 'Đang thêm test case...' }) + ); + }); + this.orgService.importMemberExcel(file).subscribe({ next: (res) => { this.importResult = res.result; @@ -157,10 +167,13 @@ export class OrganizationManagementComponent implements OnInit, OnDestroy { `Import hoàn tất:\nTổng: ${res.result.total} \nTạo mới: ${res.result.created} \nBỏ qua: ${res.result.skipped}\nLỗi: ${res.result.errors.length}`, 'success' ); + + this.store.dispatch(clearLoading()); }, error: (err) => { alert('Import thất bại!'); console.error(err); + this.store.dispatch(clearLoading()); }, }); diff --git a/src/app/features/resource-learning/pages/resource-create/resource-create.ts b/src/app/features/resource-learning/pages/resource-create/resource-create.ts index d8282cb1..2fdd72e3 100644 --- a/src/app/features/resource-learning/pages/resource-create/resource-create.ts +++ b/src/app/features/resource-learning/pages/resource-create/resource-create.ts @@ -21,6 +21,7 @@ import { Store } from '@ngrx/store'; import { InputComponent } from '../../../../shared/components/fxdonad-shared/input/input'; import { decodeJWT } from '../../../../shared/utils/stringProcess'; import { FormsModule } from '@angular/forms'; +import { Location } from '@angular/common'; @Component({ selector: 'app-resource-create', @@ -41,20 +42,9 @@ export class ResourceCreatePageComponent { private resourceService: ResourceService, private store: Store, private cdr: ChangeDetectorRef, - private ngZone: NgZone - ) { - this.tag = [ - { value: 'tag1', label: 'Tag 1' }, - { value: 'tag2', label: 'Tag 2' }, - { value: 'tag3', label: 'Tag 3' }, - ]; - this.category = [ - { value: '0', label: 'Tệp ảnh' }, - { value: '1', label: 'Tệp video' }, - { value: '2', label: 'Tệp tài liệu' }, - { value: '3', label: 'Tệp khác' }, - ]; - } + private ngZone: NgZone, + private location: Location + ) {} thumbnail: string = ''; thumbnailError: string | null = null; handleInputChange(value: string | number): void { @@ -64,7 +54,6 @@ export class ResourceCreatePageComponent { console.log('Input changed:', this.thumbnail); } - // ===== Link cũ (oldImgesUrls) ===== newLink = ''; isAddingLink = false; @@ -163,7 +152,7 @@ export class ResourceCreatePageComponent { cancelResource(): void { // Logic to cancel the post creation - console.log('Post creation cancelled'); + this.location.back(); } editorContent: string = ''; diff --git a/src/app/shared/components/my-shared/table/table.component.ts b/src/app/shared/components/my-shared/table/table.component.ts index 5a53f814..b5cf710d 100644 --- a/src/app/shared/components/my-shared/table/table.component.ts +++ b/src/app/shared/components/my-shared/table/table.component.ts @@ -15,12 +15,7 @@ import { ButtonComponent } from '../button/button.component'; templateUrl: './table.component.html', styleUrls: ['./table.component.scss'], standalone: true, - imports: [ - NgClass, - CommonModule, - InteractiveButtonComponent, - ButtonComponent -], + imports: [NgClass, CommonModule, InteractiveButtonComponent, ButtonComponent], }) export class TableComponent implements AfterContentInit { @Input() data: Array<{ [key: string]: any }> = []; @@ -64,6 +59,7 @@ export class TableComponent implements AfterContentInit { handleDeleteClick(id: string | number) { console.log('Delete', id); + this.onDeleteClick(id); } handleEditClick(id: string | number) {