Commit 1e4426e9 by sawit

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

parent 26ac6e17
......@@ -48,6 +48,7 @@
"@syncfusion/ej2-angular-pivotview": "^31.1.17",
"@syncfusion/ej2-angular-popups": "^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-base": "^31.1.17",
"@syncfusion/ej2-buttons": "^31.1.17",
......@@ -6334,6 +6335,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": {
"version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-treemap/-/ej2-angular-treemap-31.1.17.tgz",
......@@ -6346,9 +6358,9 @@
}
},
"node_modules/@syncfusion/ej2-base": {
"version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-base/-/ej2-base-31.1.17.tgz",
"integrity": "sha512-Jl2Ls77kMfX13VwrAEow/kdDrefX3gh76Uwh943NUtEuChOXzvoe+CHecxJ+49VFRCrLYhB4Hj/7R9UOh65VCg==",
"version": "31.1.22",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-base/-/ej2-base-31.1.22.tgz",
"integrity": "sha512-zgNULXYUFG8Cr2ML8VcUqj8EcQSVd/eXNnD8APZdOjELJPE1ZzeNmRzCab16A/hqCZ+wAXwKmjaTC0I6xiXjSQ==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-icons": "~31.1.17"
......@@ -6611,6 +6623,97 @@
"@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": {
"version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-splitbuttons/-/ej2-splitbuttons-31.1.17.tgz",
......
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">ระบบจองห้องประชุม</h4>
<p class="card-subtitle">จองห้องประชุมและจัดการการประชุมของคุณ</p>
<!-- Meeting Room Booking System with Syncfusion Schedule -->
<div class="meeting-booking-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-title">
<h1><i class="fas fa-calendar-alt"></i> ระบบจองห้องประชุม</h1>
<p>จัดการการจองห้องประชุมอย่างมีประสิทธิภาพด้วย Syncfusion Schedule</p>
</div>
<div class="header-actions">
<button mat-raised-button color="primary" (click)="showBookingForm = true">
<mat-icon>add</mat-icon>
จองห้องประชุม
</button>
<button mat-raised-button color="accent" (click)="loadSampleData()">
<mat-icon>refresh</mat-icon>
โหลดข้อมูลตัวอย่าง
</button>
</div>
</div>
</div>
<!-- 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>
<div class="card-body">
<mat-tab-group>
<!-- Booking Tab -->
<mat-tab label="จองห้องประชุม">
<div class="row mt-3">
<div class="col-md-8">
<mat-card>
<mat-card-header>
<mat-card-title>ข้อมูลการจอง</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="bookingForm" (ngSubmit)="createBooking()">
<div class="row">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ห้องประชุม</mat-label>
<mat-select formControlName="roomId" (selectionChange)="onRoomChange()">
<mat-option *ngFor="let room of rooms$ | async" [value]="room.id">
{{ room.name }} ({{ room.capacity }} ที่นั่ง) - {{ room.location }}
</mat-option>
</mat-select>
</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">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>หัวข้อการประชุม</mat-label>
<input matInput formControlName="title" placeholder="ระบุหัวข้อการประชุม">
</mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>รายละเอียด</mat-label>
<input matInput formControlName="description" placeholder="รายละเอียดเพิ่มเติม (ไม่บังคับ)">
</mat-form-field>
</div>
</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-select formControlName="roomId" (selectionChange)="onRoomChange()">
<mat-option *ngFor="let room of rooms$ | async" [value]="room.id">
{{ room.name }} ({{ room.capacity }} ที่นั่ง) - {{ room.location }}
</mat-option>
</mat-select>
</mat-form-field>
<div class="row">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>เวลาเริ่มต้น</mat-label>
<input matInput formControlName="startDateTime" type="datetime-local">
</mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>เวลาสิ้นสุด</mat-label>
<input matInput formControlName="endDateTime" type="datetime-local">
</mat-form-field>
</div>
</div>
<mat-form-field appearance="outline" class="form-field">
<mat-label>หัวข้อการประชุม</mat-label>
<input matInput formControlName="title" placeholder="ระบุหัวข้อการประชุม">
</mat-form-field>
</div>
<div class="row">
<div class="col-12">
<mat-form-field appearance="outline" class="w-100">
<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>
</div>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="form-field">
<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>
<div class="row mt-3">
<div class="col-12">
<button mat-raised-button color="primary" type="submit"
[disabled]="!bookingForm.valid">
<mat-icon>event</mat-icon>
จองห้องประชุม
</button>
<button mat-button type="button" (click)="bookingForm.reset()">
<mat-icon>refresh</mat-icon>
รีเซ็ต
</button>
</div>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<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>
</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 class="form-row">
<mat-form-field appearance="outline" class="form-field">
<mat-label>เวลาเริ่มต้น</mat-label>
<input matInput type="time" formControlName="startTime">
</mat-form-field>
<div *ngIf="!isLoading && timeSlots.length > 0" class="time-slots">
<div *ngFor="let slot of timeSlots"
class="time-slot"
[class.available]="slot.isAvailable"
[class.unavailable]="!slot.isAvailable"
(click)="selectTimeSlot(slot)">
<span class="time">{{ slot.startTime }} - {{ slot.endTime }}</span>
<mat-icon *ngIf="slot.isAvailable">check_circle</mat-icon>
<mat-icon *ngIf="!slot.isAvailable">cancel</mat-icon>
</div>
</div>
<mat-form-field appearance="outline" class="form-field">
<mat-label>เวลาสิ้นสุด</mat-label>
<input matInput type="time" formControlName="endTime">
</mat-form-field>
</div>
<div *ngIf="!isLoading && timeSlots.length === 0" class="text-center text-muted">
<mat-icon>event_busy</mat-icon>
<p>ไม่มีช่วงเวลาที่ว่างในวันนี้</p>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
<mat-form-field appearance="outline" class="form-field-full">
<mat-label>รายละเอียด</mat-label>
<textarea matInput formControlName="description" rows="3" placeholder="รายละเอียดเพิ่มเติม (ไม่บังคับ)"></textarea>
</mat-form-field>
<!-- 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>
<div class="form-actions">
<button mat-raised-button color="primary" type="submit" [disabled]="!bookingForm.valid">
<mat-icon>event</mat-icon>
จองห้องประชุม
</button>
<button mat-button type="button" (click)="bookingForm.reset()">
<mat-icon>refresh</mat-icon>
รีเซ็ต
</button>
<button mat-button type="button" (click)="showBookingForm = false">
<mat-icon>close</mat-icon>
ยกเลิก
</button>
</div>
</form>
</div>
</div>
<!-- Statistics Cards -->
<div class="statistics-section" *ngIf="scheduleData.length > 0">
<div class="stats-title">
<h3><mat-icon>analytics</mat-icon> สถิติการจอง</h3>
</div>
<div class="stats-cards">
<div class="stat-card">
<div class="stat-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 class="stat-card">
<div class="stat-icon">
<mat-icon>check_circle</mat-icon>
</div>
<div class="stat-content">
<div class="stat-number">{{ getConfirmedCount() }}</div>
<div class="stat-label">ยืนยันแล้ว</div>
</div>
</div>
<ng-container matColumnDef="room">
<th mat-header-cell *matHeaderCellDef>ห้องประชุม</th>
<td mat-cell *matCellDef="let booking">{{ booking.roomName }}</td>
</ng-container>
<div class="stat-card">
<div class="stat-icon">
<mat-icon>pending</mat-icon>
</div>
<div class="stat-content">
<div class="stat-number">{{ getPendingCount() }}</div>
<div class="stat-label">รอดำเนินการ</div>
</div>
</div>
<ng-container matColumnDef="startTime">
<th mat-header-cell *matHeaderCellDef>เวลาเริ่มต้น</th>
<td mat-cell *matCellDef="let booking">
{{ booking.startDateTime | date:'dd/MM/yyyy HH:mm' }}
</td>
</ng-container>
<div class="stat-card">
<div class="stat-icon">
<mat-icon>room</mat-icon>
</div>
<div class="stat-content">
<div class="stat-number">{{ getUniqueRoomsCount() }}</div>
<div class="stat-label">ห้องที่ใช้งาน</div>
</div>
</div>
</div>
</div>
<ng-container matColumnDef="endTime">
<th mat-header-cell *matHeaderCellDef>เวลาสิ้นสุด</th>
<td mat-cell *matCellDef="let booking">
{{ booking.endDateTime | date:'dd/MM/yyyy HH:mm' }}
</td>
</ng-container>
<!-- Schedule View -->
<div class="schedule-section">
<div class="schedule-header">
<div class="schedule-title">
<h2><mat-icon>schedule</mat-icon> ปฏิทินการจองห้องประชุม</h2>
<p>ดูและจัดการการจองในรูปแบบปฏิทินแบบเต็มรูปแบบ</p>
</div>
<ng-container matColumnDef="organizer">
<th mat-header-cell *matHeaderCellDef>ผู้จัด</th>
<td mat-cell *matCellDef="let booking">{{ booking.organizerName }}</td>
</ng-container>
<div class="schedule-controls">
<mat-form-field appearance="outline" class="control-field">
<mat-label>กรองตามห้องประชุม</mat-label>
<mat-select [(value)]="selectedRoom" (selectionChange)="onRoomChange()">
<mat-option value="">ทั้งหมด</mat-option>
<mat-option *ngFor="let room of rooms$ | async" [value]="room.id">
{{ room.name }}
</mat-option>
</mat-select>
</mat-form-field>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>สถานะ</th>
<td mat-cell *matCellDef="let booking">
<span class="badge badge-{{ getStatusColor(booking.status) }}">
{{ getStatusText(booking.status) }}
</span>
</td>
</ng-container>
<mat-form-field appearance="outline" class="control-field">
<mat-label>มุมมอง</mat-label>
<mat-select [(value)]="currentView" (selectionChange)="onViewChange($event)">
<mat-option value="Day">รายวัน</mat-option>
<mat-option value="Week">รายสัปดาห์</mat-option>
<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>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>การดำเนินการ</th>
<td mat-cell *matCellDef="let booking">
<button mat-icon-button
*ngIf="booking.status === 'pending' || booking.status === 'confirmed'"
(click)="cancelBooking(booking.id)"
matTooltip="ยกเลิกการจอง">
<mat-icon>cancel</mat-icon>
</button>
<button mat-icon-button matTooltip="ดูรายละเอียด">
<mat-icon>visibility</mat-icon>
</button>
</td>
</ng-container>
<!-- Syncfusion Schedule Component -->
<div class="schedule-wrapper">
<ejs-schedule
#scheduleObj
[selectedDate]="selectedDate_schedule"
[currentView]="currentView"
[eventSettings]="eventSettings"
[showQuickInfo]="showQuickInfo"
[allowDragAndDrop]="allowDragAndDrop"
(eventClick)="onEventClick($event)"
(eventCreate)="onEventCreate($event)"
(eventUpdate)="onEventUpdate($event)"
(eventDelete)="onEventDelete($event)"
(viewChange)="onViewChange($event)"
(dateChange)="onDateChange_schedule($event)"
height="700px"
width="100%"
cssClass="custom-schedule">
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
<!-- 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>
</mat-tab>
</div>
</ng-template>
<!-- Statistics 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-subtitle>ข้อมูลสถิติการใช้งานห้องประชุม</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="statistics$ | async as stats" class="row">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.totalBookings }}</div>
<div class="stat-label">การจองทั้งหมด</div>
</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>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.pendingBookings }}</div>
<div class="stat-label">รอดำเนินการ</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.averageBookingDuration | number:'1.0-0' }} นาที</div>
<div class="stat-label">ระยะเวลาเฉลี่ย</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
<!-- 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 class="quick-info-content">
<div class="info-item">
<mat-icon>location_on</mat-icon>
<span>{{ data.Location }}</span>
</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 class="info-item" *ngIf="data.Description">
<mat-icon>description</mat-icon>
<span>{{ data.Description }}</span>
</div>
<div class="info-item">
<mat-icon>person</mat-icon>
<span>{{ data.Organizer }}</span>
</div>
<div class="info-item" *ngIf="data.Attendees">
<mat-icon>group</mat-icon>
<span>{{ data.Attendees }}</span>
</div>
</div>
</mat-tab>
</mat-tab-group>
</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>
</ng-template>
</ejs-schedule>
</div>
</div>
</div>
......
.time-slots {
max-height: 400px;
overflow-y: auto;
// Modern Meeting Room Booking System Styles
.meeting-booking-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
font-family: 'Noto Sans Thai', sans-serif;
}
.time-slot {
// 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;
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
&.available {
background-color: #e8f5e8;
border: 1px solid #4caf50;
color: #2e7d32;
.header-title {
h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 1rem;
&:hover {
background-color: #c8e6c9;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
i {
font-size: 2rem;
}
}
p {
margin: 0.5rem 0 0 0;
font-size: 1.1rem;
opacity: 0.9;
}
}
&.unavailable {
background-color: #ffebee;
border: 1px solid #f44336;
color: #c62828;
cursor: not-allowed;
opacity: 0.6;
.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;
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e9ecef;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 16px 16px 0 0;
.time {
font-weight: 500;
h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
button {
color: white;
}
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
.booking-form {
padding: 2rem;
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
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 {
transform: translateY(-1px);
}
}
}
}
}
}
.stat-card {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
margin-bottom: 16px;
// 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;
.schedule-title {
margin-bottom: 1.5rem;
.stat-number {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 8px;
h2 {
margin: 0 0 0.5rem 0;
font-size: 1.8rem;
font-weight: 700;
color: #495057;
display: flex;
align-items: center;
gap: 0.5rem;
}
p {
margin: 0;
color: #6c757d;
font-size: 1rem;
}
}
.schedule-controls {
display: flex;
gap: 1rem;
flex-wrap: wrap;
.control-field {
min-width: 200px;
}
}
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
.schedule-wrapper {
padding: 1rem;
}
}
.badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
// Statistics Section
.statistics-section {
margin-bottom: 2rem;
background: white;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
padding: 2rem;
overflow: hidden;
&.badge-success {
background-color: #4caf50;
color: white;
}
.stats-title {
margin-bottom: 1.5rem;
&.badge-warning {
background-color: #ff9800;
color: white;
h3 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #495057;
display: flex;
align-items: center;
gap: 0.5rem;
}
}
&.badge-danger {
background-color: #f44336;
color: white;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
&.badge-info {
background-color: #2196f3;
color: white;
}
.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;
&.badge-secondary {
background-color: #6c757d;
color: white;
}
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
}
.table-responsive {
overflow-x: auto;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
mat-card {
margin-bottom: 16px;
}
mat-icon {
font-size: 2rem;
width: 2rem;
height: 2rem;
}
}
mat-card-header {
margin-bottom: 16px;
}
.stat-content {
.stat-number {
font-size: 2rem;
font-weight: 700;
color: #495057;
margin-bottom: 0.25rem;
}
mat-form-field {
margin-bottom: 16px;
}
.stat-label {
font-size: 0.9rem;
color: #6c757d;
font-weight: 500;
}
}
// Time slot grid
.time-slots {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
}
&:nth-child(1) .stat-icon {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
// Responsive design
@media (max-width: 768px) {
.time-slots {
grid-template-columns: 1fr;
}
&:nth-child(2) .stat-icon {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-card {
margin-bottom: 12px;
&:nth-child(3) .stat-icon {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.stat-number {
font-size: 2rem;
&:nth-child(4) .stat-icon {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
}
}
}
// Form styling
form {
.row {
margin-bottom: 16px;
// Syncfusion Schedule Custom Styling
::ng-deep {
.custom-schedule {
.e-schedule {
border-radius: 12px;
font-family: 'Noto Sans Thai', sans-serif;
border: 1px solid #e9ecef;
}
.e-schedule .e-header-cells {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
border: none;
}
.e-schedule .e-date-header {
background-color: #f8f9fa;
color: #495057;
font-weight: 600;
}
.e-schedule .e-work-cells {
background-color: #ffffff;
border-color: #e9ecef;
}
.e-schedule .e-work-cells:hover {
background-color: #f8f9fa;
}
.e-schedule .e-current-day {
background-color: #e3f2fd !important;
}
.e-schedule .e-selected-date {
background-color: #bbdefb !important;
}
// Event styling
.e-schedule .e-appointment {
border-radius: 8px;
border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.e-schedule .e-appointment .e-appointment-details {
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;
}
.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;
padding-bottom: 12px;
border-bottom: 2px solid #e9ecef;
h6 {
margin: 0;
color: #495057;
font-weight: 700;
font-size: 1.1rem;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
&.status-confirmed {
background-color: #d4edda;
color: #155724;
}
&.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;
color: #6c757d;
font-size: 14px;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: #667eea;
}
span {
font-weight: 500;
}
}
}
.quick-info-actions {
display: flex;
gap: 8px;
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;
}
}
}
// Status colors for events
.e-appointment[data-category="success"] {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%) !important;
}
.e-appointment[data-category="warning"] {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%) !important;
}
.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;
}
}
}
}
// Button styling
button[mat-raised-button] {
margin-right: 8px;
}
// Responsive Design
@media (max-width: 768px) {
.page-header .header-content {
flex-direction: column;
gap: 1rem;
text-align: center;
}
// Loading spinner
mat-spinner {
margin: 0 auto;
}
.main-content {
padding: 1rem;
}
// Empty state
.text-center {
text-align: center;
}
.schedule-section .schedule-header .schedule-controls {
flex-direction: column;
}
.text-muted {
color: #6c757d;
}
.stats-cards {
grid-template-columns: 1fr;
}
// Card hover effects
mat-card:hover {
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';
import { TranslateModule } from '@ngx-translate/core';
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 { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from '../models/meeting-booking.model';
......@@ -47,7 +52,20 @@ import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from
MatTabsModule,
MatSnackBarModule,
MatProgressSpinnerModule,
TranslateModule
TranslateModule,
// Syncfusion modules
ScheduleModule,
DateTimePickerModule,
DropDownListModule
],
providers: [
DayService,
WeekService,
WorkWeekService,
MonthService,
AgendaService,
ResizeService,
DragAndDropService
],
templateUrl: './meeting-booking.component.html',
styleUrls: ['./meeting-booking.component.scss']
......@@ -69,6 +87,19 @@ export class MeetingBookingComponent implements OnInit {
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(
private meetingBookingService: MeetingBookingService,
private fb: FormBuilder,
......@@ -95,6 +126,8 @@ export class MeetingBookingComponent implements OnInit {
ngOnInit(): void {
this.loadTimeSlots();
this.loadScheduleData();
this.loadSampleData(); // เพิ่มข้อมูลตัวอย่าง
}
onDateChange(): void {
......@@ -233,4 +266,167 @@ export class MeetingBookingComponent implements OnInit {
});
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