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