diff --git a/ng-ttc-frontend/src/app/component/main-project-add/main-project-add.html b/ng-ttc-frontend/src/app/component/main-project-add/main-project-add.html index 549725d..3869aba 100644 --- a/ng-ttc-frontend/src/app/component/main-project-add/main-project-add.html +++ b/ng-ttc-frontend/src/app/component/main-project-add/main-project-add.html @@ -25,39 +25,46 @@

ข้อมูลโครงการ

- + @if(currentStep > 1) { + + } + @if(currentStep === 1) { +
+
+
+ + + @if(f['prjnam'] && f['prjnam'].invalid && f['prjnam'].touched) { +

กรุณาระบุชื่อโครงการ

+ } +
-
-
-
- - -

กรุณาระบุชื่อโครงการ

-
+
+ +
+ ฿ + +
+ @if(f['prjwntbdg'] && f['prjwntbdg'].invalid && f['prjwntbdg'].touched) { +

กรุณาระบุจำนวนเงิน

+ } +
-
- -
- ฿ - -
-

กรุณาระบุจำนวนเงิน

-
- -
- -
-
-
+
+ +
+
+
+ }

เอกสารอ้างอิง

- + @if(currentStep > 2) { + + } - -
+ @if(currentStep === 2) { +

- สามารถแนบไฟล์ PDF, JPG หรือ PNG ที่เกี่ยวข้องเพื่อประกอบการพิจารณา + สามารถแนบไฟล์เฉพาะไฟล์รูปภาพ, PDF หรือเอกสาร Word ที่เกี่ยวข้องเพื่อประกอบการพิจารณา

@@ -92,19 +101,22 @@
คลิกเพื่อเลือกไฟล์ หรือ ลากไฟล์มาวาง
- -
-
-
- - {{ file.name }} - ({{ (file.size / 1024).toFixed(2) }} KB) -
- -
-
+ @if ( filePreviews.length > 0 ) { +
+ @for (file of filePreviews; track $index; let i = $index) { +
+
+ + {{ file.name }} + ({{ (file.size / 1024).toFixed(2) }} KB) +
+ +
+ } +
+ }
+ }
-
-
-
-
- -
-

ตรวจสอบความถูกต้อง

-

กรุณาตรวจสอบข้อมูลก่อนทำการส่งขออนุมัติ

-
+ @if(currentStep === 3) { +
+
+
+
+ +
+

ตรวจสอบความถูกต้อง

+

กรุณาตรวจสอบข้อมูลก่อนทำการส่งขออนุมัติ

+
-
-
- ชื่อโครงการ - {{ f['projectName'].value || '-' }} -
-
- จำนวนเงิน - {{ formatCurrency(f['budgetAmount'].value) }} -
-
- เอกสารแนบ - - {{ attachedFiles.length }} ไฟล์ - -
-
+
+
+ ชื่อโครงการ + {{ f['prjnam'].value || '-' }} +
+
+ จำนวนเงิน + {{ formatCurrency(f['prjwntbdg'].value) }} +
+
+ เอกสารแนบ + + {{ filePreviews.length }} ไฟล์ + +
+
-
- - -
-
-
+
+ + +
+
+
+ } diff --git a/ng-ttc-frontend/src/app/component/main-project-add/main-project-add.ts b/ng-ttc-frontend/src/app/component/main-project-add/main-project-add.ts index 6d756b8..af1163a 100644 --- a/ng-ttc-frontend/src/app/component/main-project-add/main-project-add.ts +++ b/ng-ttc-frontend/src/app/component/main-project-add/main-project-add.ts @@ -1,3 +1,4 @@ +import { GeneralService } from './../../services/generalservice'; import { Component, OnInit, Output, EventEmitter } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; import { ToastrService } from 'ngx-toastr'; @@ -12,14 +13,21 @@ export class MainProjectAdd implements OnInit { // ไม่ต้องใช้ @Input() แล้ว Parent จะเข้าถึงผ่าน ViewChild isLoading: boolean = false; - @Output() save = new EventEmitter(); + @Output() projectAddSave = new EventEmitter(); @Output() cancel = new EventEmitter(); currentStep: number = 1; projectForm!: FormGroup; - attachedFiles: any[] = []; + // attachedFiles: any[] = []; + // ตัวแปรสำหรับเก็บไฟล์ที่ผ่านการตรวจสอบแล้ว + selectedFiles: File[] = []; + // ตัวแปรสำหรับเก็บ Preview (ถ้าต้องการแสดงผล) + filePreviews: any[] = []; - constructor(private toastr: ToastrService) {} + constructor( + private generalService: GeneralService, + private toastr: ToastrService + ) {} ngOnInit(): void { this.setupFormControl(); @@ -27,8 +35,8 @@ export class MainProjectAdd implements OnInit { setupFormControl(): void { this.projectForm = new FormGroup({ - projectName: new FormControl('', [Validators.required, Validators.maxLength(200)]), - budgetAmount: new FormControl('', [Validators.required, Validators.min(1)]) + prjnam: new FormControl('', [Validators.required, Validators.maxLength(200)]), + prjwntbdg: new FormControl('', [Validators.required, Validators.min(1)]) }); } @@ -42,28 +50,62 @@ export class MainProjectAdd implements OnInit { } this.currentStep = step; } + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; - onFileSelected(event: any): void { - const files = event.target.files; - if (files && files.length > 0) { - for (let i = 0; i < files.length; i++) { - const file = files[i]; + if (input.files && input.files.length > 0) { + // Reset ค่าเก่า (ถ้าต้องการให้เลือกใหม่ทับของเดิม) หรือจะใช้ concat ถ้าต้องการเพิ่มต่อท้าย + // this.selectedFiles = []; + // this.filePreviews = []; + + Array.from(input.files).forEach((file: File) => { + + // 1. Validate File Type (ตัวอย่าง: รับเฉพาะ Image และ PDF) + const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; + if (!allowedTypes.includes(file.type)) { + this.toastr.error('Invalid File Type', `ไฟล์ ${file.name} ไม่รองรับ (รองรับเฉพาะรูปภาพ, PDF, Word)`); + return; // ข้ามไฟล์นี้ไป + } + + // 2. Validate File Size (ตัวอย่าง: ไม่เกิน 5MB) + const maxSize = 5 * 1024 * 1024; // 5MB in bytes + if (file.size > maxSize) { + this.toastr.error('File Too Large', `ไฟล์ ${file.name} มีขนาดใหญ่เกิน 5MB`); + return; // ข้ามไฟล์นี้ไป + } + + //ผ่านการตรวจสอบ: เก็บไฟล์ลง Array เพื่อเตรียมส่ง Form Data + this.selectedFiles.push(file); + + // 3. Generate Preview (Optional: สำหรับแสดงผลหน้าเว็บ) const reader = new FileReader(); - reader.onload = (e: any) => { - this.attachedFiles.push({ + reader.onload = (e: ProgressEvent) => { + this.filePreviews.push({ name: file.name, - size: file.size, + size: this.formatBytes(file.size), // แปลง bytes เป็น KB/MB type: file.type, - content: e.target.result + content: e.target?.result // Base64 String }); }; reader.readAsDataURL(file); - } + }); } } + // Helper Function: แปลงขนาดไฟล์ให้อ่านง่าย + formatBytes(bytes: number, decimals = 2): string { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + } + + // ฟังก์ชันลบไฟล์ที่เลือก (เผื่อใช้ใน UI) removeFile(index: number): void { - this.attachedFiles.splice(index, 1); + this.selectedFiles.splice(index, 1); + this.filePreviews.splice(index, 1); } get f() { return this.projectForm.controls; } @@ -75,13 +117,19 @@ export class MainProjectAdd implements OnInit { onSubmit(): void { if (this.projectForm.invalid) return; + let seq = localStorage.getItem('id'); + const formData = new FormData(); + const prjnam = this.projectForm.get('prjnam')?.value || ''; + const prjwntbdg = this.projectForm.get('prjwntbdg')?.value; + formData.append('prjusrseq', seq ?? ''); + formData.append('prjnam', prjnam); + formData.append('prjwntbdg', prjwntbdg ? prjwntbdg : '0.00'); + formData.append('typ', 'prj') - const body = { - ...this.projectForm.value, - files: this.attachedFiles - }; - - this.save.emit(body); + for (let file of this.selectedFiles) { + formData.append('prjdoc', file); // Key ต้องชื่อ 'prjdoc' ตาม Middleware + } + this.projectAddSave.emit(formData); } onCancel(): void { diff --git a/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts b/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts index 484f08b..3b123a3 100644 --- a/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts +++ b/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts @@ -11,8 +11,8 @@ export class SidebarComponent implements OnInit { @Input() isCollapsed: boolean = false; // รับค่าสถานะย่อ/ขยาย userData: any = { - name: 'Nuttakit', - role: 'Admin', + name: localStorage.getItem('usrthinam') + ' ' + localStorage.getItem('usrthilstnam'), + role: '', avatar: '' }; diff --git a/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts b/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts index d3e1981..e593a0d 100644 --- a/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts +++ b/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts @@ -59,6 +59,9 @@ export class LoginContentComponent implements OnInit { if (result.code === '200' && result.data?.token) { this.generalService.trowApi(result); localStorage.setItem('access_token', result.data.token); + localStorage.setItem('id', result.data.usrseq); + localStorage.setItem('usrthinam', result.data.usrthinam); + localStorage.setItem('usrthilstnam', result.data.usrthilstnam); this.jwtService.restartCountdown(); this.router.navigate(['main']); } else { diff --git a/ng-ttc-frontend/src/app/content/main-project-content/main-project-content.html b/ng-ttc-frontend/src/app/content/main-project-content/main-project-content.html index 3544b2e..7accf22 100644 --- a/ng-ttc-frontend/src/app/content/main-project-content/main-project-content.html +++ b/ng-ttc-frontend/src/app/content/main-project-content/main-project-content.html @@ -38,7 +38,7 @@ } @else if ( mode == 'add') { } diff --git a/ng-ttc-frontend/src/app/content/main-project-content/main-project-content.ts b/ng-ttc-frontend/src/app/content/main-project-content/main-project-content.ts index b2f3c96..9c3afdf 100644 --- a/ng-ttc-frontend/src/app/content/main-project-content/main-project-content.ts +++ b/ng-ttc-frontend/src/app/content/main-project-content/main-project-content.ts @@ -40,34 +40,34 @@ export class MainProjectContent implements OnInit { } // รับ Event (save) จากลูก แล้วยิง API - onSaveProject(projectData: any): void { - // เปิด Loading ที่ลูก - if (this.mainProjectAdd) { - this.mainProjectAdd.isLoading = true; - } + // onSaveProject(projectData: any): void { + // // เปิด Loading ที่ลูก + // if (this.mainProjectAdd) { + // this.mainProjectAdd.isLoading = true; + // } - const uri = '/api/project/create'; // Endpoint Backend + // const uri = '/api/project/create'; // Endpoint Backend - this.generalService.postRequest(uri, projectData).subscribe({ - next: (result: any) => { - // ปิด Loading ที่ลูก - if (this.mainProjectAdd) this.mainProjectAdd.isLoading = false; + // this.generalService.postRequest(uri, projectData).subscribe({ + // next: (result: any) => { + // // ปิด Loading ที่ลูก + // if (this.mainProjectAdd) this.mainProjectAdd.isLoading = false; - this.generalService.trowApi(result); + // this.generalService.trowApi(result); - if (result.code === '200') { - // สำเร็จ -> กลับไปหน้ารายการ - this.router.navigate(['/main/project']); - } - }, - error: (error: any) => { - // Error -> ปิด Loading ที่ลูก - if (this.mainProjectAdd) this.mainProjectAdd.isLoading = false; + // if (result.code === '200') { + // // สำเร็จ -> กลับไปหน้ารายการ + // this.router.navigate(['/main/project']); + // } + // }, + // error: (error: any) => { + // // Error -> ปิด Loading ที่ลูก + // if (this.mainProjectAdd) this.mainProjectAdd.isLoading = false; - this.generalService.trowApi(error); - } - }); - } + // this.generalService.trowApi(error); + // } + // }); + // } // ฟังก์ชันดึงข้อมูลโครงการ และ update state onSearchPrj(): void { @@ -93,4 +93,34 @@ export class MainProjectContent implements OnInit { onCancelProject(): void { this.router.navigate(['/main/project']); } + + onProjectAddSave(value : FormData) { + let formData = value; + // const formData = new FormData(); + + // // 1. ใส่ Text Fields (Key ต้องตรงกับหลังบ้านที่ req.body รับ) + // formData.append('prjnam', this.form.get('prjnam').value); + // formData.append('prjwntbdg', this.form.get('prjwntbdg').value); + // formData.append('typ', 'prj'); // สำคัญ! ตาม Logic ย้ายไฟล์ + + // 2. ใส่ไฟล์ (รองรับหลายไฟล์) + // สมมติ this.selectedFiles เป็น Array ของ File Object + // for (let file of this.selectedFiles) { + // formData.append('prjdoc', file); // Key ต้องชื่อ 'prjdoc' ตาม Middleware + // } + + // 3. ส่งเข้า Service (ไม่ต้องทำอะไรเพิ่ม มันจะ detect FormData เอง) + this.generalService.postRequest('/api/ttc/projectadd', formData).subscribe({ + next: (res) => { + this.generalService.trowApi(res); + }, + error: (err) => { + this.generalService.trowApi(err); + }, + complete: () => { + this.onSearchPrj(); + this.router.navigate(['/main/project']); + } + }); + } } diff --git a/ng-ttc-frontend/src/app/services/generalservice.ts b/ng-ttc-frontend/src/app/services/generalservice.ts index 3be2af2..ee6ac04 100644 --- a/ng-ttc-frontend/src/app/services/generalservice.ts +++ b/ng-ttc-frontend/src/app/services/generalservice.ts @@ -10,48 +10,56 @@ import { environment } from '../../environments/environment'; }) export class GeneralService { - private baseUrl = environment.apiBaseUrl; private mode = environment.production; - constructor( private http: HttpClient, private toastr: ToastrService ) {} - // Default header - private getHttpOptions() { + // ✅ แก้ไข 1: รับ parameter เพื่อกำหนดว่าต้องใส่ Content-Type: application/json หรือไม่ + private getHttpOptions(isJson: boolean = true) { const token = localStorage.getItem('access_token'); - const headers = new HttpHeaders({ - 'Content-Type': 'application/json', + + let headersConfig: any = { ...(token ? { 'Authorization': `Bearer ${token}` } : {}) - }); + }; + + // ถ้าเป็น JSON ให้ใส่ Header แต่ถ้าเป็น FormData ห้ามใส่ (Browser จะจัดการเอง) + if (isJson) { + headersConfig['Content-Type'] = 'application/json'; + } + + const headers = new HttpHeaders(headersConfig); return { headers }; } - // Log ต้นแบบ - // private logRequest(method: string, url: string, body?: any) { - // if (this.mode === 'development') { - // console.log(`📡 [${method}] ${url}`, body || ''); - // } - // } - - - // Helper: wrap body ให้มี request ครอบเสมอ + // Helper: wrap body ให้มี request ครอบเสมอ (ใช้เฉพาะ JSON) private wrapRequestBody(body: any): any { - // ถ้ามี request อยู่แล้ว จะไม่ซ้ำ if (body && body.request) { return body; } return { request: body ?? {} }; } - // POST Request + // แก้ไข 2: ปรับ POST Request ให้รองรับ FormData postRequest(uri: string, body: any): Observable { - const payload = this.wrapRequestBody(body); + let payload: any; + let isJson = true; + + // เช็คว่าเป็น FormData หรือไม่? + if (body instanceof FormData) { + payload = body; // ส่งไปตรงๆ ไม่ต้องครอบ request + isJson = false; // บอก getHttpOptions ว่าไม่ต้องใส่ JSON Header + } else { + payload = this.wrapRequestBody(body); // ทำงานแบบเดิม + isJson = true; + } + const fullUrl = `${this.baseUrl}${uri}`; - return this.http.post(fullUrl, payload, this.getHttpOptions()).pipe( + + return this.http.post(fullUrl, payload, this.getHttpOptions(isJson)).pipe( map((res: any) => res), catchError((error: any) => { const response = error?.error; @@ -66,10 +74,37 @@ export class GeneralService { ); } - // GET Request + + // ตัวอย่างใน Component ที่จะ Upload +// onSave() { +// const formData = new FormData(); + +// // 1. ใส่ Text Fields (Key ต้องตรงกับหลังบ้านที่ req.body รับ) +// formData.append('prjnam', this.form.get('prjnam').value); +// formData.append('prjwntbdg', this.form.get('prjwntbdg').value); +// formData.append('typ', 'prj'); // สำคัญ! ตาม Logic ย้ายไฟล์ + +// // 2. ใส่ไฟล์ (รองรับหลายไฟล์) +// // สมมติ this.selectedFiles เป็น Array ของ File Object +// for (let file of this.selectedFiles) { +// formData.append('prjdoc', file); // Key ต้องชื่อ 'prjdoc' ตาม Middleware +// } + +// // 3. ส่งเข้า Service (ไม่ต้องทำอะไรเพิ่ม มันจะ detect FormData เอง) +// this.generalService.postRequest('/ttc/projectadd', formData).subscribe({ +// next: (res) => { +// this.generalService.trowApi(res); +// }, +// error: (err) => { +// this.generalService.trowApi(err); +// } +// }); +// } + + // GET Request (เหมือนเดิม) getRequest(uri: string): Observable { const fullUrl = `${this.baseUrl}${uri}`; - return this.http.get(fullUrl, this.getHttpOptions()).pipe( + return this.http.get(fullUrl, this.getHttpOptions(true)).pipe( // default true map((res: any) => res), catchError((error: any) => { const response = error?.error; @@ -79,15 +114,25 @@ export class GeneralService { code: response?.code ?? '500', message: response?.message ?? 'Internal Server Error', message_th: response?.message_th ?? 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์' - })); }) + })); + }) ); } - // PUT Request + // PUT Request (ปรับเหมือน POST เผื่ออนาคตใช้ Edit รูป) putRequest(uri: string, body: any): Observable { - const payload = this.wrapRequestBody(body); + let payload: any; + let isJson = true; + + if (body instanceof FormData) { + payload = body; + isJson = false; + } else { + payload = this.wrapRequestBody(body); + } + const fullUrl = `${this.baseUrl}${uri}`; - return this.http.put(fullUrl, payload, this.getHttpOptions()).pipe( + return this.http.put(fullUrl, payload, this.getHttpOptions(isJson)).pipe( map((res: any) => res), catchError((error: any) => { console.error('❌ [PUT Request Error]:', error); @@ -96,11 +141,11 @@ export class GeneralService { ); } - // DELETE Request + // DELETE Request (เหมือนเดิม) deleteRequest(uri: string, body?: any): Observable { const payload = this.wrapRequestBody(body); const fullUrl = `${this.baseUrl}${uri}`; - return this.http.delete(fullUrl, { ...this.getHttpOptions(), body: payload }).pipe( + return this.http.delete(fullUrl, { ...this.getHttpOptions(true), body: payload }).pipe( map((res: any) => res), catchError((error: any) => { console.error('❌ [DELETE Request Error]:', error); @@ -109,41 +154,29 @@ export class GeneralService { ); } - // showToast(type: 'success' | 'error' | 'warning' | 'info', message: string, title?: string) { - // const options = { - // positionClass: 'toast-top-right', - // timeOut: 3000, - // progressBar: true, - // progressAnimation: 'decreasing', - // toastClass: 'ngx-toastr bg-white bg-opacity-90 shadow-lg border' - // }; - // this.toastr[type](message, title || type.toUpperCase(), options); - // } + trowApi(result: any){ + const code = result?.code ?? 500; + const msg = result?.message ?? 'unknow'; + const msgTh = result?.message_th ?? 'unknow'; - - trowApi(result: any){ - const code = result?.code ?? 500; - const msg = result?.message ?? 'unknow'; - const msgTh = result?.message_th ?? 'unknow'; - - if(code == 200){ - this.toastr.success(`${msgTh || msg}`,'success',{ - positionClass: 'toast-top-right', - timeOut: 2500, - progressBar: true, - progressAnimation: 'decreasing', - toastClass: - 'ngx-toastr success-toast bg-white bg-opacity-90 text-green-700 shadow-lg' - }); - } else { - this.toastr.error(`${msgTh || msg}`,'error',{ - positionClass: 'toast-top-right', - timeOut: 3500, - progressBar: true, - progressAnimation: 'decreasing', - toastClass: - 'ngx-toastr error-toast bg-white bg-opacity-90 text-red-700 shadow-lg' - }); + if(code == 200){ + this.toastr.success(`${msgTh || msg}`,'success',{ + positionClass: 'toast-top-right', + timeOut: 2500, + progressBar: true, + progressAnimation: 'decreasing', + toastClass: + 'ngx-toastr success-toast bg-white bg-opacity-90 text-green-700 shadow-lg' + }); + } else { + this.toastr.error(`${msgTh || msg}`,'error',{ + positionClass: 'toast-top-right', + timeOut: 3500, + progressBar: true, + progressAnimation: 'decreasing', + toastClass: + 'ngx-toastr error-toast bg-white bg-opacity-90 text-red-700 shadow-lg' + }); + } } - } }