new project add
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m50s
Build Docker Image / Restart Docker Compose (push) Successful in 0s

This commit is contained in:
x2Skyz
2025-11-28 19:28:41 +07:00
parent 012d590f40
commit b8c6512f8a
2 changed files with 274 additions and 174 deletions

View File

@@ -1,175 +1,183 @@
<div class="flex items-start justify-center p-4">
<div class="bg-white p-6 rounded-xl shadow-lg w-full max-w-2xl">
<h2 class="text-2xl font-bold text-gray-800 mb-6 border-b pb-3">สร้างโครงการใหม่</h2>
<div class="space-y-4">
<div class="border rounded-xl overflow-hidden shadow-sm"
[ngClass]="{
'border-red-500': currentStep === 1,
'border-gray-200': currentStep !== 1
}">
<div class="p-4 flex items-center justify-between cursor-pointer"
[ngClass]="{
'bg-red-50 text-red-700': currentStep === 1,
'bg-gray-50 text-gray-700 hover:bg-gray-100': currentStep !== 1
}"
(click)="goToStep(1)">
<div class="flex items-center space-x-3">
<div class="w-7 h-7 rounded-full flex items-center justify-center text-sm font-bold"
[ngClass]="{
'bg-red-500 text-white': currentStep === 1,
'bg-gray-300 text-gray-700': currentStep !== 1
}">
1
</div>
<h3 class="text-lg font-semibold">1. กรอกข้อมูลโครงการ</h3>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"
[ngClass]="{'rotate-180': currentStep === 1}">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
<div class="p-4 pt-0 transition-all duration-300" *ngIf="currentStep === 1">
<div class="border-t border-gray-200 pt-4 space-y-4">
<div>
<label for="projectName" class="block text-sm font-medium text-gray-700 mb-1">ชื่อโครงการ</label>
<input type="text" id="projectName" name="projectName"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-red-500 focus:border-red-500 shadow-sm"
placeholder="ป้อนชื่อโครงการ">
</div>
<div>
<label for="budgetType" class="block text-sm font-medium text-gray-700 mb-1">ประเภทงบประมาณ</label>
<select id="budgetType" name="budgetType"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-red-500 focus:border-red-500 shadow-sm">
<option value="">-- เลือกประเภท --</option>
<option value="operation">งบดำเนินงาน</option>
<option value="investment">งบลงทุน</option>
<option value="research">งบวิจัย</option>
</select>
</div>
<div class="flex justify-end pt-2">
<button (click)="goToStep(2)"
class="px-6 py-2 bg-red-500 text-white font-semibold rounded-lg shadow-md hover:bg-red-600 transition">
บันทึกและต่อไป
</button>
</div>
</div>
</div>
</div>
<div class="border rounded-xl overflow-hidden shadow-sm"
[ngClass]="{
'border-red-500': currentStep === 2,
'border-gray-200': currentStep !== 2
}">
<div class="p-4 flex items-center justify-between cursor-pointer"
[ngClass]="{
'bg-red-50 text-red-700': currentStep === 2,
'bg-gray-50 text-gray-700 hover:bg-gray-100': currentStep !== 2
}"
(click)="goToStep(2)">
<div class="flex items-center space-x-3">
<div class="w-7 h-7 rounded-full flex items-center justify-center text-sm font-bold"
[ngClass]="{
'bg-red-500 text-white': currentStep === 2,
'bg-gray-300 text-gray-700': currentStep !== 2
}">
2
</div>
<h3 class="text-lg font-semibold">2. แนบเอกสารอ้างอิง</h3>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"
[ngClass]="{'rotate-180': currentStep === 2}">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
<div class="p-4 pt-0" *ngIf="currentStep === 2">
<div class="border-t border-gray-200 pt-4 space-y-4">
<p class="text-gray-600">โปรดแนบไฟล์ที่เกี่ยวข้อง (เช่น ใบเสนอราคา, แผนผัง)</p>
<div class="h-24 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center text-gray-500">
คลิกหรือลากไฟล์มาวางที่นี่
</div>
<div class="flex justify-between pt-2">
<button (click)="goToStep(1)"
class="px-6 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50">
ย้อนกลับ
</button>
<button (click)="goToStep(3)"
class="px-6 py-2 bg-red-500 text-white font-semibold rounded-lg shadow-md hover:bg-red-600 transition">
ตรวจสอบและส่ง
</button>
</div>
</div>
</div>
</div>
<div class="border rounded-xl overflow-hidden shadow-sm"
[ngClass]="{
'border-red-500': currentStep === 3,
'border-gray-200': currentStep !== 3
}">
<div class="p-4 flex items-center justify-between cursor-pointer"
[ngClass]="{
'bg-red-50 text-red-700': currentStep === 3,
'bg-gray-50 text-gray-700 hover:bg-gray-100': currentStep !== 3
}"
(click)="goToStep(3)">
<div class="flex items-center space-x-3">
<div class="w-7 h-7 rounded-full flex items-center justify-center text-sm font-bold"
[ngClass]="{
'bg-red-500 text-white': currentStep === 3,
'bg-gray-300 text-gray-700': currentStep !== 3
}">
3
</div>
<h3 class="text-lg font-semibold">3. สรุปและส่งโครงการ</h3>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"
[ngClass]="{'rotate-180': currentStep === 3}">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
<div class="p-4 pt-0" *ngIf="currentStep === 3">
<div class="border-t border-gray-200 pt-4 space-y-4">
<p class="text-base font-medium text-gray-800">ตรวจสอบข้อมูลทั้งหมดก่อนยืนยันการส่ง</p>
<ul class="text-sm text-gray-600 space-y-1 bg-gray-50 p-3 rounded-lg">
<li>**ชื่อโครงการ:** โครงการปรับปรุงระบบน้ำ</li>
<li>**ประเภทงบ:** งบดำเนินงาน</li>
<li>**จำนวนเงิน:** 50,000 บาท</li>
<li>**สถานะเอกสาร:** แนบครบ 2 ไฟล์</li>
</ul>
<div class="flex justify-end pt-2">
<button (click)="goToStep(2)"
class="px-6 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 mr-2">
ย้อนกลับ
</button>
<button type="submit"
class="px-6 py-2 bg-green-500 text-white font-semibold rounded-lg shadow-md hover:bg-green-600 transition">
ยืนยันและส่งโครงการ
</button>
</div>
</div>
</div>
</div>
<div class="flex items-start justify-center p-6">
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-3xl border border-gray-100">
<div class="text-center mb-8">
<h2 class="text-3xl font-bold text-gray-800">สร้างโครงการใหม่</h2>
<p class="text-gray-500 mt-2">กรอกรายละเอียดเพื่อขออนุมัติงบประมาณ</p>
</div>
<div class="space-y-6">
<div [formGroup]="projectForm">
<div class="transition-all duration-300 ease-in-out border rounded-xl overflow-hidden"
[ngClass]="{
'border-red-500 shadow-md ring-2 ring-red-100 bg-white': currentStep === 1,
'border-gray-200 bg-gray-50 opacity-60': currentStep !== 1
}">
<div class="p-4 flex items-center justify-between"
[ngClass]="{ 'bg-red-50 text-red-700': currentStep === 1, 'text-gray-500': currentStep !== 1 }">
<div class="flex items-center space-x-4">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold shadow-sm transition-colors"
[ngClass]="{ 'bg-red-500 text-white': currentStep === 1, 'bg-gray-300 text-gray-600': currentStep !== 1 }">
1
</div>
<h3 class="text-lg font-semibold">ข้อมูลโครงการ</h3>
</div>
<i *ngIf="currentStep > 1" class="fas fa-check-circle text-green-500 text-xl"></i>
</div>
<div class="p-6 pt-2 bg-white" *ngIf="currentStep === 1">
<div class="space-y-5">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">ชื่อโครงการ <span class="text-red-500">*</span></label>
<input type="text" formControlName="projectName"
class="w-full p-3 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 outline-none transition-all"
[ngClass]="{'border-red-500 bg-red-50': f['projectName'].invalid && f['projectName'].touched}"
placeholder="ระบุชื่อโครงการของคุณ">
<p *ngIf="f['projectName'].invalid && f['projectName'].touched" class="text-red-500 text-xs mt-1">กรุณาระบุชื่อโครงการ</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">จำนวนเงิน (บาท) <span class="text-red-500">*</span></label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500">฿</span>
<input type="number" formControlName="budgetAmount"
class="w-full p-3 pl-8 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 outline-none transition-all"
[ngClass]="{'border-red-500 bg-red-50': f['budgetAmount'].invalid && f['budgetAmount'].touched}"
placeholder="0.00">
</div>
<p *ngIf="f['budgetAmount'].invalid && f['budgetAmount'].touched" class="text-red-500 text-xs mt-1">กรุณาระบุจำนวนเงิน</p>
</div>
<div class="flex justify-end pt-4">
<button (click)="goToStep(2)" class="px-8 py-2.5 bg-gradient-to-r from-red-500 to-red-600 text-white font-semibold rounded-lg shadow-md hover:shadow-lg hover:from-red-600 hover:to-red-700 transform hover:-translate-y-0.5 transition-all duration-200">
ถัดไป <i class="fas fa-arrow-right ml-2"></i>
</button>
</div>
</div>
</div>
</div>
<div class="transition-all duration-300 ease-in-out border rounded-xl overflow-hidden mt-4"
[ngClass]="{
'border-red-500 shadow-md ring-2 ring-red-100 bg-white': currentStep === 2,
'border-gray-200 bg-gray-50 opacity-60': currentStep !== 2
}">
<div class="p-4 flex items-center justify-between"
[ngClass]="{ 'bg-red-50 text-red-700': currentStep === 2, 'text-gray-500': currentStep !== 2 }">
<div class="flex items-center space-x-4">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold shadow-sm transition-colors"
[ngClass]="{ 'bg-red-500 text-white': currentStep === 2, 'bg-gray-300 text-gray-600': currentStep !== 2 }">
2
</div>
<h3 class="text-lg font-semibold">เอกสารอ้างอิง</h3>
</div>
<i *ngIf="currentStep > 2" class="fas fa-check-circle text-green-500 text-xl"></i>
</div>
<div class="p-6 pt-2 bg-white" *ngIf="currentStep === 2">
<div class="space-y-4">
<p class="text-sm text-gray-600 bg-blue-50 p-3 rounded-lg border border-blue-100 flex items-start">
<i class="fas fa-info-circle text-blue-500 mt-0.5 mr-2"></i>
สามารถแนบไฟล์ PDF, JPG หรือ PNG ที่เกี่ยวข้องเพื่อประกอบการพิจารณา
</p>
<div class="relative h-40 border-2 border-dashed border-gray-300 rounded-xl flex flex-col items-center justify-center text-gray-400 hover:bg-gray-50 hover:border-red-400 transition-all cursor-pointer group">
<input type="file" multiple (change)="onFileSelected($event)" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
<div class="group-hover:scale-110 transition-transform duration-200">
<i class="fas fa-cloud-upload-alt text-4xl mb-2 text-gray-300 group-hover:text-red-400"></i>
</div>
<span class="font-medium group-hover:text-gray-600">คลิกเพื่อเลือกไฟล์ หรือ ลากไฟล์มาวาง</span>
</div>
<div *ngIf="attachedFiles.length > 0" class="space-y-2 mt-4">
<div *ngFor="let file of attachedFiles; let i = index" class="flex justify-between items-center bg-gray-50 p-3 rounded-lg border border-gray-200 hover:shadow-sm transition-shadow">
<div class="flex items-center space-x-3 overflow-hidden">
<i class="fas fa-file-alt text-red-400 text-lg"></i>
<span class="text-sm text-gray-700 truncate max-w-[200px]">{{ file.name }}</span>
<span class="text-xs text-gray-400">({{ (file.size / 1024).toFixed(2) }} KB)</span>
</div>
<button (click)="removeFile(i)" class="text-gray-400 hover:text-red-500 transition-colors p-1 rounded-full hover:bg-red-50">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
<div class="flex justify-between pt-6 border-t border-gray-100 mt-4">
<button (click)="goToStep(1)" class="px-6 py-2.5 text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 font-medium transition-colors">
ย้อนกลับ
</button>
<button (click)="goToStep(3)" class="px-8 py-2.5 bg-gradient-to-r from-red-500 to-red-600 text-white font-semibold rounded-lg shadow-md hover:shadow-lg hover:from-red-600 hover:to-red-700 transform hover:-translate-y-0.5 transition-all duration-200">
ตรวจสอบข้อมูล <i class="fas fa-arrow-right ml-2"></i>
</button>
</div>
</div>
</div>
</div>
<div class="transition-all duration-300 ease-in-out border rounded-xl overflow-hidden mt-4"
[ngClass]="{
'border-red-500 shadow-md ring-2 ring-red-100 bg-white': currentStep === 3,
'border-gray-200 bg-gray-50 opacity-60': currentStep !== 3
}">
<div class="p-4 flex items-center justify-between"
[ngClass]="{ 'bg-red-50 text-red-700': currentStep === 3, 'text-gray-500': currentStep !== 3 }">
<div class="flex items-center space-x-4">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold shadow-sm transition-colors"
[ngClass]="{ 'bg-red-500 text-white': currentStep === 3, 'bg-gray-300 text-gray-600': currentStep !== 3 }">
3
</div>
<h3 class="text-lg font-semibold">ยืนยันการส่ง</h3>
</div>
</div>
<div class="p-6 pt-2 bg-white" *ngIf="currentStep === 3">
<div class="space-y-6">
<div class="text-center py-4">
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
<i class="fas fa-file-invoice text-2xl text-green-600"></i>
</div>
<h4 class="text-lg font-bold text-gray-800">ตรวจสอบความถูกต้อง</h4>
<p class="text-gray-500 text-sm">กรุณาตรวจสอบข้อมูลก่อนทำการส่งขออนุมัติ</p>
</div>
<div class="bg-gray-50 p-5 rounded-xl border border-gray-200 space-y-3">
<div class="flex justify-between border-b border-gray-200 pb-2">
<span class="text-gray-500 text-sm">ชื่อโครงการ</span>
<span class="font-semibold text-gray-800 text-right w-2/3">{{ f['projectName'].value || '-' }}</span>
</div>
<div class="flex justify-between border-b border-gray-200 pb-2">
<span class="text-gray-500 text-sm">จำนวนเงิน</span>
<span class="font-bold text-red-600 text-lg">{{ formatCurrency(f['budgetAmount'].value) }}</span>
</div>
<div class="flex justify-between pt-1">
<span class="text-gray-500 text-sm">เอกสารแนบ</span>
<span class="font-medium text-gray-800">
<i class="fas fa-paperclip text-gray-400 mr-1"></i> {{ attachedFiles.length }} ไฟล์
</span>
</div>
</div>
<div class="flex justify-between pt-4">
<button (click)="goToStep(2)" class="px-6 py-2.5 text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 font-medium transition-colors" [disabled]="isLoading">
ย้อนกลับ
</button>
<button (click)="onSubmit()" [disabled]="isLoading"
class="px-8 py-2.5 bg-green-500 text-white font-semibold rounded-lg shadow-md hover:bg-green-600 hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200 flex items-center disabled:opacity-70 disabled:cursor-not-allowed">
<span *ngIf="isLoading" class="mr-2 animate-spin"><i class="fas fa-circle-notch"></i></span>
<span *ngIf="!isLoading"><i class="fas fa-paper-plane mr-2"></i> ยืนยันและส่ง</span>
<span *ngIf="isLoading">กำลังส่ง...</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,4 +1,8 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { GeneralService } from '../../services/generalservice';
import { ToastrService } from 'ngx-toastr';
@Component({
selector: 'app-main-project-add',
@@ -6,11 +10,99 @@ import { Component } from '@angular/core';
templateUrl: './main-project-add.html',
styleUrl: './main-project-add.css',
})
export class MainProjectAdd {
currentStep: number = 1; // 1 = กรอกข้อมูล, 2 = แนบเอกสาร, 3 = สรุปและส่ง
export class MainProjectAdd implements OnInit {
currentStep: number = 1;
isLoading: boolean = false;
// ฟังก์ชันสำหรับเปลี่ยนขั้นตอน
projectForm!: FormGroup;
attachedFiles: any[] = [];
constructor(
private generalService: GeneralService,
private router: Router,
private toastr: ToastrService
) {}
ngOnInit(): void {
this.setupFormControl();
}
setupFormControl(): void {
this.projectForm = new FormGroup({
projectName: new FormControl('', [Validators.required, Validators.maxLength(200)]),
budgetAmount: new FormControl('', [Validators.required, Validators.min(1)])
});
}
// ฟังก์ชันเปลี่ยน Step (เรียกใช้จากปุ่มเท่านั้น)
goToStep(step: number): void {
// กรณีจะไป Step 2 ต้องผ่าน Validation Step 1 ก่อน
if (step === 2 && this.currentStep === 1) {
if (this.projectForm.invalid) {
this.projectForm.markAllAsTouched();
this.toastr.warning('กรุณากรอกข้อมูลให้ครบถ้วน', 'แจ้งเตือน');
return;
}
}
this.currentStep = step;
}
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];
const reader = new FileReader();
reader.onload = (e: any) => {
this.attachedFiles.push({
name: file.name,
size: file.size,
type: file.type,
content: e.target.result
});
};
reader.readAsDataURL(file);
}
}
}
removeFile(index: number): void {
this.attachedFiles.splice(index, 1);
}
get f() {
return this.projectForm.controls;
}
formatCurrency(amount: any): string {
if (!amount) return '0.00 บาท';
return new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB' }).format(Number(amount));
}
onSubmit(): void {
if (this.projectForm.invalid) return;
this.isLoading = true;
const uri = '/api/project/create';
const request = {
...this.projectForm.value,
files: this.attachedFiles
};
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
this.isLoading = false;
this.generalService.trowApi(result);
if (result.code === '200') {
this.router.navigate(['/main']);
}
},
error: (error: any) => {
this.isLoading = false;
this.generalService.trowApi(error);
}
});
}
}