Commit 1e4426e9 by sawit

หน้าจองห้องประชุม

parent 26ac6e17
...@@ -48,6 +48,7 @@ ...@@ -48,6 +48,7 @@
"@syncfusion/ej2-angular-pivotview": "^31.1.17", "@syncfusion/ej2-angular-pivotview": "^31.1.17",
"@syncfusion/ej2-angular-popups": "^31.1.17", "@syncfusion/ej2-angular-popups": "^31.1.17",
"@syncfusion/ej2-angular-progressbar": "^31.1.17", "@syncfusion/ej2-angular-progressbar": "^31.1.17",
"@syncfusion/ej2-angular-schedule": "^31.1.21",
"@syncfusion/ej2-angular-treemap": "^31.1.17", "@syncfusion/ej2-angular-treemap": "^31.1.17",
"@syncfusion/ej2-base": "^31.1.17", "@syncfusion/ej2-base": "^31.1.17",
"@syncfusion/ej2-buttons": "^31.1.17", "@syncfusion/ej2-buttons": "^31.1.17",
...@@ -6334,6 +6335,17 @@ ...@@ -6334,6 +6335,17 @@
"@syncfusion/ej2-progressbar": "31.1.17" "@syncfusion/ej2-progressbar": "31.1.17"
} }
}, },
"node_modules/@syncfusion/ej2-angular-schedule": {
"version": "31.1.21",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-schedule/-/ej2-angular-schedule-31.1.21.tgz",
"integrity": "sha512-mRlZmqimr980wWSu595e0s/zkIHlpV9EfvQua9EeveGvaZaf8fHoXysuL1GaUvjj4dCtEbJRrvuj3gDsaUqdlg==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-angular-base": "~31.1.17",
"@syncfusion/ej2-base": "~31.1.20",
"@syncfusion/ej2-schedule": "31.1.21"
}
},
"node_modules/@syncfusion/ej2-angular-treemap": { "node_modules/@syncfusion/ej2-angular-treemap": {
"version": "31.1.17", "version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-treemap/-/ej2-angular-treemap-31.1.17.tgz", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-treemap/-/ej2-angular-treemap-31.1.17.tgz",
...@@ -6346,9 +6358,9 @@ ...@@ -6346,9 +6358,9 @@
} }
}, },
"node_modules/@syncfusion/ej2-base": { "node_modules/@syncfusion/ej2-base": {
"version": "31.1.17", "version": "31.1.22",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-base/-/ej2-base-31.1.17.tgz", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-base/-/ej2-base-31.1.22.tgz",
"integrity": "sha512-Jl2Ls77kMfX13VwrAEow/kdDrefX3gh76Uwh943NUtEuChOXzvoe+CHecxJ+49VFRCrLYhB4Hj/7R9UOh65VCg==", "integrity": "sha512-zgNULXYUFG8Cr2ML8VcUqj8EcQSVd/eXNnD8APZdOjELJPE1ZzeNmRzCab16A/hqCZ+wAXwKmjaTC0I6xiXjSQ==",
"license": "SEE LICENSE IN license", "license": "SEE LICENSE IN license",
"dependencies": { "dependencies": {
"@syncfusion/ej2-icons": "~31.1.17" "@syncfusion/ej2-icons": "~31.1.17"
...@@ -6611,6 +6623,97 @@ ...@@ -6611,6 +6623,97 @@
"@syncfusion/ej2-svg-base": "~31.1.17" "@syncfusion/ej2-svg-base": "~31.1.17"
} }
}, },
"node_modules/@syncfusion/ej2-schedule": {
"version": "31.1.21",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-schedule/-/ej2-schedule-31.1.21.tgz",
"integrity": "sha512-OXwKKNvkZ06Y66A0yvu0lV6Xd/xafz2LokTB/bXa+0v05txzxAxbjAC95C3yDJUkRA2Mtq1cdyyETxrJX768pA==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~31.1.20",
"@syncfusion/ej2-buttons": "~31.1.21",
"@syncfusion/ej2-calendars": "~31.1.21",
"@syncfusion/ej2-data": "~31.1.17",
"@syncfusion/ej2-dropdowns": "~31.1.20",
"@syncfusion/ej2-excel-export": "~31.1.17",
"@syncfusion/ej2-inputs": "~31.1.21",
"@syncfusion/ej2-lists": "~31.1.17",
"@syncfusion/ej2-navigations": "~31.1.20",
"@syncfusion/ej2-popups": "~31.1.20"
}
},
"node_modules/@syncfusion/ej2-schedule/node_modules/@syncfusion/ej2-buttons": {
"version": "31.1.21",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-buttons/-/ej2-buttons-31.1.21.tgz",
"integrity": "sha512-NcBMK19Yn7//bwbREsdvqy3v1237AzulrphSmeS9SrKMd7L5H6apeqFe1BAkvAs/K+JPpt6A3Dv6Fhl3SkVcUg==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~31.1.20"
}
},
"node_modules/@syncfusion/ej2-schedule/node_modules/@syncfusion/ej2-calendars": {
"version": "31.1.22",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-calendars/-/ej2-calendars-31.1.22.tgz",
"integrity": "sha512-eepI70LowYogxOVQRA+pEFoHpFdqge2wyyVkz/ixhRzniVi2WwA5nN6AXJtYvS9fOMKRUThmzXXfjUdO/TP/4g==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~31.1.22",
"@syncfusion/ej2-buttons": "~31.1.21",
"@syncfusion/ej2-inputs": "~31.1.22",
"@syncfusion/ej2-lists": "~31.1.17",
"@syncfusion/ej2-popups": "~31.1.20"
}
},
"node_modules/@syncfusion/ej2-schedule/node_modules/@syncfusion/ej2-dropdowns": {
"version": "31.1.22",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-dropdowns/-/ej2-dropdowns-31.1.22.tgz",
"integrity": "sha512-UQaBh/7DudnkUAf//XOEPezc/mmmTTqdNMaKchEUlRErKOcAulY+HxDK8lsJegusxNiMz6vM8gxviiMhqW0uDg==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~31.1.22",
"@syncfusion/ej2-data": "~31.1.17",
"@syncfusion/ej2-inputs": "~31.1.22",
"@syncfusion/ej2-lists": "~31.1.17",
"@syncfusion/ej2-navigations": "~31.1.20",
"@syncfusion/ej2-notifications": "~31.1.17",
"@syncfusion/ej2-popups": "~31.1.20"
}
},
"node_modules/@syncfusion/ej2-schedule/node_modules/@syncfusion/ej2-inputs": {
"version": "31.1.22",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-inputs/-/ej2-inputs-31.1.22.tgz",
"integrity": "sha512-XkF69fWeYcDpbokpiqIgjSEyVlsG7G8qGYUslUpT0ynZZ60+tz/DWemxh7gfQicJ3QCI9dzQFxxTnzEe/aRAzw==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~31.1.22",
"@syncfusion/ej2-buttons": "~31.1.21",
"@syncfusion/ej2-popups": "~31.1.20",
"@syncfusion/ej2-splitbuttons": "~31.1.17"
}
},
"node_modules/@syncfusion/ej2-schedule/node_modules/@syncfusion/ej2-navigations": {
"version": "31.1.20",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-navigations/-/ej2-navigations-31.1.20.tgz",
"integrity": "sha512-KzJT4vHbMF9ooq8o41xwYUJX70Dr96mMEaRYJusRv/RJ+1zM/UvHhgX8n15TIFTXexH1y+KwkFvYaGchcokBgA==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~31.1.20",
"@syncfusion/ej2-buttons": "~31.1.17",
"@syncfusion/ej2-data": "~31.1.17",
"@syncfusion/ej2-inputs": "~31.1.20",
"@syncfusion/ej2-lists": "~31.1.17",
"@syncfusion/ej2-popups": "~31.1.20"
}
},
"node_modules/@syncfusion/ej2-schedule/node_modules/@syncfusion/ej2-popups": {
"version": "31.1.20",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-popups/-/ej2-popups-31.1.20.tgz",
"integrity": "sha512-GRPZWX2ZRNS73GpG8NbhC12eBNNqdMR80nhJrun1ZBVfvPTvd6y5/qvJLWg6/kYN02tM+smgyxfxfwOH4HGi3w==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~31.1.20",
"@syncfusion/ej2-buttons": "~31.1.17"
}
},
"node_modules/@syncfusion/ej2-splitbuttons": { "node_modules/@syncfusion/ej2-splitbuttons": {
"version": "31.1.17", "version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-splitbuttons/-/ej2-splitbuttons-31.1.17.tgz", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-splitbuttons/-/ej2-splitbuttons-31.1.17.tgz",
......
<div class="container-fluid"> <!-- Meeting Room Booking System with Syncfusion Schedule -->
<div class="row"> <div class="meeting-booking-container">
<div class="col-12"> <!-- Header Section -->
<div class="card"> <div class="page-header">
<div class="card-header"> <div class="header-content">
<h4 class="card-title">ระบบจองห้องประชุม</h4> <div class="header-title">
<p class="card-subtitle">จองห้องประชุมและจัดการการประชุมของคุณ</p> <h1><i class="fas fa-calendar-alt"></i> ระบบจองห้องประชุม</h1>
</div> <p>จัดการการจองห้องประชุมอย่างมีประสิทธิภาพด้วย Syncfusion Schedule</p>
<div class="card-body"> </div>
<mat-tab-group> <div class="header-actions">
<!-- Booking Tab --> <button mat-raised-button color="primary" (click)="showBookingForm = true">
<mat-tab label="จองห้องประชุม"> <mat-icon>add</mat-icon>
<div class="row mt-3"> จองห้องประชุม
<div class="col-md-8"> </button>
<mat-card> <button mat-raised-button color="accent" (click)="loadSampleData()">
<mat-card-header> <mat-icon>refresh</mat-icon>
<mat-card-title>ข้อมูลการจอง</mat-card-title> โหลดข้อมูลตัวอย่าง
</mat-card-header> </button>
<mat-card-content> </div>
<form [formGroup]="bookingForm" (ngSubmit)="createBooking()"> </div>
<div class="row"> </div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100"> <!-- Main Content -->
<div class="main-content">
<!-- Booking Form Modal -->
<div class="booking-form-overlay" *ngIf="showBookingForm" (click)="showBookingForm = false">
<div class="booking-form-container" (click)="$event.stopPropagation()">
<div class="form-header">
<h2><mat-icon>event_available</mat-icon> จองห้องประชุม</h2>
<button mat-icon-button (click)="showBookingForm = false">
<mat-icon>close</mat-icon>
</button>
</div>
<form [formGroup]="bookingForm" (ngSubmit)="createBooking()" class="booking-form">
<div class="form-row">
<mat-form-field appearance="outline" class="form-field">
<mat-label>ห้องประชุม</mat-label> <mat-label>ห้องประชุม</mat-label>
<mat-select formControlName="roomId" (selectionChange)="onRoomChange()"> <mat-select formControlName="roomId" (selectionChange)="onRoomChange()">
<mat-option *ngFor="let room of rooms$ | async" [value]="room.id"> <mat-option *ngFor="let room of rooms$ | async" [value]="room.id">
...@@ -28,74 +42,48 @@ ...@@ -28,74 +42,48 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>วันที่</mat-label>
<input matInput [matDatepicker]="picker"
[(ngModel)]="selectedDate"
(dateChange)="onDateChange()"
formControlName="startDateTime">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
</div>
<div class="row"> <mat-form-field appearance="outline" class="form-field">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>หัวข้อการประชุม</mat-label> <mat-label>หัวข้อการประชุม</mat-label>
<input matInput formControlName="title" placeholder="ระบุหัวข้อการประชุม"> <input matInput formControlName="title" placeholder="ระบุหัวข้อการประชุม">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100"> <div class="form-row">
<mat-label>รายละเอียด</mat-label> <mat-form-field appearance="outline" class="form-field">
<input matInput formControlName="description" placeholder="รายละเอียดเพิ่มเติม (ไม่บังคับ)"> <mat-label>วันที่เริ่มต้น</mat-label>
<input matInput [matDatepicker]="startPicker" formControlName="startDateTime">
<mat-datepicker-toggle matSuffix [for]="startPicker"></mat-datepicker-toggle>
<mat-datepicker #startPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>วันที่สิ้นสุด</mat-label>
<input matInput [matDatepicker]="endPicker" formControlName="endDateTime">
<mat-datepicker-toggle matSuffix [for]="endPicker"></mat-datepicker-toggle>
<mat-datepicker #endPicker></mat-datepicker>
</mat-form-field> </mat-form-field>
</div>
</div> </div>
<div class="row"> <div class="form-row">
<div class="col-md-6"> <mat-form-field appearance="outline" class="form-field">
<mat-form-field appearance="outline" class="w-100">
<mat-label>เวลาเริ่มต้น</mat-label> <mat-label>เวลาเริ่มต้น</mat-label>
<input matInput formControlName="startDateTime" type="datetime-local"> <input matInput type="time" formControlName="startTime">
</mat-form-field> </mat-form-field>
</div>
<div class="col-md-6"> <mat-form-field appearance="outline" class="form-field">
<mat-form-field appearance="outline" class="w-100">
<mat-label>เวลาสิ้นสุด</mat-label> <mat-label>เวลาสิ้นสุด</mat-label>
<input matInput formControlName="endDateTime" type="datetime-local"> <input matInput type="time" formControlName="endTime">
</mat-form-field> </mat-form-field>
</div> </div>
</div>
<div class="row"> <mat-form-field appearance="outline" class="form-field-full">
<div class="col-12"> <mat-label>รายละเอียด</mat-label>
<mat-form-field appearance="outline" class="w-100"> <textarea matInput formControlName="description" rows="3" placeholder="รายละเอียดเพิ่มเติม (ไม่บังคับ)"></textarea>
<mat-label>ผู้เข้าร่วมประชุม</mat-label>
<mat-chip-grid #chipGrid>
<mat-chip-row *ngFor="let attendee of attendees" (removed)="removeAttendee(attendee)">
{{ attendee }}
<button matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
<input placeholder="เพิ่มผู้เข้าร่วมประชุม"
[matChipInputFor]="chipGrid"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addAttendee($event)">
</mat-chip-grid>
</mat-form-field> </mat-form-field>
</div>
</div>
<div class="row mt-3"> <div class="form-actions">
<div class="col-12"> <button mat-raised-button color="primary" type="submit" [disabled]="!bookingForm.valid">
<button mat-raised-button color="primary" type="submit"
[disabled]="!bookingForm.valid">
<mat-icon>event</mat-icon> <mat-icon>event</mat-icon>
จองห้องประชุม จองห้องประชุม
</button> </button>
...@@ -103,164 +91,169 @@ ...@@ -103,164 +91,169 @@
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
รีเซ็ต รีเซ็ต
</button> </button>
</div> <button mat-button type="button" (click)="showBookingForm = false">
<mat-icon>close</mat-icon>
ยกเลิก
</button>
</div> </div>
</form> </form>
</mat-card-content>
</mat-card>
</div> </div>
<div class="col-md-4">
<mat-card>
<mat-card-header>
<mat-card-title>ช่วงเวลาที่ว่าง</mat-card-title>
<mat-card-subtitle *ngIf="selectedRoom">ห้อง: {{ getRoomName(selectedRoom) }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="isLoading" class="text-center">
<mat-spinner diameter="30"></mat-spinner>
<p>กำลังโหลด...</p>
</div> </div>
<div *ngIf="!isLoading && timeSlots.length > 0" class="time-slots"> <!-- Statistics Cards -->
<div *ngFor="let slot of timeSlots" <div class="statistics-section" *ngIf="scheduleData.length > 0">
class="time-slot" <div class="stats-title">
[class.available]="slot.isAvailable" <h3><mat-icon>analytics</mat-icon> สถิติการจอง</h3>
[class.unavailable]="!slot.isAvailable" </div>
(click)="selectTimeSlot(slot)"> <div class="stats-cards">
<span class="time">{{ slot.startTime }} - {{ slot.endTime }}</span> <div class="stat-card">
<mat-icon *ngIf="slot.isAvailable">check_circle</mat-icon> <div class="stat-icon">
<mat-icon *ngIf="!slot.isAvailable">cancel</mat-icon> <mat-icon>event</mat-icon>
</div>
<div class="stat-content">
<div class="stat-number">{{ scheduleData.length }}</div>
<div class="stat-label">การจองทั้งหมด</div>
</div> </div>
</div> </div>
<div *ngIf="!isLoading && timeSlots.length === 0" class="text-center text-muted"> <div class="stat-card">
<mat-icon>event_busy</mat-icon> <div class="stat-icon">
<p>ไม่มีช่วงเวลาที่ว่างในวันนี้</p> <mat-icon>check_circle</mat-icon>
</div> </div>
</mat-card-content> <div class="stat-content">
</mat-card> <div class="stat-number">{{ getConfirmedCount() }}</div>
<div class="stat-label">ยืนยันแล้ว</div>
</div> </div>
</div> </div>
</mat-tab>
<!-- My Bookings Tab -->
<mat-tab label="การจองของฉัน">
<div class="row mt-3">
<div class="col-12">
<mat-card>
<mat-card-header>
<mat-card-title>รายการการจองของฉัน</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="table-responsive">
<table mat-table [dataSource]="(bookings$ | async) || []" class="w-100">
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef>หัวข้อ</th>
<td mat-cell *matCellDef="let booking">{{ booking.title }}</td>
</ng-container>
<ng-container matColumnDef="room">
<th mat-header-cell *matHeaderCellDef>ห้องประชุม</th>
<td mat-cell *matCellDef="let booking">{{ booking.roomName }}</td>
</ng-container>
<ng-container matColumnDef="startTime"> <div class="stat-card">
<th mat-header-cell *matHeaderCellDef>เวลาเริ่มต้น</th> <div class="stat-icon">
<td mat-cell *matCellDef="let booking"> <mat-icon>pending</mat-icon>
{{ booking.startDateTime | date:'dd/MM/yyyy HH:mm' }} </div>
</td> <div class="stat-content">
</ng-container> <div class="stat-number">{{ getPendingCount() }}</div>
<div class="stat-label">รอดำเนินการ</div>
<ng-container matColumnDef="endTime"> </div>
<th mat-header-cell *matHeaderCellDef>เวลาสิ้นสุด</th> </div>
<td mat-cell *matCellDef="let booking">
{{ booking.endDateTime | date:'dd/MM/yyyy HH:mm' }}
</td>
</ng-container>
<ng-container matColumnDef="organizer"> <div class="stat-card">
<th mat-header-cell *matHeaderCellDef>ผู้จัด</th> <div class="stat-icon">
<td mat-cell *matCellDef="let booking">{{ booking.organizerName }}</td> <mat-icon>room</mat-icon>
</ng-container> </div>
<div class="stat-content">
<div class="stat-number">{{ getUniqueRoomsCount() }}</div>
<div class="stat-label">ห้องที่ใช้งาน</div>
</div>
</div>
</div>
</div>
<ng-container matColumnDef="status"> <!-- Schedule View -->
<th mat-header-cell *matHeaderCellDef>สถานะ</th> <div class="schedule-section">
<td mat-cell *matCellDef="let booking"> <div class="schedule-header">
<span class="badge badge-{{ getStatusColor(booking.status) }}"> <div class="schedule-title">
{{ getStatusText(booking.status) }} <h2><mat-icon>schedule</mat-icon> ปฏิทินการจองห้องประชุม</h2>
</span> <p>ดูและจัดการการจองในรูปแบบปฏิทินแบบเต็มรูปแบบ</p>
</td> </div>
</ng-container>
<ng-container matColumnDef="actions"> <div class="schedule-controls">
<th mat-header-cell *matHeaderCellDef>การดำเนินการ</th> <mat-form-field appearance="outline" class="control-field">
<td mat-cell *matCellDef="let booking"> <mat-label>กรองตามห้องประชุม</mat-label>
<button mat-icon-button <mat-select [(value)]="selectedRoom" (selectionChange)="onRoomChange()">
*ngIf="booking.status === 'pending' || booking.status === 'confirmed'" <mat-option value="">ทั้งหมด</mat-option>
(click)="cancelBooking(booking.id)" <mat-option *ngFor="let room of rooms$ | async" [value]="room.id">
matTooltip="ยกเลิกการจอง"> {{ room.name }}
<mat-icon>cancel</mat-icon> </mat-option>
</button> </mat-select>
<button mat-icon-button matTooltip="ดูรายละเอียด"> </mat-form-field>
<mat-icon>visibility</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <mat-form-field appearance="outline" class="control-field">
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <mat-label>มุมมอง</mat-label>
</table> <mat-select [(value)]="currentView" (selectionChange)="onViewChange($event)">
</div> <mat-option value="Day">รายวัน</mat-option>
</mat-card-content> <mat-option value="Week">รายสัปดาห์</mat-option>
</mat-card> <mat-option value="WorkWeek">วันทำงาน</mat-option>
<mat-option value="Month">รายเดือน</mat-option>
<mat-option value="Agenda">วาระการประชุม</mat-option>
</mat-select>
</mat-form-field>
</div> </div>
</div> </div>
</mat-tab>
<!-- Statistics Tab --> <!-- Syncfusion Schedule Component -->
<mat-tab label="สถิติ"> <div class="schedule-wrapper">
<div class="row mt-3"> <ejs-schedule
<div class="col-12"> #scheduleObj
<mat-card> [selectedDate]="selectedDate_schedule"
<mat-card-header> [currentView]="currentView"
<mat-card-title>สถิติการจองห้องประชุม</mat-card-title> [eventSettings]="eventSettings"
<mat-card-subtitle>ข้อมูลสถิติการใช้งานห้องประชุม</mat-card-subtitle> [showQuickInfo]="showQuickInfo"
</mat-card-header> [allowDragAndDrop]="allowDragAndDrop"
<mat-card-content> (eventClick)="onEventClick($event)"
<div *ngIf="statistics$ | async as stats" class="row"> (eventCreate)="onEventCreate($event)"
<div class="col-md-3"> (eventUpdate)="onEventUpdate($event)"
<div class="stat-card"> (eventDelete)="onEventDelete($event)"
<div class="stat-number">{{ stats.totalBookings }}</div> (viewChange)="onViewChange($event)"
<div class="stat-label">การจองทั้งหมด</div> (dateChange)="onDateChange_schedule($event)"
</div> height="700px"
width="100%"
cssClass="custom-schedule">
<!-- Event Template -->
<ng-template #eventTemplate let-data>
<div class="event-template">
<div class="event-title">{{ data.Subject }}</div>
<div class="event-location">{{ data.Location }}</div>
<div class="event-time">
{{ data.StartTime | date:'HH:mm' }} - {{ data.EndTime | date:'HH:mm' }}
</div> </div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.confirmedBookings }}</div>
<div class="stat-label">ยืนยันแล้ว</div>
</div> </div>
</ng-template>
<!-- Quick Info Template -->
<ng-template #quickInfoTemplate let-data>
<div class="quick-info-template">
<div class="quick-info-header">
<h6>{{ data.Subject }}</h6>
<span class="status-badge status-{{ data.Status }}">
{{ getStatusText(data.Status) }}
</span>
</div> </div>
<div class="col-md-3"> <div class="quick-info-content">
<div class="stat-card"> <div class="info-item">
<div class="stat-number">{{ stats.pendingBookings }}</div> <mat-icon>location_on</mat-icon>
<div class="stat-label">รอดำเนินการ</div> <span>{{ data.Location }}</span>
</div> </div>
<div class="info-item">
<mat-icon>schedule</mat-icon>
<span>{{ data.StartTime | date:'dd/MM/yyyy HH:mm' }} - {{ data.EndTime | date:'dd/MM/yyyy HH:mm' }}</span>
</div> </div>
<div class="col-md-3"> <div class="info-item" *ngIf="data.Description">
<div class="stat-card"> <mat-icon>description</mat-icon>
<div class="stat-number">{{ stats.averageBookingDuration | number:'1.0-0' }} นาที</div> <span>{{ data.Description }}</span>
<div class="stat-label">ระยะเวลาเฉลี่ย</div>
</div> </div>
<div class="info-item">
<mat-icon>person</mat-icon>
<span>{{ data.Organizer }}</span>
</div> </div>
<div class="info-item" *ngIf="data.Attendees">
<mat-icon>group</mat-icon>
<span>{{ data.Attendees }}</span>
</div> </div>
</mat-card-content>
</mat-card>
</div> </div>
<div class="quick-info-actions">
<button mat-button color="primary" (click)="onEventClick(data)">
<mat-icon>visibility</mat-icon>
ดูรายละเอียด
</button>
<button mat-button color="warn" *ngIf="data.Status === 'pending' || data.Status === 'confirmed'">
<mat-icon>cancel</mat-icon>
ยกเลิก
</button>
</div> </div>
</mat-tab>
</mat-tab-group>
</div> </div>
</ng-template>
</ejs-schedule>
</div> </div>
</div> </div>
</div> </div>
......
.time-slots { // Modern Meeting Room Booking System Styles
max-height: 400px; .meeting-booking-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
font-family: 'Noto Sans Thai', sans-serif;
}
// Header Section
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem 0;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
.header-title {
h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 1rem;
i {
font-size: 2rem;
}
}
p {
margin: 0.5rem 0 0 0;
font-size: 1.1rem;
opacity: 0.9;
}
}
.header-actions {
display: flex;
gap: 1rem;
button {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
}
}
}
}
}
// Main Content
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
// Booking Form Modal
.booking-form-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(5px);
.booking-form-container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto; overflow-y: auto;
.time-slot { .form-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 8px 12px; padding: 1.5rem 2rem;
margin-bottom: 4px; border-bottom: 1px solid #e9ecef;
border-radius: 4px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: pointer; color: white;
transition: all 0.2s ease; border-radius: 16px 16px 0 0;
h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
button {
color: white;
}
}
.booking-form {
padding: 2rem;
&.available { .form-row {
background-color: #e8f5e8; display: grid;
border: 1px solid #4caf50; grid-template-columns: 1fr 1fr;
color: #2e7d32; gap: 1rem;
margin-bottom: 1rem;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.form-field {
width: 100%;
margin-bottom: 1rem;
}
.form-field-full {
width: 100%;
margin-bottom: 1rem;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #e9ecef;
button {
padding: 0.75rem 1.5rem;
font-weight: 600;
border-radius: 8px;
transition: all 0.3s ease;
&:hover { &:hover {
background-color: #c8e6c9;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} }
} }
}
}
}
}
// Schedule Section
.schedule-section {
background: white;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
overflow: hidden;
margin-bottom: 2rem;
.schedule-header {
padding: 2rem;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-bottom: 1px solid #dee2e6;
&.unavailable { .schedule-title {
background-color: #ffebee; margin-bottom: 1.5rem;
border: 1px solid #f44336;
color: #c62828; h2 {
cursor: not-allowed; margin: 0 0 0.5rem 0;
opacity: 0.6; font-size: 1.8rem;
font-weight: 700;
color: #495057;
display: flex;
align-items: center;
gap: 0.5rem;
} }
.time { p {
font-weight: 500; margin: 0;
color: #6c757d;
font-size: 1rem;
}
} }
mat-icon { .schedule-controls {
font-size: 18px; display: flex;
width: 18px; gap: 1rem;
height: 18px; flex-wrap: wrap;
.control-field {
min-width: 200px;
}
} }
} }
.schedule-wrapper {
padding: 1rem;
}
} }
.stat-card { // Statistics Section
text-align: center; .statistics-section {
padding: 20px; margin-bottom: 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: white;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
padding: 2rem;
overflow: hidden;
.stats-title {
margin-bottom: 1.5rem;
h3 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #495057;
display: flex;
align-items: center;
gap: 0.5rem;
}
}
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
.stat-card {
background: white;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 1rem;
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white; color: white;
border-radius: 8px;
margin-bottom: 16px;
mat-icon {
font-size: 2rem;
width: 2rem;
height: 2rem;
}
}
.stat-content {
.stat-number { .stat-number {
font-size: 2.5rem; font-size: 2rem;
font-weight: bold; font-weight: 700;
margin-bottom: 8px; color: #495057;
margin-bottom: 0.25rem;
} }
.stat-label { .stat-label {
font-size: 0.9rem; font-size: 0.9rem;
opacity: 0.9; color: #6c757d;
font-weight: 500;
}
}
&:nth-child(1) .stat-icon {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&:nth-child(2) .stat-icon {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&:nth-child(3) .stat-icon {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
&:nth-child(4) .stat-icon {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
}
} }
} }
.badge { // Syncfusion Schedule Custom Styling
padding: 4px 8px; ::ng-deep {
.custom-schedule {
.e-schedule {
border-radius: 12px; border-radius: 12px;
font-size: 0.75rem; font-family: 'Noto Sans Thai', sans-serif;
font-weight: 500; border: 1px solid #e9ecef;
}
&.badge-success { .e-schedule .e-header-cells {
background-color: #4caf50; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
font-weight: 600;
border: none;
} }
&.badge-warning { .e-schedule .e-date-header {
background-color: #ff9800; background-color: #f8f9fa;
color: white; color: #495057;
font-weight: 600;
} }
&.badge-danger { .e-schedule .e-work-cells {
background-color: #f44336; background-color: #ffffff;
color: white; border-color: #e9ecef;
} }
&.badge-info { .e-schedule .e-work-cells:hover {
background-color: #2196f3; background-color: #f8f9fa;
color: white;
} }
&.badge-secondary { .e-schedule .e-current-day {
background-color: #6c757d; background-color: #e3f2fd !important;
color: white;
} }
}
.table-responsive { .e-schedule .e-selected-date {
overflow-x: auto; background-color: #bbdefb !important;
} }
mat-card { // Event styling
margin-bottom: 16px; .e-schedule .e-appointment {
} border-radius: 8px;
border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
mat-card-header { .e-schedule .e-appointment .e-appointment-details {
margin-bottom: 16px; padding: 6px 10px;
} }
// Event templates
.event-template {
padding: 6px 10px;
font-size: 12px;
line-height: 1.3;
.event-title {
font-weight: 700;
color: #ffffff;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
}
mat-form-field { .event-location {
color: rgba(255, 255, 255, 0.9);
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
.event-time {
color: rgba(255, 255, 255, 0.8);
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// Quick info template
.quick-info-template {
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
min-width: 350px;
border: 1px solid #e9ecef;
.quick-info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px; margin-bottom: 16px;
} padding-bottom: 12px;
border-bottom: 2px solid #e9ecef;
// Time slot grid h6 {
.time-slots { margin: 0;
display: grid; color: #495057;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); font-weight: 700;
gap: 8px; font-size: 1.1rem;
} }
// Responsive design .status-badge {
@media (max-width: 768px) { padding: 4px 12px;
.time-slots { border-radius: 20px;
grid-template-columns: 1fr; font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
&.status-confirmed {
background-color: #d4edda;
color: #155724;
} }
.stat-card { &.status-pending {
background-color: #fff3cd;
color: #856404;
}
&.status-cancelled {
background-color: #f8d7da;
color: #721c24;
}
}
}
.quick-info-content {
.info-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px; margin-bottom: 12px;
color: #6c757d;
font-size: 14px;
.stat-number { mat-icon {
font-size: 2rem; font-size: 18px;
width: 18px;
height: 18px;
color: #667eea;
}
span {
font-weight: 500;
}
} }
} }
}
// Form styling .quick-info-actions {
form { display: flex;
.row { gap: 8px;
margin-bottom: 16px; margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #e9ecef;
button {
padding: 8px 16px;
font-size: 0.9rem;
font-weight: 600;
border-radius: 6px;
}
}
} }
}
// Button styling // Status colors for events
button[mat-raised-button] { .e-appointment[data-category="success"] {
margin-right: 8px; background: linear-gradient(135deg, #4caf50 0%, #45a049 100%) !important;
} }
// Loading spinner .e-appointment[data-category="warning"] {
mat-spinner { background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%) !important;
margin: 0 auto; }
.e-appointment[data-category="danger"] {
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%) !important;
}
.e-appointment[data-category="info"] {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%) !important;
}
.e-appointment[data-category="secondary"] {
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%) !important;
}
// Toolbar styling
.e-schedule .e-toolbar {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-bottom: 2px solid #dee2e6;
padding: 12px 20px;
}
.e-schedule .e-toolbar .e-toolbar-item {
color: #495057;
font-weight: 600;
}
.e-schedule .e-toolbar .e-toolbar-item:hover {
background-color: #e9ecef;
border-radius: 6px;
}
// View switcher
.e-schedule .e-view-switcher {
.e-toolbar-item {
margin: 0 4px;
padding: 8px 16px;
border-radius: 6px;
transition: all 0.3s ease;
font-weight: 600;
&.e-active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
}
}
// Date navigator
.e-schedule .e-date-navigator {
.e-toolbar-item {
font-weight: 700;
color: #495057;
font-size: 1.1rem;
}
}
}
} }
// Empty state // Responsive Design
.text-center { @media (max-width: 768px) {
.page-header .header-content {
flex-direction: column;
gap: 1rem;
text-align: center; text-align: center;
} }
.text-muted { .main-content {
color: #6c757d; padding: 1rem;
} }
.schedule-section .schedule-header .schedule-controls {
flex-direction: column;
}
// Card hover effects .stats-cards {
mat-card:hover { grid-template-columns: 1fr;
box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
transition: box-shadow 0.2s ease;
.booking-form-container {
margin: 1rem;
width: calc(100% - 2rem);
}
} }
...@@ -21,6 +21,11 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; ...@@ -21,6 +21,11 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
// Syncfusion Schedule imports
import { ScheduleModule, View, EventSettingsModel, DayService, WeekService, WorkWeekService, MonthService, AgendaService, ResizeService, DragAndDropService } from '@syncfusion/ej2-angular-schedule';
import { DateTimePickerModule } from '@syncfusion/ej2-angular-calendars';
import { DropDownListModule } from '@syncfusion/ej2-angular-dropdowns';
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';
...@@ -47,7 +52,20 @@ import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from ...@@ -47,7 +52,20 @@ import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from
MatTabsModule, MatTabsModule,
MatSnackBarModule, MatSnackBarModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
TranslateModule TranslateModule,
// Syncfusion modules
ScheduleModule,
DateTimePickerModule,
DropDownListModule
],
providers: [
DayService,
WeekService,
WorkWeekService,
MonthService,
AgendaService,
ResizeService,
DragAndDropService
], ],
templateUrl: './meeting-booking.component.html', templateUrl: './meeting-booking.component.html',
styleUrls: ['./meeting-booking.component.scss'] styleUrls: ['./meeting-booking.component.scss']
...@@ -69,6 +87,19 @@ export class MeetingBookingComponent implements OnInit { ...@@ -69,6 +87,19 @@ export class MeetingBookingComponent implements OnInit {
separatorKeysCodes: number[] = [13, 188]; // ENTER and COMMA key codes separatorKeysCodes: number[] = [13, 188]; // ENTER and COMMA key codes
// Syncfusion Schedule properties
public selectedDate_schedule: Date = new Date();
public currentView: View = 'Month';
public eventSettings: EventSettingsModel = {
dataSource: []
};
public scheduleData: any[] = [];
public showQuickInfo: boolean = true;
public allowDragAndDrop: boolean = true;
// UI State properties
public showBookingForm: boolean = false;
constructor( constructor(
private meetingBookingService: MeetingBookingService, private meetingBookingService: MeetingBookingService,
private fb: FormBuilder, private fb: FormBuilder,
...@@ -95,6 +126,8 @@ export class MeetingBookingComponent implements OnInit { ...@@ -95,6 +126,8 @@ export class MeetingBookingComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.loadTimeSlots(); this.loadTimeSlots();
this.loadScheduleData();
this.loadSampleData(); // เพิ่มข้อมูลตัวอย่าง
} }
onDateChange(): void { onDateChange(): void {
...@@ -233,4 +266,167 @@ export class MeetingBookingComponent implements OnInit { ...@@ -233,4 +266,167 @@ export class MeetingBookingComponent implements OnInit {
}); });
return roomName; return roomName;
} }
// Syncfusion Schedule methods
loadScheduleData(): void {
if (this.bookings$) {
this.bookings$.subscribe(bookings => {
if (bookings && bookings.length > 0) {
this.scheduleData = bookings.map(booking => ({
Id: booking.id,
Subject: booking.title,
StartTime: new Date(booking.startDateTime),
EndTime: new Date(booking.endDateTime),
Location: booking.roomName,
Description: booking.description,
IsAllDay: false,
CategoryColor: this.getStatusColor(booking.status),
Status: booking.status,
Organizer: booking.organizerName,
Attendees: booking.attendees.map(a => a.userName).join(', ')
}));
} else {
// ถ้าไม่มีข้อมูลจาก service ให้ใช้ข้อมูลตัวอย่าง
this.loadSampleData();
}
this.eventSettings = {
dataSource: this.scheduleData
};
console.log('Schedule data loaded:', this.scheduleData);
});
} else {
// ถ้าไม่มี bookings$ observable ให้ใช้ข้อมูลตัวอย่าง
this.loadSampleData();
}
}
onEventClick(args: any): void {
console.log('Event clicked:', args);
// Handle event click - show details or edit
this.snackBar.open(`ดูรายละเอียดการจอง: ${args.event.Subject}`, 'ปิด', { duration: 3000 });
}
onEventCreate(args: any): void {
console.log('Event created:', args);
// Handle new event creation
const newEvent = args.data;
const booking = {
roomId: this.selectedRoom,
roomName: newEvent.Location || '',
title: newEvent.Subject,
description: newEvent.Description || '',
startDateTime: newEvent.StartTime,
endDateTime: newEvent.EndTime,
organizerId: 'current-user',
organizerName: 'Current User',
attendees: [],
status: 'pending' as const
};
this.meetingBookingService.createBooking(booking).subscribe({
next: () => {
this.snackBar.open('จองห้องประชุมสำเร็จ', 'ปิด', { duration: 3000 });
this.loadScheduleData();
},
error: (error) => {
this.snackBar.open('เกิดข้อผิดพลาดในการจอง: ' + error.message, 'ปิด', { duration: 5000 });
}
});
}
onEventUpdate(args: any): void {
console.log('Event updated:', args);
// Handle event update
}
onEventDelete(args: any): void {
console.log('Event deleted:', args);
// Handle event deletion
const eventId = args.data.Id;
this.cancelBooking(eventId);
}
onViewChange(args: any): void {
console.log('View changed:', args);
this.currentView = args.currentView;
}
onDateChange_schedule(args: any): void {
console.log('Date changed:', args);
this.selectedDate_schedule = args.selectedDate;
}
// ฟังก์ชันสำหรับโหลดข้อมูลตัวอย่าง
loadSampleData(): void {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const nextWeek = new Date(today);
nextWeek.setDate(nextWeek.getDate() + 7);
const sampleData = [
{
Id: 1,
Subject: 'ประชุมทีมพัฒนา',
StartTime: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 9, 0),
EndTime: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 10, 30),
Location: 'ห้องประชุม A',
Description: 'ประชุมทบทวนความคืบหน้าโปรเจค',
IsAllDay: false,
CategoryColor: 'success',
Status: 'confirmed',
Organizer: 'John Doe',
Attendees: 'Jane Smith, Bob Johnson'
},
{
Id: 2,
Subject: 'การนำเสนอผลงาน',
StartTime: new Date(tomorrow.getFullYear(), tomorrow.getMonth(), tomorrow.getDate(), 14, 0),
EndTime: new Date(tomorrow.getFullYear(), tomorrow.getMonth(), tomorrow.getDate(), 15, 30),
Location: 'ห้องประชุม B',
Description: 'นำเสนอผลงานไตรมาสที่ 1',
IsAllDay: false,
CategoryColor: 'info',
Status: 'pending',
Organizer: 'Alice Brown',
Attendees: 'Charlie Wilson, Diana Lee'
},
{
Id: 3,
Subject: 'อบรมเทคโนโลยีใหม่',
StartTime: new Date(nextWeek.getFullYear(), nextWeek.getMonth(), nextWeek.getDate(), 10, 0),
EndTime: new Date(nextWeek.getFullYear(), nextWeek.getMonth(), nextWeek.getDate(), 12, 0),
Location: 'ห้องประชุม C',
Description: 'อบรม Angular 17 และ TypeScript',
IsAllDay: false,
CategoryColor: 'warning',
Status: 'confirmed',
Organizer: 'Tech Lead',
Attendees: 'ทีมพัฒนา'
}
];
this.scheduleData = sampleData;
this.eventSettings = {
dataSource: this.scheduleData
};
console.log('Sample data loaded:', this.scheduleData);
}
// Statistics methods
getConfirmedCount(): number {
return this.scheduleData.filter(booking => booking.Status === 'confirmed').length;
}
getPendingCount(): number {
return this.scheduleData.filter(booking => booking.Status === 'pending').length;
}
getUniqueRoomsCount(): number {
const uniqueRooms = new Set(this.scheduleData.map(booking => booking.Location));
return uniqueRooms.size;
}
} }
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