Commit ca26828b by sawit

ตารางปฏิทิน และทะเบียนห้องประชุม

parent 66b5f4ce
...@@ -200,53 +200,6 @@ ...@@ -200,53 +200,6 @@
<p class="schedule-subtitle">ดูและจัดการการจองในรูปแบบปฏิทินแบบเต็มรูปแบบ</p> <p class="schedule-subtitle">ดูและจัดการการจองในรูปแบบปฏิทินแบบเต็มรูปแบบ</p>
</div> </div>
<!-- Monthly Calendar Grid -->
<div class="month-calendar">
<div class="month-toolbar">
<button mat-icon-button (click)="prevMonth()" aria-label="Previous Month">
<mat-icon>chevron_left</mat-icon>
</button>
<div class="month-title">{{ monthLabel() }}</div>
<button mat-icon-button (click)="nextMonth()" aria-label="Next Month">
<mat-icon>chevron_right</mat-icon>
</button>
<button mat-button (click)="today()">วันนี้</button>
</div>
<!-- Weekday headers -->
<div class="calendar-grid calendar-header" [ngStyle]="{display:'grid','grid-template-columns':'repeat(7, 1fr)',gap:'4px'}">
<div class="calendar-cell">อา</div>
<div class="calendar-cell"></div>
<div class="calendar-cell"></div>
<div class="calendar-cell"></div>
<div class="calendar-cell">พฤ</div>
<div class="calendar-cell"></div>
<div class="calendar-cell"></div>
</div>
<!-- Weeks -->
<div class="calendar-week" *ngFor="let week of monthWeeks">
<div class="calendar-grid" [ngStyle]="{display:'grid','grid-template-columns':'repeat(7, 1fr)',gap:'4px'}">
<div class="calendar-cell"
*ngFor="let day of week"
[class.outside-month]="!isCurrentMonth(day)"
[class.today]="isToday(day)">
<div class="cell-header">
<span class="date-number">{{ day.getDate() }}</span>
</div>
<div class="cell-events">
<div class="event-chip status-{{ e.Status }}"
*ngFor="let e of getBookingsForDate(day)"
(click)="onEventClick({ event: e })">
<span class="time" *ngIf="e.StartTime && e.EndTime">{{ e.StartTime | date:'HH:mm' }}-{{ e.EndTime | date:'HH:mm' }}</span>
<span class="title">{{ e.Subject }}</span>
<span class="count" *ngIf="e.AttendeesCount">({{ e.AttendeesCount }})</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Syncfusion Schedule Component --> <!-- Syncfusion Schedule Component -->
<div class="schedule-wrapper"> <div class="schedule-wrapper">
...@@ -257,16 +210,24 @@ ...@@ -257,16 +210,24 @@
[eventSettings]="eventSettings" [eventSettings]="eventSettings"
[showQuickInfo]="showQuickInfo" [showQuickInfo]="showQuickInfo"
[allowDragAndDrop]="allowDragAndDrop" [allowDragAndDrop]="allowDragAndDrop"
[showHeaderBar]="false" [showHeaderBar]="true"
[views]="scheduleViews"
[locale]="locale"
[enableRtl]="enableRtl"
[firstDayOfWeek]="firstDayOfWeek"
[timeFormat]="timeFormat"
[dateFormat]="dateFormat"
[workDays]="workDays"
[workHours]="workHours"
[height]="height"
[width]="width"
[cssClass]="cssClass"
(eventClick)="onEventClick($event)" (eventClick)="onEventClick($event)"
(eventCreate)="onEventCreate($event)" (eventCreate)="onEventCreate($event)"
(eventUpdate)="onEventUpdate($event)" (eventUpdate)="onEventUpdate($event)"
(eventDelete)="onEventDelete($event)" (eventDelete)="onEventDelete($event)"
(viewChange)="onViewChange($event)" (viewChange)="onViewChange($event)"
(dateChange)="onDateChange_schedule($event)" (dateChange)="onDateChange_schedule($event)">
height="700px"
width="100%"
cssClass="custom-schedule">
<!-- Event Template --> <!-- Event Template -->
<ng-template #eventTemplate let-data> <ng-template #eventTemplate let-data>
......
...@@ -18,6 +18,7 @@ import { MatDialogModule, MatDialog } from '@angular/material/dialog'; ...@@ -18,6 +18,7 @@ import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
...@@ -25,6 +26,7 @@ import { Observable } from 'rxjs'; ...@@ -25,6 +26,7 @@ import { Observable } from 'rxjs';
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 } from '@syncfusion/ej2-angular-dropdowns';
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';
...@@ -52,6 +54,7 @@ import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from ...@@ -52,6 +54,7 @@ import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from
MatTabsModule, MatTabsModule,
MatSnackBarModule, MatSnackBarModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatButtonToggleModule,
TranslateModule, TranslateModule,
// Syncfusion modules // Syncfusion modules
ScheduleModule, ScheduleModule,
...@@ -91,6 +94,20 @@ export class MeetingBookingComponent implements OnInit { ...@@ -91,6 +94,20 @@ export class MeetingBookingComponent implements OnInit {
// Syncfusion Schedule properties // Syncfusion Schedule properties
public selectedDate_schedule: Date = new Date(); public selectedDate_schedule: Date = new Date();
public currentView: View = 'Month'; public currentView: View = 'Month';
public scheduleViews: View[] = ['Month', 'Week', 'Day', 'Agenda'];
public locale: string = 'en';
public enableRtl: boolean = false;
public firstDayOfWeek: number = 0; // 0 = Sunday
public timeFormat: string = 'HH:mm';
public dateFormat: string = 'dd/MM/yyyy';
public workDays: number[] = [0, 1, 2, 3, 4, 5, 6]; // All days
public workHours: any = {
start: '08:00',
end: '18:00'
};
public height: string = '700px';
public width: string = '100%';
public cssClass: string = 'custom-schedule';
public eventSettings: EventSettingsModel = { public eventSettings: EventSettingsModel = {
dataSource: [] dataSource: []
}; };
...@@ -101,9 +118,6 @@ export class MeetingBookingComponent implements OnInit { ...@@ -101,9 +118,6 @@ export class MeetingBookingComponent implements OnInit {
// UI State properties // UI State properties
public showBookingForm: boolean = false; public showBookingForm: boolean = false;
// Monthly calendar grid state
public monthWeeks: Date[][] = [];
constructor( constructor(
private meetingBookingService: MeetingBookingService, private meetingBookingService: MeetingBookingService,
private fb: FormBuilder, private fb: FormBuilder,
...@@ -131,10 +145,21 @@ export class MeetingBookingComponent implements OnInit { ...@@ -131,10 +145,21 @@ export class MeetingBookingComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.setupLocale();
this.loadTimeSlots(); this.loadTimeSlots();
this.loadScheduleData(); this.loadScheduleData();
this.loadSampleData(); // เพิ่มข้อมูลตัวอย่าง this.loadSampleData(); // เพิ่มข้อมูลตัวอย่าง
this.buildMonthWeeks(this.selectedDate_schedule); }
private setupLocale(): void {
// Ensure locale is set before Schedule initializes
try {
// Try to set English locale first to avoid the error
setCulture('en');
console.log('Locale setup completed successfully with English');
} catch (error) {
console.error('Error setting up locale:', error);
}
} }
onDateChange(): void { onDateChange(): void {
...@@ -304,7 +329,6 @@ export class MeetingBookingComponent implements OnInit { ...@@ -304,7 +329,6 @@ export class MeetingBookingComponent implements OnInit {
}; };
console.log('Schedule data loaded:', this.scheduleData); console.log('Schedule data loaded:', this.scheduleData);
this.buildMonthWeeks(this.selectedDate_schedule);
}); });
} else { } else {
// ถ้าไม่มี bookings$ observable ให้ใช้ข้อมูลตัวอย่าง // ถ้าไม่มี bookings$ observable ให้ใช้ข้อมูลตัวอย่าง
...@@ -366,7 +390,6 @@ export class MeetingBookingComponent implements OnInit { ...@@ -366,7 +390,6 @@ export class MeetingBookingComponent implements OnInit {
onDateChange_schedule(args: any): void { onDateChange_schedule(args: any): void {
console.log('Date changed:', args); console.log('Date changed:', args);
this.selectedDate_schedule = args.selectedDate; this.selectedDate_schedule = args.selectedDate;
this.buildMonthWeeks(this.selectedDate_schedule);
} }
// ฟังก์ชันสำหรับโหลดข้อมูลตัวอย่าง // ฟังก์ชันสำหรับโหลดข้อมูลตัวอย่าง
...@@ -431,7 +454,6 @@ export class MeetingBookingComponent implements OnInit { ...@@ -431,7 +454,6 @@ export class MeetingBookingComponent implements OnInit {
}; };
console.log('Sample data loaded:', this.scheduleData); console.log('Sample data loaded:', this.scheduleData);
this.buildMonthWeeks(this.selectedDate_schedule);
} }
// Statistics methods // Statistics methods
...@@ -448,79 +470,21 @@ export class MeetingBookingComponent implements OnInit { ...@@ -448,79 +470,21 @@ export class MeetingBookingComponent implements OnInit {
return uniqueRooms.size; return uniqueRooms.size;
} }
// ===== Monthly Calendar Helpers ===== // ===== Month Navigation Helpers =====
private startOfWeek(date: Date): Date {
const d = new Date(date);
const day = d.getDay(); // 0 = Sun
d.setDate(d.getDate() - day);
d.setHours(0, 0, 0, 0);
return d;
}
private endOfWeek(date: Date): Date {
const d = new Date(date);
const day = d.getDay();
d.setDate(d.getDate() + (6 - day));
d.setHours(23, 59, 59, 999);
return d;
}
public buildMonthWeeks(baseDate: Date): void {
const firstOfMonth = new Date(baseDate.getFullYear(), baseDate.getMonth(), 1);
const lastOfMonth = new Date(baseDate.getFullYear(), baseDate.getMonth() + 1, 0);
const gridStart = this.startOfWeek(firstOfMonth);
const gridEnd = this.endOfWeek(lastOfMonth);
const weeks: Date[][] = [];
const cursor = new Date(gridStart);
while (cursor <= gridEnd) {
const week: Date[] = [];
for (let i = 0; i < 7; i++) {
week.push(new Date(cursor));
cursor.setDate(cursor.getDate() + 1);
}
weeks.push(week);
}
this.monthWeeks = weeks;
}
public prevMonth(): void { public prevMonth(): void {
const d = new Date(this.selectedDate_schedule); const d = new Date(this.selectedDate_schedule);
d.setMonth(d.getMonth() - 1); d.setMonth(d.getMonth() - 1);
this.selectedDate_schedule = d; this.selectedDate_schedule = d;
this.buildMonthWeeks(this.selectedDate_schedule);
} }
public nextMonth(): void { public nextMonth(): void {
const d = new Date(this.selectedDate_schedule); const d = new Date(this.selectedDate_schedule);
d.setMonth(d.getMonth() + 1); d.setMonth(d.getMonth() + 1);
this.selectedDate_schedule = d; this.selectedDate_schedule = d;
this.buildMonthWeeks(this.selectedDate_schedule);
} }
public today(): void { public today(): void {
this.selectedDate_schedule = new Date(); this.selectedDate_schedule = new Date();
this.buildMonthWeeks(this.selectedDate_schedule);
}
public isSameDay(a: Date, b: Date): boolean {
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}
public isCurrentMonth(date: Date): boolean {
return date.getMonth() === this.selectedDate_schedule.getMonth() && date.getFullYear() === this.selectedDate_schedule.getFullYear();
}
public isToday(date: Date): boolean {
return this.isSameDay(date, new Date());
}
public getBookingsForDate(date: Date): any[] {
const items = this.scheduleData.filter(e => this.isSameDay(new Date(e.StartTime), date));
if (this.selectedRoom) {
return items.filter(e => e.Location === this.getRoomName(this.selectedRoom));
}
return items;
} }
public getRoomImageByName(roomName: string): string | undefined { public getRoomImageByName(roomName: string): string | undefined {
......
<app-page-header [title]="'ทะเบียนห้องประชุม'" [activeTitle]="'ผู้ดูแลระบบ'" [title1]="'ทะเบียนห้องประชุม'"></app-page-header>
<div class="grid grid-cols-12 gap-6">
<div class="xl:col-span-12 col-span-12">
<div class="box">
<div class="box-header justify-between">
<div class="box-title flex items-center gap-2">
<mat-icon>meeting_room</mat-icon>
ทะเบียนห้องประชุม
<span class="badge bg-light text-default rounded-full ms-1 text-[0.75rem] align-middle">{{ rooms.length }}</span>
</div>
<div class="flex flex-wrap gap-2">
<a href="javascript:void(0);" class="hs-dropdown-toggle ti-btn ti-btn-primary-full me-2" (click)="startCreate()" data-hs-overlay="#room-modal">
<i class="ri-add-line font-semibold align-middle"></i>เพิ่มห้อง
</a>
<div>
<input class="form-control form-control" type="text" placeholder="ค้นหาห้อง/อาคาร" [(ngModel)]="searchTerm">
</div>
</div>
</div>
<div class="box-body">
<div class="table-responsive">
<table class="table whitespace-nowrap min-w-full ti-custom-table-hover">
<thead>
<tr class="border-b border-defaultborder">
<th scope="col" class="text-start">ห้อง</th>
<th scope="col" class="text-start">อาคาร/ชั้น</th>
<th scope="col" class="text-start">ความจุ</th>
<th scope="col" class="text-start">สถานะ</th>
<th scope="col" class="text-start">Action</th>
</tr>
</thead>
<tbody>
<tr class="border border-defaultborder dark:border-defaultborder/10" *ngFor="let r of filteredRooms">
<td>
<div class="flex items-center">
<span class="avatar avatar-sm p-1 me-1 bg-light !rounded-full">
<img [src]="r.imageUrl || 'assets/images/media/backgrounds/1.png'" alt="" />
</span>
<div class="ms-2">
<p class="font-semibold mb-0 text-primary">{{ r.name }}</p>
</div>
</div>
</td>
<td>{{ r.building }} / {{ r.floor }}</td>
<td>{{ r.capacity }} คน</td>
<td>
<span class="badge" [ngClass]="r.isActive ? 'bg-primary' : 'bg-warning'">{{ r.isActive ? 'Active' : 'Inactive' }}</span>
</td>
<td>
<div class="flex flex-row items-center !gap-2 ">
<a aria-label="anchor" (click)="edit(r)" data-hs-overlay="#room-modal" class="ti-btn ti-btn-wave !gap-0 !m-0 bg-info/10 text-info hover:bg-info hover:text-white hover:border-info"><i class="ri-pencil-line"></i></a>
<a aria-label="anchor" href="javascript:void(0);" (click)="remove(r)" class="ti-btn ti-btn-wave product-btn !gap-0 !m-0 bg-danger/10 text-danger hover:bg-danger hover:text-white hover:border-danger"><i class="ri-delete-bin-line"></i></a>
</div>
</td>
</tr>
<tr *ngIf="filteredRooms.length === 0">
<td [attr.colspan]="5" class="text-center py-4">ไม่พบข้อมูล...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Modal ฟอร์มห้องประชุม -->
<div id="room-modal" class="hs-overlay hidden ti-modal [--overlay-backdrop:static]">
<div class="hs-overlay-open:mt-7 ti-modal-box mt-0 ease-out">
<div class="ti-modal-content">
<div class="ti-modal-header">
<h6 class="modal-title text-[1rem] font-semibold text-defaulttextcolor">{{ form.value.id ? 'แก้ไขห้อง' : 'เพิ่มห้อง' }}</h6>
<button type="button" class="hs-dropdown-toggle !text-[1rem] !font-semibold !text-defaulttextcolor" data-hs-overlay="#room-modal">
<span class="sr-only">Close</span>
<i class="ri-close-line"></i>
</button>
</div>
<div class="ti-modal-body px-4">
<form [formGroup]="form" (ngSubmit)="save()" class="grid grid-cols-12 gap-4">
<div class="xl:col-span-12 col-span-12">
<div class="mb-0 text-center">
<span class="avatar avatar-xxl avatar-rounded">
<img [src]="imagePreview || form.value.imageUrl || 'assets/images/media/backgrounds/1.png'" alt="" id="room-img">
<span class="badge rounded-full bg-primary avatar-badge">
<input type="file" accept="image/*" class="absolute w-full h-full opacity-[0]" (change)="onImageFileChange($event)">
<i class="fe fe-camera text-[.625rem]"></i>
</span>
</span>
</div>
</div>
<div class="xl:col-span-12 col-span-12">
<label class="form-label">ชื่อห้อง</label>
<input type="text" class="form-control" formControlName="name">
</div>
<div class="xl:col-span-6 col-span-12">
<label class="form-label">ความจุ (คน)</label>
<input type="number" class="form-control" formControlName="capacity">
</div>
<div class="xl:col-span-6 col-span-12">
<label class="form-label">อาคาร</label>
<input type="text" class="form-control" formControlName="building">
</div>
<div class="xl:col-span-6 col-span-12">
<label class="form-label">ชั้น</label>
<input type="text" class="form-control" formControlName="floor">
</div>
<div class="xl:col-span-6 col-span-12">
<label class="form-label">สถานะ</label>
<select class="form-control" formControlName="isActive">
<option [ngValue]="true">Active</option>
<option [ngValue]="false">Inactive</option>
</select>
</div>
<div class="xl:col-span-12 col-span-12">
<label class="form-label">รายละเอียด</label>
<input type="text" class="form-control" formControlName="amenitiesText">
</div>
<div class="xl:col-span-12 col-span-12">
<label class="form-label">หมายเหตุ</label>
<input type="text" class="form-control" formControlName="remarksText">
</div>
</form>
</div>
<div class="ti-modal-footer">
<button type="button" class="hs-dropdown-toggle ti-btn ti-btn-light align-middle" data-hs-overlay="#room-modal">ยกเลิก</button>
<button type="button" class="ti-btn bg-primary text-white !font-medium" (click)="save()" [class.ti-btn-disabled]="form.invalid" [disabled]="form.invalid">บันทึก</button>
</div>
</div>
</div>
</div>
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { MeetingRoomComponent } from './meeting-room.component';
describe('MeetingRoomComponent', () => {
let component: MeetingRoomComponent;
let fixture: ComponentFixture<MeetingRoomComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MeetingRoomComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MeetingRoomComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { MatTableModule } from '@angular/material/table';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MeetingRoom } from '../../models/meeting-booking.model';
import { SharedModule } from '../../../shared/shared.module';
@Component({
selector: 'app-meeting-room',
templateUrl: './meeting-room.component.html',
styleUrls: ['./meeting-room.component.css'],
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
MatTableModule,
MatButtonModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatCardModule,
MatChipsModule,
MatSnackBarModule
],
})
export class MeetingRoomComponent implements OnInit {
displayedColumns = ['name','building','capacity','actions'];
rooms: MeetingRoom[] = [];
searchTerm: string = '';
imagePreview: string = '';
form = this.fb.group({
id: [''],
name: ['', Validators.required],
capacity: [0],
building: [''],
floor: [''],
amenitiesText: [''],
imageUrl: [''],
isActive: [true]
});
constructor(private fb: FormBuilder, private snack: MatSnackBar){
// seed sample
this.rooms = [
{ id:'A101', name:'ห้องประชุม A101', capacity:12, location:'โซนตะวันออก', floor:'10', building:'อาคาร A', amenities:['Display 75"','HDMI'], isActive:true, imageUrl:'' },
{ id:'B201', name:'ห้องประชุม B201', capacity:8, location:'โซนเหนือ', floor:'2', building:'อาคาร B', amenities:['TV','Whiteboard'], isActive:true, imageUrl:'' }
];
}
ngOnInit(): void {
throw new Error('Method not implemented.');
}
get filteredRooms(): MeetingRoom[] {
const q = (this.searchTerm || '').toLowerCase();
if(!q) return this.rooms;
return this.rooms.filter(r =>
r.name.toLowerCase().includes(q) ||
(r.building || '').toLowerCase().includes(q) ||
(r.floor || '').toLowerCase().includes(q)
);
}
// overlay handled by HS overlay via data-hs-overlay attributes
startCreate(){
this.form.reset({ id: '', name:'', capacity:0, building:'', floor:'', amenitiesText:'', imageUrl:'', isActive:true });
}
edit(r: MeetingRoom){
this.form.setValue({
id: r.id,
name: r.name,
capacity: r.capacity,
building: r.building,
floor: r.floor,
amenitiesText: r.amenities?.join(', ') || '',
imageUrl: r.imageUrl || '',
isActive: r.isActive
});
this.imagePreview = r.imageUrl || '';
}
cancel(){
this.form.reset();
}
save(){
const v = this.form.getRawValue();
if(!v) return;
const idx = this.rooms.findIndex(x => x.id === v.id && v.id);
const data: MeetingRoom = {
id: v.id || this.genId(),
name: v.name || '',
capacity: Number(v.capacity) || 0,
building: v.building || '',
floor: v.floor || '',
location: '',
amenities: (v.amenitiesText || '').split(',').map(s => s.trim()).filter(Boolean),
isActive: !!v.isActive,
imageUrl: this.imagePreview || v.imageUrl || ''
};
if(idx >= 0){ this.rooms[idx] = data; } else { this.rooms = [...this.rooms, data]; }
this.snack.open('บันทึกห้องประชุมแล้ว', 'ปิด', { duration: 2000 });
this.form.reset();
}
remove(r: MeetingRoom){
this.rooms = this.rooms.filter(x => x.id !== r.id);
this.snack.open('ลบห้องประชุมแล้ว', 'ปิด', { duration: 2000 });
}
private genId(){
return 'RM-' + Math.random().toString(36).slice(2,8).toUpperCase();
}
onImageFileChange(evt: any){
const file: File | undefined = evt?.target?.files?.[0];
if(!file) return;
const reader = new FileReader();
reader.onload = () => {
this.imagePreview = String(reader.result || '');
};
reader.readAsDataURL(file);
}
}
...@@ -2,6 +2,7 @@ import { Routes } from '@angular/router'; ...@@ -2,6 +2,7 @@ import { Routes } from '@angular/router';
import { moduleAccessGuard } from '../core/guards/module-access.guard'; import { moduleAccessGuard } from '../core/guards/module-access.guard';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
import { MeetingBookingComponent } from './meeting-booking/meeting-booking.component'; import { MeetingBookingComponent } from './meeting-booking/meeting-booking.component';
import { MeetingRoomComponent } from '../portal-manage/meeting-booking/meeting-room/meeting-room.component';
import { MenuPermissionManagementComponent } from './menu-permission-management/menu-permission-management.component'; import { MenuPermissionManagementComponent } from './menu-permission-management/menu-permission-management.component';
// import { CompanyManagementComponent } from './company-management/company-management.component'; // import { CompanyManagementComponent } from './company-management/company-management.component';
...@@ -80,6 +81,13 @@ export const portalManageRoutes: Routes = [ ...@@ -80,6 +81,13 @@ export const portalManageRoutes: Routes = [
canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
}, },
// Meeting Room Registry
{
path: 'meeting-rooms',
component: MeetingRoomComponent,
canActivate: [moduleAccessGuard]
},
// === การตั้งค่าระบบ === // === การตั้งค่าระบบ ===
// Permission Management // Permission Management
......
...@@ -81,6 +81,7 @@ export class SidebarComponent { ...@@ -81,6 +81,7 @@ export class SidebarComponent {
isPermissionManagementRoute: boolean = false; isPermissionManagementRoute: boolean = false;
isMenuPermissionManagementRoute: boolean = false; isMenuPermissionManagementRoute: boolean = false;
isMeetingBookingRoute: boolean = false; isMeetingBookingRoute: boolean = false;
isMeetingRoomRegistryRoute: boolean = false;
isWidgetWarehouseRoute: boolean = false; isWidgetWarehouseRoute: boolean = false;
isWidgetLinkerRoute: boolean = false; isWidgetLinkerRoute: boolean = false;
previousUrl: string = ''; previousUrl: string = '';
...@@ -133,6 +134,7 @@ export class SidebarComponent { ...@@ -133,6 +134,7 @@ export class SidebarComponent {
this.isPermissionManagementRoute = this.currentUrl.includes('/permission-management'); this.isPermissionManagementRoute = this.currentUrl.includes('/permission-management');
this.isMenuPermissionManagementRoute = this.currentUrl.includes('/menu-permission-management'); this.isMenuPermissionManagementRoute = this.currentUrl.includes('/menu-permission-management');
this.isMeetingBookingRoute = this.currentUrl.includes('/meeting-booking'); this.isMeetingBookingRoute = this.currentUrl.includes('/meeting-booking');
this.isMeetingRoomRegistryRoute = this.currentUrl.includes('/meeting-rooms');
this.isWidgetWarehouseRoute = this.currentUrl.includes('/widget-warehouse'); this.isWidgetWarehouseRoute = this.currentUrl.includes('/widget-warehouse');
this.isWidgetLinkerRoute = this.currentUrl.includes('/widget-linker'); this.isWidgetLinkerRoute = this.currentUrl.includes('/widget-linker');
this.menuitemsSubscribe$ = this.navServices.items.subscribe((items) => { this.menuitemsSubscribe$ = this.navServices.items.subscribe((items) => {
...@@ -175,6 +177,7 @@ export class SidebarComponent { ...@@ -175,6 +177,7 @@ export class SidebarComponent {
this.isPermissionManagementRoute = this.currentUrl.includes('/permission-management'); this.isPermissionManagementRoute = this.currentUrl.includes('/permission-management');
this.isMenuPermissionManagementRoute = this.currentUrl.includes('/menu-permission-management'); this.isMenuPermissionManagementRoute = this.currentUrl.includes('/menu-permission-management');
this.isMeetingBookingRoute = this.currentUrl.includes('/meeting-booking'); this.isMeetingBookingRoute = this.currentUrl.includes('/meeting-booking');
this.isMeetingRoomRegistryRoute = this.currentUrl.includes('/meeting-rooms');
this.isWidgetWarehouseRoute = this.currentUrl.includes('/widget-warehouse'); this.isWidgetWarehouseRoute = this.currentUrl.includes('/widget-warehouse');
this.isWidgetLinkerRoute = this.currentUrl.includes('/widget-linker'); this.isWidgetLinkerRoute = this.currentUrl.includes('/widget-linker');
this.checkUrlChanges() this.checkUrlChanges()
...@@ -237,6 +240,7 @@ export class SidebarComponent { ...@@ -237,6 +240,7 @@ export class SidebarComponent {
this.isPermissionManagementRoute = this.currentUrl.includes('/permission-management'); this.isPermissionManagementRoute = this.currentUrl.includes('/permission-management');
this.isMenuPermissionManagementRoute = this.currentUrl.includes('/menu-permission-management'); this.isMenuPermissionManagementRoute = this.currentUrl.includes('/menu-permission-management');
this.isMeetingBookingRoute = this.currentUrl.includes('/meeting-booking'); this.isMeetingBookingRoute = this.currentUrl.includes('/meeting-booking');
this.isMeetingRoomRegistryRoute = this.currentUrl.includes('/meeting-rooms');
this.isWidgetWarehouseRoute = this.currentUrl.includes('/widget-warehouse'); this.isWidgetWarehouseRoute = this.currentUrl.includes('/widget-warehouse');
this.isWidgetLinkerRoute = this.currentUrl.includes('/widget-linker'); this.isWidgetLinkerRoute = this.currentUrl.includes('/widget-linker');
...@@ -292,7 +296,7 @@ export class SidebarComponent { ...@@ -292,7 +296,7 @@ export class SidebarComponent {
this.menuItems = this.navServices.getSystemManagementMenu(); this.menuItems = this.navServices.getSystemManagementMenu();
} else if (this.isMenuPermissionManagementRoute){ } else if (this.isMenuPermissionManagementRoute){
this.menuItems = this.navServices.getSystemManagementMenu(); this.menuItems = this.navServices.getSystemManagementMenu();
} else if (this.isMeetingBookingRoute){ } else if (this.isMeetingBookingRoute || this.isMeetingRoomRegistryRoute){
this.menuItems = this.navServices.getMeetingBookingMenu(); this.menuItems = this.navServices.getMeetingBookingMenu();
} else if (this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute){ } else if (this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute){
// สำหรับ widget-warehouse และ widget-linker ใช้เมนูตามแอปที่อยู่ใน URL // สำหรับ widget-warehouse และ widget-linker ใช้เมนูตามแอปที่อยู่ใน URL
......
...@@ -486,6 +486,7 @@ export class NavService implements OnDestroy { ...@@ -486,6 +486,7 @@ export class NavService implements OnDestroy {
type: 'sub', type: 'sub',
active: false, active: false,
children: [ children: [
{ path: '/portal-manage/meeting-rooms', title: 'ทะเบียนห้องประชุม', type: 'link' },
{ path: '/portal-manage/meeting-booking', title: 'จองห้องประชุม', type: 'link' }, { path: '/portal-manage/meeting-booking', title: 'จองห้องประชุม', type: 'link' },
], ],
}, },
......
...@@ -3,9 +3,86 @@ ...@@ -3,9 +3,86 @@
import { bootstrapApplication } from '@angular/platform-browser'; import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config'; import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
import { registerLicense } from '@syncfusion/ej2-base'; import { registerLicense, L10n, setCulture } from '@syncfusion/ej2-base';
registerLicense('ORg4AjUWIQA/Gnt2XFhhQlJHfV5AQmBIYVp/TGpJfl96cVxMZVVBJAtUQF1hTH5WdUVjWXtXdHNdRWFbWkdx'); registerLicense('ORg4AjUWIQA/Gnt2XFhhQlJHfV5AQmBIYVp/TGpJfl96cVxMZVVBJAtUQF1hTH5WdUVjWXtXdHNdRWFbWkdx');
// Set Thai locale for Syncfusion components
L10n.load({
'th': {
'schedule': {
'dayNames': ['อาทิตย์', 'จันทร์', 'อังคาร', 'พุธ', 'พฤหัสบดี', 'ศุกร์', 'เสาร์'],
'dayNamesShort': ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส'],
'dayNamesMin': ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส'],
'monthNames': ['มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'],
'monthNamesShort': ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'],
'day': 'วัน',
'week': 'สัปดาห์',
'workWeek': 'สัปดาห์ทำงาน',
'month': 'เดือน',
'agenda': 'รายการ',
'today': 'วันนี้',
'noEvents': 'ไม่มีกิจกรรม',
'emptyContainer': 'ไม่มีกิจกรรมที่กำหนดไว้ในวันที่นี้',
'allDay': 'ทั้งวัน',
'start': 'เริ่มต้น',
'end': 'สิ้นสุด',
'more': 'เพิ่มเติม',
'close': 'ปิด',
'cancel': 'ยกเลิก',
'noTitle': '(ไม่มีชื่อ)',
'delete': 'ลบ',
'deleteEvent': 'ลบกิจกรรม',
'selected': 'เลือกแล้ว',
'occurrence': 'การเกิดขึ้น',
'series': 'ชุด',
'previous': 'ก่อนหน้า',
'next': 'ถัดไป',
'edit': 'แก้ไข',
'editEvent': 'แก้ไขกิจกรรม',
'create': 'สร้าง',
'newEvent': 'กิจกรรมใหม่',
'save': 'บันทึก',
'subject': 'หัวข้อ',
'title': 'ชื่อ',
'startTime': 'เวลาเริ่มต้น',
'endTime': 'เวลาสิ้นสุด',
'repeat': 'ทำซ้ำ',
'location': 'สถานที่',
'description': 'รายละเอียด',
'timezone': 'เขตเวลา',
'none': 'ไม่มี',
'daily': 'รายวัน',
'weekly': 'รายสัปดาห์',
'monthly': 'รายเดือน',
'yearly': 'รายปี',
'never': 'ไม่เคย',
'until': 'จนถึง',
'count': 'จำนวน',
'first': 'แรก',
'second': 'สอง',
'third': 'สาม',
'fourth': 'สี่',
'last': 'สุดท้าย'
}
}
});
// Set culture to Thai
setCulture('th');
// Additional locale setup for Schedule
L10n.load({
'th-TH': {
'schedule': {
'dayNames': ['อาทิตย์', 'จันทร์', 'อังคาร', 'พุธ', 'พฤหัสบดี', 'ศุกร์', 'เสาร์'],
'dayNamesShort': ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส'],
'dayNamesMin': ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส'],
'monthNames': ['มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'],
'monthNamesShort': ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.']
}
}
});
bootstrapApplication(AppComponent, appConfig) bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err)); .catch((err) => console.error(err));
...@@ -49,6 +49,7 @@ ...@@ -49,6 +49,7 @@
@import "../node_modules/@syncfusion/ej2-splitbuttons/styles/tailwind.css"; @import "../node_modules/@syncfusion/ej2-splitbuttons/styles/tailwind.css";
@import '../node_modules/@syncfusion/ej2-angular-pivotview/styles/tailwind.css'; @import '../node_modules/@syncfusion/ej2-angular-pivotview/styles/tailwind.css';
@import "../node_modules/@syncfusion/ej2-angular-layouts/styles/tailwind.css"; @import "../node_modules/@syncfusion/ej2-angular-layouts/styles/tailwind.css";
@import '../node_modules/@syncfusion/ej2-angular-schedule/styles/tailwind.css';
// @import "../node_modules/angular-calendar/scss/angular-calendar.scss"; // @import "../node_modules/angular-calendar/scss/angular-calendar.scss";
//swiperjs //swiperjs
...@@ -109,3 +110,204 @@ ...@@ -109,3 +110,204 @@
.e-grid td.e-selectionbackground { .e-grid td.e-selectionbackground {
background-color: #aec2ec !important; background-color: #aec2ec !important;
} }
// ===== Syncfusion Schedule Custom Styles =====
.custom-schedule {
.e-schedule {
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
.e-header-cells {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
border-bottom: 2px solid #e2e8f0;
}
.e-date-header {
background-color: #f1f5f9;
color: #334155;
font-weight: 500;
}
.e-work-cells {
border-right: 1px solid #e2e8f0;
border-bottom: 1px solid #e2e8f0;
}
.e-appointment {
border-radius: 4px;
border: none;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
}
.e-appointment .e-subject {
font-weight: 500;
color: #1e293b;
}
.e-appointment .e-location {
font-size: 0.75rem;
color: #64748b;
}
.e-appointment .e-time {
font-size: 0.75rem;
color: #64748b;
}
// Status colors
.e-appointment.status-confirmed {
background-color: #dcfce7;
border-left: 4px solid #22c55e;
}
.e-appointment.status-pending {
background-color: #fef3c7;
border-left: 4px solid #f59e0b;
}
.e-appointment.status-cancelled {
background-color: #fee2e2;
border-left: 4px solid #ef4444;
}
.e-appointment.status-completed {
background-color: #e0f2fe;
border-left: 4px solid #0ea5e9;
}
// Month view specific styles
.e-month-view .e-date-header {
padding: 8px 4px;
text-align: center;
}
.e-month-view .e-work-cells {
height: 100px;
vertical-align: top;
}
.e-month-view .e-appointment {
margin: 1px 2px;
padding: 2px 4px;
font-size: 0.75rem;
}
// Week view specific styles
.e-week-view .e-work-cells {
height: 60px;
}
.e-week-view .e-appointment {
margin: 1px;
padding: 4px 6px;
}
// Day view specific styles
.e-day-view .e-work-cells {
height: 40px;
}
.e-day-view .e-appointment {
margin: 1px;
padding: 6px 8px;
}
// Agenda view specific styles
.e-agenda-view .e-appointment {
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 8px;
padding: 12px;
}
.e-agenda-view .e-subject {
font-size: 1rem;
font-weight: 600;
color: #1e293b;
}
.e-agenda-view .e-location {
font-size: 0.875rem;
color: #64748b;
margin-top: 4px;
}
.e-agenda-view .e-time {
font-size: 0.875rem;
color: #64748b;
margin-top: 2px;
}
}
// View switcher styles
.view-switcher {
margin-bottom: 16px;
display: flex;
justify-content: center;
.mat-button-toggle-group {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
.mat-button-toggle {
border: none;
background-color: #f8fafc;
color: #64748b;
font-weight: 500;
min-width: 100px;
&.mat-button-toggle-checked {
background-color: #3b82f6;
color: white;
}
&:hover:not(.mat-button-toggle-checked) {
background-color: #e2e8f0;
}
.mat-icon {
margin-right: 4px;
font-size: 18px;
}
}
}
// Month controls styles
.month-controls {
margin-bottom: 16px;
display: flex;
justify-content: center;
.month-toolbar {
display: flex;
align-items: center;
gap: 16px;
background-color: #f8fafc;
padding: 8px 16px;
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
.month-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
min-width: 200px;
text-align: center;
}
button {
color: #64748b;
&:hover {
color: #3b82f6;
background-color: #e2e8f0;
}
}
}
}
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