Commit 91bc8256 by sawit

ปรับ input จองห้องประชุม

parent 559c22d0
...@@ -160,30 +160,35 @@ ...@@ -160,30 +160,35 @@
type="number" type="number"
min="0" min="0"
formControlName="attendeeCount" formControlName="attendeeCount"
[value]="attendees.length" class="w-full px-3 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200 bg-gray-100"
class="w-full px-3 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200"> readonly>
</div> </div>
<!-- Add Attendees --> <!-- Add Attendees -->
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">เพิ่มผู้เข้าร่วม</label> <label class="block text-sm font-medium text-gray-700 mb-2">
<div class="border border-gray-300 rounded-lg p-3 min-h-[42px]"> <i class="ri-group-line mr-2 text-blue-500"></i>
<mat-chip-grid #chipList aria-label="Attendees" class="w-full"> ผู้เข้าร่วมประชุม
<mat-chip-row *ngFor="let a of attendees" [removable]="true" (removed)="removeAttendee(a)" </label>
class="bg-blue-100 text-blue-800 text-sm px-2 py-1 rounded-full mr-2 mb-2 inline-flex items-center"> <div class="relative">
{{ a }} <ejs-multiselect
<button matChipRemove aria-label="remove" class="ml-1 text-blue-600 hover:text-blue-800"> id='attendee-multiselect'
<i class="ri-close-line text-sm"></i> formControlName="attendees"
</button> [dataSource]="allEmployees"
</mat-chip-row> [fields]="employeeFields"
<input [placeholder]="employeeWatermark"
class="border-0 outline-none w-full text-sm" mode="Box"
placeholder="พิมพ์อีเมลแล้วกด Enter" [allowFiltering]="true"
[matChipInputFor]="chipList" cssClass="w-full e-outline custom-multiselect">
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" </ejs-multiselect>
(matChipInputTokenEnd)="addAttendee($event)" /> <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
</mat-chip-grid> <i class="ri-user-add-line text-gray-400"></i>
</div>
</div> </div>
<p class="text-xs text-gray-500 mt-1">
<i class="ri-information-line mr-1"></i>
เลือกผู้เข้าร่วมประชุมจากรายชื่อพนักงาน
</p>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -646,6 +646,231 @@ ...@@ -646,6 +646,231 @@
} }
} }
// Custom Multiselect Styling
::ng-deep {
.custom-multiselect {
.e-multiselect {
border: 2px solid #e5e7eb;
border-radius: 12px;
padding: 12px 16px;
font-size: 14px;
font-weight: 500;
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
min-height: 48px;
&:hover {
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
transform: translateY(-1px);
}
&:focus-within {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
outline: none;
}
.e-multiselect-wrapper {
.e-multiselect-input {
font-size: 14px;
color: #374151;
font-weight: 500;
padding: 0;
margin: 0;
background: transparent;
border: none;
outline: none;
&::placeholder {
color: #9ca3af;
font-weight: 400;
}
}
.e-multiselect-chip {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
border: none;
border-radius: 20px;
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
margin: 2px 4px 2px 0;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.4);
}
.e-chip-close {
color: white;
opacity: 0.8;
font-size: 14px;
margin-left: 6px;
&:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
padding: 2px;
}
}
}
.e-multiselect-arrow {
color: #6b7280;
font-size: 16px;
transition: all 0.3s ease;
&:hover {
color: #3b82f6;
transform: scale(1.1);
}
}
}
}
// Dropdown styling
.e-multiselect-dropdown {
border: 2px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
background: white;
overflow: hidden;
margin-top: 4px;
.e-multiselect-list {
.e-list-item {
padding: 12px 16px;
font-size: 14px;
font-weight: 500;
color: #374151;
border-bottom: 1px solid #f3f4f6;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 12px;
&:hover {
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
color: #1d4ed8;
transform: translateX(4px);
}
&.e-active {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
font-weight: 600;
}
.e-checkbox-wrapper {
.e-checkbox {
width: 18px;
height: 18px;
border: 2px solid #d1d5db;
border-radius: 4px;
background: white;
transition: all 0.2s ease;
&:checked {
background: #3b82f6;
border-color: #3b82f6;
}
}
}
.e-list-text {
flex: 1;
font-weight: 500;
}
}
.e-list-item:last-child {
border-bottom: none;
}
}
// Search box styling
.e-multiselect-search {
padding: 12px 16px;
border-bottom: 2px solid #f3f4f6;
background: #f9fafb;
.e-multiselect-searchbox {
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
font-weight: 500;
background: white;
transition: all 0.3s ease;
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
outline: none;
}
&::placeholder {
color: #9ca3af;
font-weight: 400;
}
}
}
}
// Loading state
.e-multiselect-loading {
.e-multiselect-wrapper {
.e-multiselect-arrow {
animation: spin 1s linear infinite;
}
}
}
}
// Animation keyframes
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// Focus states
.custom-multiselect.e-focused {
.e-multiselect {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
}
// Error state
.custom-multiselect.e-error {
.e-multiselect {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
}
// Disabled state
.custom-multiselect.e-disabled {
.e-multiselect {
background: #f9fafb;
border-color: #e5e7eb;
color: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
}
}
}
// Responsive Design // Responsive Design
@media (max-width: 768px) { @media (max-width: 768px) {
.page-header .header-content { .page-header .header-content {
...@@ -670,4 +895,22 @@ ...@@ -670,4 +895,22 @@
margin: 1rem; margin: 1rem;
width: calc(100% - 2rem); width: calc(100% - 2rem);
} }
// Mobile multiselect adjustments
::ng-deep .custom-multiselect {
.e-multiselect {
padding: 10px 14px;
min-height: 44px;
}
.e-multiselect-dropdown {
margin-top: 2px;
border-radius: 8px;
.e-multiselect-list .e-list-item {
padding: 10px 14px;
font-size: 13px;
}
}
}
} }
...@@ -25,11 +25,12 @@ import { Observable } from 'rxjs'; ...@@ -25,11 +25,12 @@ import { Observable } from 'rxjs';
// Syncfusion Schedule imports // Syncfusion Schedule imports
import { ScheduleModule, View, EventSettingsModel, DayService, WeekService, WorkWeekService, MonthService, AgendaService, ResizeService, DragAndDropService } from '@syncfusion/ej2-angular-schedule'; import { ScheduleModule, View, EventSettingsModel, DayService, WeekService, WorkWeekService, MonthService, AgendaService, ResizeService, DragAndDropService } from '@syncfusion/ej2-angular-schedule';
import { DateTimePickerModule } from '@syncfusion/ej2-angular-calendars'; import { DateTimePickerModule } from '@syncfusion/ej2-angular-calendars';
import { DropDownListModule } from '@syncfusion/ej2-angular-dropdowns'; import { DropDownListModule, MultiSelectModule } from '@syncfusion/ej2-angular-dropdowns';
import { L10n, setCulture } from '@syncfusion/ej2-base'; import { L10n, setCulture } from '@syncfusion/ej2-base';
import { MeetingBookingService } from '../services/meeting-booking.service'; import { MeetingBookingService } from '../services/meeting-booking.service';
import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from '../models/meeting-booking.model'; import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from '../models/meeting-booking.model';
import { PermissionModel2 } from '../models/permission/permission.model';
@Component({ @Component({
selector: 'app-meeting-booking', selector: 'app-meeting-booking',
...@@ -59,7 +60,8 @@ import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from ...@@ -59,7 +60,8 @@ import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from
// Syncfusion modules // Syncfusion modules
ScheduleModule, ScheduleModule,
DateTimePickerModule, DateTimePickerModule,
DropDownListModule DropDownListModule,
MultiSelectModule
], ],
providers: [ providers: [
DayService, DayService,
...@@ -85,11 +87,12 @@ export class MeetingBookingComponent implements OnInit { ...@@ -85,11 +87,12 @@ export class MeetingBookingComponent implements OnInit {
isLoading = false; isLoading = false;
bookingForm: FormGroup; bookingForm: FormGroup;
attendees: string[] = []; allEmployees: (PermissionModel2 & { displayName: string })[] = [];
public employeeFields: Object = { text: 'displayName' };
public employeeWatermark: string = 'ค้นหาและเลือกผู้เข้าร่วม';
displayedColumns: string[] = ['title', 'room', 'startTime', 'endTime', 'organizer', 'status', 'actions'];
separatorKeysCodes: number[] = [13, 188]; // ENTER and COMMA key codes displayedColumns: string[] = ['title', 'room', 'startTime', 'endTime', 'organizer', 'status', 'actions'];
// Syncfusion Schedule properties // Syncfusion Schedule properties
public selectedDate_schedule: Date = new Date(); public selectedDate_schedule: Date = new Date();
...@@ -134,10 +137,16 @@ export class MeetingBookingComponent implements OnInit { ...@@ -134,10 +137,16 @@ export class MeetingBookingComponent implements OnInit {
description: [''], description: [''],
startDateTime: ['', Validators.required], startDateTime: ['', Validators.required],
endDateTime: ['', Validators.required], endDateTime: ['', Validators.required],
startTime: ['', Validators.required],
endTime: ['', Validators.required],
attendees: [[]], attendees: [[]],
attendeeCount: [0, [Validators.min(0)]] attendeeCount: [0, [Validators.min(0)]]
}); });
this.bookingForm.get('attendees')?.valueChanges.subscribe(val => {
this.bookingForm.get('attendeeCount')?.setValue(val?.length || 0);
});
this.statistics$ = this.meetingBookingService.getBookingStatistics( this.statistics$ = this.meetingBookingService.getBookingStatistics(
new Date(new Date().setMonth(new Date().getMonth() - 1)), new Date(new Date().setMonth(new Date().getMonth() - 1)),
new Date() new Date()
...@@ -149,6 +158,30 @@ export class MeetingBookingComponent implements OnInit { ...@@ -149,6 +158,30 @@ export class MeetingBookingComponent implements OnInit {
this.loadTimeSlots(); this.loadTimeSlots();
this.loadScheduleData(); this.loadScheduleData();
this.loadSampleData(); // เพิ่มข้อมูลตัวอย่าง this.loadSampleData(); // เพิ่มข้อมูลตัวอย่าง
this.loadEmployees();
}
loadEmployees(): void {
this.meetingBookingService.getAllEmployeesMini().subscribe(response => {
// The type `PermissionModel` suggests a direct array, but the runtime error
// indicates an object is being returned. This is common for paginated APIs
// where the array is in a 'content' property.
let employees: PermissionModel2[] = [];
if (response && (response as any).content && Array.isArray((response as any).content)) {
employees = (response as any).content;
} else if (Array.isArray(response)) {
// Handle the case where the API correctly returns a direct array.
employees = response;
} else {
console.error('Received employee data is not in a recognized format (Array or { content: Array }).', response);
}
// Add a displayName property for the multiselect component
this.allEmployees = employees.map(emp => ({
...emp,
displayName: `${emp.fname} ${emp.lname}`
}));
});
} }
private setupLocale(): void { private setupLocale(): void {
...@@ -204,39 +237,34 @@ export class MeetingBookingComponent implements OnInit { ...@@ -204,39 +237,34 @@ export class MeetingBookingComponent implements OnInit {
} }
} }
addAttendee(event: MatChipInputEvent): void {
const value = (event.value || '').trim();
if (value) {
this.attendees.push(value);
this.bookingForm.patchValue({ attendees: this.attendees, attendeeCount: this.attendees.length });
}
event.chipInput!.clear();
}
removeAttendee(attendee: string): void {
const index = this.attendees.indexOf(attendee);
if (index >= 0) {
this.attendees.splice(index, 1);
this.bookingForm.patchValue({ attendees: this.attendees, attendeeCount: this.attendees.length });
}
}
createBooking(): void { createBooking(): void {
if (this.bookingForm.valid) { if (this.bookingForm.valid) {
const formValue = this.bookingForm.value; const formValue = this.bookingForm.value;
// Combine Date and Time
const startDate = new Date(formValue.startDateTime);
const [startHours, startMinutes] = formValue.startTime.split(':');
startDate.setHours(startHours, startMinutes, 0, 0);
const endDate = new Date(formValue.endDateTime);
const [endHours, endMinutes] = formValue.endTime.split(':');
endDate.setHours(endHours, endMinutes, 0, 0);
const selectedAttendees: PermissionModel2[] = formValue.attendees || [];
const booking = { const booking = {
roomId: formValue.roomId, roomId: formValue.roomId,
roomName: '', // Will be filled by service roomName: '', // Will be filled by service
title: formValue.title, title: formValue.title,
description: formValue.description, description: formValue.description,
startDateTime: formValue.startDateTime, startDateTime: startDate, // Use combined value
endDateTime: formValue.endDateTime, endDateTime: endDate, // Use combined value
organizerId: 'current-user', // From auth service organizerId: 'current-user', // From auth service
organizerName: 'Current User', // From auth service organizerName: 'Current User', // From auth service
attendees: this.attendees.map(email => ({ attendees: selectedAttendees.map(emp => ({
userId: '', userId: emp.employeeId,
userName: email, userName: `${emp.fname} ${emp.lname}`,
email: email, email: '', // email is not available in the employee mini data
status: 'pending' as const status: 'pending' as const
})), })),
status: 'pending' as const status: 'pending' as const
...@@ -246,7 +274,6 @@ export class MeetingBookingComponent implements OnInit { ...@@ -246,7 +274,6 @@ export class MeetingBookingComponent implements OnInit {
next: () => { next: () => {
this.snackBar.open('จองห้องประชุมสำเร็จ', 'ปิด', { duration: 3000 }); this.snackBar.open('จองห้องประชุมสำเร็จ', 'ปิด', { duration: 3000 });
this.bookingForm.reset(); this.bookingForm.reset();
this.attendees = [];
this.loadTimeSlots(); this.loadTimeSlots();
}, },
error: (error) => { error: (error) => {
......
...@@ -12,6 +12,8 @@ import { ...@@ -12,6 +12,8 @@ import {
BookingFilter, BookingFilter,
BookingStatistics BookingStatistics
} from '../models/meeting-booking.model'; } from '../models/meeting-booking.model';
import { environment } from '../../../environments/environment';
import { PermissionModel } from '../models/permission/permission.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
...@@ -25,6 +27,12 @@ export class MeetingBookingService { ...@@ -25,6 +27,12 @@ export class MeetingBookingService {
private dataUrl = 'assets/data/meeting-booking.json'; private dataUrl = 'assets/data/meeting-booking.json';
lang: string = "";
private readonly baseUrl: string = environment.url; // portal api base
private readonly hrplusUrl: string = 'https://hrplus.myhr.co.th/plus';
private readonly employeePath = this.hrplusUrl + '/employee';
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
this.loadInitialData(); this.loadInitialData();
} }
...@@ -360,4 +368,9 @@ export class MeetingBookingService { ...@@ -360,4 +368,9 @@ export class MeetingBookingService {
} }
]; ];
} }
getAllEmployeesMini(): Observable<PermissionModel> {
const url = `${this.employeePath}/workings/mini?page=0&size=500`;
return this.http.get<PermissionModel>(url);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment