Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
H
Hire-Guru
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vigneswaran Shanmugam
Hire-Guru
Commits
68e5aeae
You need to sign in or sign up before continuing.
Commit
68e5aeae
authored
May 19, 2026
by
Aravind RK
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor: minor ui bug fixes and changes
parent
99f0042f
Changes
29
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
409 additions
and
463 deletions
+409
-463
Backend/models/Interview.js
Backend/models/Interview.js
+1
-0
Backend/routes/admin.js
Backend/routes/admin.js
+14
-11
Backend/routes/candidate.js
Backend/routes/candidate.js
+61
-29
Backend/routes/hr.js
Backend/routes/hr.js
+4
-1
Backend/routes/interview.js
Backend/routes/interview.js
+4
-2
Frontend/src/app/components/layout/layout.css
Frontend/src/app/components/layout/layout.css
+101
-0
Frontend/src/app/components/layout/layout.html
Frontend/src/app/components/layout/layout.html
+27
-1
Frontend/src/app/components/layout/layout.ts
Frontend/src/app/components/layout/layout.ts
+6
-0
Frontend/src/app/pages/admin/dashboard/dashboard.css
Frontend/src/app/pages/admin/dashboard/dashboard.css
+43
-4
Frontend/src/app/pages/admin/group-interview/group-interview.html
.../src/app/pages/admin/group-interview/group-interview.html
+5
-2
Frontend/src/app/pages/admin/group-interview/group-interview.ts
...nd/src/app/pages/admin/group-interview/group-interview.ts
+2
-1
Frontend/src/app/pages/admin/hr-users/hr-users.css
Frontend/src/app/pages/admin/hr-users/hr-users.css
+5
-3
Frontend/src/app/pages/admin/hr-users/hr-users.html
Frontend/src/app/pages/admin/hr-users/hr-users.html
+1
-1
Frontend/src/app/pages/admin/individual-interview/individual-interview.html
...ages/admin/individual-interview/individual-interview.html
+5
-2
Frontend/src/app/pages/admin/individual-interview/individual-interview.ts
.../pages/admin/individual-interview/individual-interview.ts
+2
-1
Frontend/src/app/pages/admin/manage-groups/manage-groups.css
Frontend/src/app/pages/admin/manage-groups/manage-groups.css
+1
-1
Frontend/src/app/pages/admin/manage-groups/manage-groups.ts
Frontend/src/app/pages/admin/manage-groups/manage-groups.ts
+3
-0
Frontend/src/app/pages/candidate/take-quiz/take-quiz.css
Frontend/src/app/pages/candidate/take-quiz/take-quiz.css
+10
-2
Frontend/src/app/pages/candidate/take-quiz/take-quiz.html
Frontend/src/app/pages/candidate/take-quiz/take-quiz.html
+3
-3
Frontend/src/app/pages/candidate/take-quiz/take-quiz.ts
Frontend/src/app/pages/candidate/take-quiz/take-quiz.ts
+47
-2
Frontend/src/app/pages/hr/dashboard/dashboard.css
Frontend/src/app/pages/hr/dashboard/dashboard.css
+44
-5
Frontend/src/app/pages/hr/group-interview/group-interview.html
...end/src/app/pages/hr/group-interview/group-interview.html
+5
-2
Frontend/src/app/pages/hr/group-interview/group-interview.ts
Frontend/src/app/pages/hr/group-interview/group-interview.ts
+2
-1
Frontend/src/app/pages/hr/individual-interview/individual-interview.html
...p/pages/hr/individual-interview/individual-interview.html
+5
-2
Frontend/src/app/pages/hr/individual-interview/individual-interview.ts
...app/pages/hr/individual-interview/individual-interview.ts
+2
-1
Frontend/src/app/pages/hr/manage-groups/manage-groups.css
Frontend/src/app/pages/hr/manage-groups/manage-groups.css
+1
-1
Frontend/src/app/pages/hr/manage-groups/manage-groups.ts
Frontend/src/app/pages/hr/manage-groups/manage-groups.ts
+3
-0
Frontend/src/styles.css
Frontend/src/styles.css
+2
-0
ITL_Intern_Evaluation_Form.html
ITL_Intern_Evaluation_Form.html
+0
-385
No files found.
Backend/models/Interview.js
View file @
68e5aeae
...
...
@@ -25,6 +25,7 @@ const interviewSchema = new mongoose.Schema({
techStack
:
{
type
:
String
,
default
:
''
,
trim
:
true
},
source
:
{
type
:
String
,
default
:
''
,
trim
:
true
},
dateOfInterview
:
{
type
:
Date
,
default
:
Date
.
now
},
timeOfInterview
:
{
type
:
String
,
default
:
''
},
// Quiz assignments (aptitude + technical)
quizzes
:
[{
...
...
Backend/routes/admin.js
View file @
68e5aeae
const
express
=
require
(
'
express
'
);
const
xlsx
=
require
(
'
xlsx
'
);
const
fs
=
require
(
'
fs
'
);
const
User
=
require
(
'
../models/User
'
);
const
Group
=
require
(
'
../models/Group
'
);
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
const
{
protect
,
authorize
}
=
require
(
'
../middleware/auth
'
);
const
upload
=
require
(
'
../middleware/upload
'
);
const
router
=
express
.
Router
();
const
express
=
require
(
'
express
'
);
const
xlsx
=
require
(
'
xlsx
'
);
const
fs
=
require
(
'
fs
'
);
const
User
=
require
(
'
../models/User
'
);
const
Group
=
require
(
'
../models/Group
'
);
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
const
Interview
=
require
(
'
../models/Interview
'
);
const
{
protect
,
authorize
}
=
require
(
'
../middleware/auth
'
);
const
upload
=
require
(
'
../middleware/upload
'
);
const
router
=
express
.
Router
();
// All admin routes require authentication + admin role
router
.
use
(
protect
,
authorize
(
'
admin
'
));
...
...
@@ -852,6 +853,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await
Group
.
findOneAndUpdate
({
name
:
oldName
},
{
name
:
newName
.
trim
()
});
await
User
.
updateMany
({
group
:
oldName
},
{
group
:
newName
.
trim
()
});
await
Quiz
.
updateMany
({
assignedGroups
:
oldName
},
{
$set
:
{
"
assignedGroups.$
"
:
newName
.
trim
()
}
});
await
Interview
.
updateMany
({
groupId
:
oldName
},
{
groupId
:
newName
.
trim
()
});
res
.
json
({
message
:
'
Group updated successfully
'
});
}
catch
(
error
)
{
...
...
@@ -869,6 +871,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await
Group
.
deleteOne
({
name
});
await
User
.
updateMany
({
group
:
name
},
{
group
:
'
General
'
});
await
Quiz
.
updateMany
({
assignedGroups
:
name
},
{
$pull
:
{
assignedGroups
:
name
}
});
await
Interview
.
updateMany
({
groupId
:
name
},
{
groupId
:
'
General
'
});
res
.
json
({
message
:
'
Group deleted successfully
'
});
}
catch
(
error
)
{
...
...
Backend/routes/candidate.js
View file @
68e5aeae
...
...
@@ -23,8 +23,29 @@ router.get('/interviews', async (req, res) => {
.
populate
(
'
createdBy
'
,
'
name
'
)
.
populate
(
'
quizzes.quizId
'
,
'
title timer totalQuestions category difficulty
'
)
.
sort
({
createdAt
:
-
1
});
res
.
json
({
interviews
});
const
now
=
new
Date
();
// Filter out interviews scheduled in the future
const
activeInterviews
=
interviews
.
filter
(
inv
=>
{
if
(
!
inv
.
dateOfInterview
)
return
true
;
// If no date, show it
const
invDate
=
new
Date
(
inv
.
dateOfInterview
);
// If time is provided, parse and set it on the date object
if
(
inv
.
timeOfInterview
)
{
const
[
hours
,
minutes
]
=
inv
.
timeOfInterview
.
split
(
'
:
'
).
map
(
Number
);
if
(
!
isNaN
(
hours
)
&&
!
isNaN
(
minutes
))
{
invDate
.
setHours
(
hours
,
minutes
,
0
,
0
);
}
}
else
{
// If no time is provided, we can assume it's available at the start of the day
invDate
.
setHours
(
0
,
0
,
0
,
0
);
}
return
now
>=
invDate
;
});
res
.
json
({
interviews
:
activeInterviews
});
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
...
...
@@ -104,12 +125,12 @@ router.get('/quiz/:quizId', async (req, res) => {
// Check assignment - is this candidate allowed to take this quiz?
const
user
=
await
User
.
findById
(
req
.
user
.
_id
);
// Check if part of interview
const
Interview
=
require
(
'
../models/Interview
'
);
const
interview
=
await
Interview
.
findOne
({
candidateId
:
req
.
user
.
_id
,
'
quizzes.quizId
'
:
quizId
const
interview
=
await
Interview
.
findOne
({
candidateId
:
req
.
user
.
_id
,
'
quizzes.quizId
'
:
quizId
});
const
isAssigned
=
quiz
.
assignToAll
||
...
...
@@ -198,19 +219,28 @@ router.post('/quiz/:quizId/submit', async (req, res) => {
// Update Interview if applicable
const
Interview
=
require
(
'
../models/Interview
'
);
await
Interview
.
findOneAndUpdate
(
const
updatedInterview
=
await
Interview
.
findOneAndUpdate
(
{
candidateId
:
req
.
user
.
_id
,
'
quizzes.quizId
'
:
quizId
},
{
$set
:
{
{
$set
:
{
'
quizzes.$.score
'
:
score
,
'
quizzes.$.totalMarks
'
:
totalMarks
,
'
quizzes.$.percentage
'
:
percentage
,
'
quizzes.$.completed
'
:
true
,
'
quizzes.$.violation
'
:
!!
violation
}
}
},
{
new
:
true
}
);
if
(
updatedInterview
)
{
const
allCompleted
=
updatedInterview
.
quizzes
.
every
(
q
=>
q
.
completed
);
if
(
allCompleted
&&
(
updatedInterview
.
status
===
'
quiz_phase
'
||
updatedInterview
.
status
===
'
pending
'
))
{
updatedInterview
.
status
=
'
coding_phase
'
;
await
updatedInterview
.
save
();
}
}
res
.
status
(
201
).
json
({
message
:
'
Quiz submitted successfully
'
,
result
:
{
...
...
@@ -327,7 +357,7 @@ router.post('/interview/:interviewId/coding', upload.single('codingZip'), async
if
(
interview
.
status
===
'
quiz_phase
'
||
interview
.
status
===
'
pending
'
)
{
interview
.
status
=
'
evaluation
'
;
}
else
{
interview
.
status
=
'
evaluation
'
;
// Since they submitted, it needs eval
interview
.
status
=
'
evaluation
'
;
// Since they submitted, it needs eval
}
await
interview
.
save
();
...
...
@@ -345,7 +375,7 @@ router.get('/profile', async (req, res) => {
try
{
const
user
=
await
User
.
findById
(
req
.
user
.
_id
).
select
(
'
-password
'
);
if
(
!
user
)
return
res
.
status
(
404
).
json
({
message
:
'
User not found
'
});
res
.
json
(
user
);
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
...
...
@@ -358,14 +388,14 @@ router.get('/profile', async (req, res) => {
router
.
put
(
'
/profile
'
,
upload
.
single
(
'
resume
'
),
async
(
req
,
res
)
=>
{
try
{
const
{
name
,
email
,
phoneNumber
,
topicsOfInterest
}
=
req
.
body
;
const
user
=
await
User
.
findById
(
req
.
user
.
_id
);
if
(
!
user
)
return
res
.
status
(
404
).
json
({
message
:
'
User not found
'
});
if
(
name
)
user
.
name
=
name
;
if
(
email
)
user
.
email
=
email
;
if
(
phoneNumber
!==
undefined
)
user
.
phoneNumber
=
phoneNumber
;
if
(
topicsOfInterest
)
{
try
{
// If sent as FormData, it will be a JSON string
...
...
@@ -377,7 +407,7 @@ router.put('/profile', upload.single('resume'), async (req, res) => {
return
res
.
status
(
400
).
json
({
message
:
'
Invalid format for topics of interest
'
});
}
}
// Handle resume upload
if
(
req
.
file
)
{
// If there's an old resume, delete it from the file system
...
...
@@ -389,9 +419,9 @@ router.put('/profile', upload.single('resume'), async (req, res) => {
}
user
.
resume
=
`/uploads/
${
req
.
file
.
filename
}
`
;
}
await
user
.
save
();
res
.
json
({
message
:
'
Profile updated successfully
'
,
user
:
{
...
...
@@ -416,7 +446,7 @@ router.delete('/profile/resume', async (req, res) => {
try
{
const
user
=
await
User
.
findById
(
req
.
user
.
_id
);
if
(
!
user
)
return
res
.
status
(
404
).
json
({
message
:
'
User not found
'
});
if
(
user
.
resume
)
{
const
resumePath
=
path
.
join
(
__dirname
,
'
..
'
,
'
uploads
'
,
path
.
basename
(
user
.
resume
));
if
(
fs
.
existsSync
(
resumePath
))
{
...
...
@@ -425,16 +455,18 @@ router.delete('/profile/resume', async (req, res) => {
user
.
resume
=
''
;
await
user
.
save
();
}
res
.
json
({
message
:
'
Resume deleted successfully
'
,
user
:
{
_id
:
user
.
_id
,
name
:
user
.
name
,
email
:
user
.
email
,
phoneNumber
:
user
.
phoneNumber
,
resume
:
user
.
resume
,
level
:
user
.
level
,
topicsOfInterest
:
user
.
topicsOfInterest
}
});
res
.
json
({
message
:
'
Resume deleted successfully
'
,
user
:
{
_id
:
user
.
_id
,
name
:
user
.
name
,
email
:
user
.
email
,
phoneNumber
:
user
.
phoneNumber
,
resume
:
user
.
resume
,
level
:
user
.
level
,
topicsOfInterest
:
user
.
topicsOfInterest
}
});
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
...
...
Backend/routes/hr.js
View file @
68e5aeae
const
express
=
require
(
'
express
'
);
const
express
=
require
(
'
express
'
);
const
xlsx
=
require
(
'
xlsx
'
);
const
fs
=
require
(
'
fs
'
);
const
User
=
require
(
'
../models/User
'
);
...
...
@@ -6,6 +6,7 @@
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
const
Interview
=
require
(
'
../models/Interview
'
);
const
{
protect
,
authorize
}
=
require
(
'
../middleware/auth
'
);
const
upload
=
require
(
'
../middleware/upload
'
);
const
router
=
express
.
Router
();
...
...
@@ -816,6 +817,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await
Group
.
findOneAndUpdate
({
name
:
oldName
},
{
name
:
newName
.
trim
()
});
await
User
.
updateMany
({
group
:
oldName
},
{
group
:
newName
.
trim
()
});
await
Quiz
.
updateMany
({
assignedGroups
:
oldName
},
{
$set
:
{
"
assignedGroups.$
"
:
newName
.
trim
()
}
});
await
Interview
.
updateMany
({
groupId
:
oldName
},
{
groupId
:
newName
.
trim
()
});
res
.
json
({
message
:
'
Group updated successfully
'
});
}
catch
(
error
)
{
...
...
@@ -833,6 +835,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await
Group
.
deleteOne
({
name
});
await
User
.
updateMany
({
group
:
name
},
{
group
:
'
General
'
});
await
Quiz
.
updateMany
({
assignedGroups
:
name
},
{
$pull
:
{
assignedGroups
:
name
}
});
await
Interview
.
updateMany
({
groupId
:
name
},
{
groupId
:
'
General
'
});
res
.
json
({
message
:
'
Group deleted successfully
'
});
}
catch
(
error
)
{
...
...
Backend/routes/interview.js
View file @
68e5aeae
...
...
@@ -19,7 +19,7 @@ router.use(protect);
// ============================================================
router
.
post
(
'
/
'
,
authorize
(
'
admin
'
,
'
hr
'
,
'
pm
'
),
async
(
req
,
res
)
=>
{
try
{
const
{
candidateId
,
interviewerId
,
assignedInterviewers
,
assignedHRs
,
assignedPMs
,
position
,
techStack
,
source
,
dateOfInterview
,
quizIds
}
=
req
.
body
;
const
{
candidateId
,
interviewerId
,
assignedInterviewers
,
assignedHRs
,
assignedPMs
,
position
,
techStack
,
source
,
dateOfInterview
,
timeOfInterview
,
quizIds
}
=
req
.
body
;
// Use interviewerId as fallback, or assignedInterviewers[0] as interviewerId for backward compatibility
const
mainInterviewerId
=
interviewerId
||
(
assignedInterviewers
&&
assignedInterviewers
.
length
>
0
?
assignedInterviewers
[
0
]
:
null
);
...
...
@@ -56,6 +56,7 @@ router.post('/', authorize('admin', 'hr', 'pm'), async (req, res) => {
techStack
:
techStack
||
''
,
source
:
source
||
''
,
dateOfInterview
:
dateOfInterview
||
new
Date
(),
timeOfInterview
:
timeOfInterview
||
''
,
quizzes
,
status
:
quizzes
.
length
>
0
?
'
quiz_phase
'
:
'
pending
'
,
type
:
'
individual
'
,
...
...
@@ -222,7 +223,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => {
try
{
const
{
groupName
,
assignedInterviewers
,
assignedHRs
,
assignedPMs
,
position
,
techStack
,
source
,
dateOfInterview
,
quizSets
position
,
techStack
,
source
,
dateOfInterview
,
timeOfInterview
,
quizSets
}
=
req
.
body
;
if
(
!
groupName
||
!
position
)
{
...
...
@@ -286,6 +287,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => {
techStack
:
techStack
||
''
,
source
:
source
||
''
,
dateOfInterview
:
dateOfInterview
||
new
Date
(),
timeOfInterview
:
timeOfInterview
||
''
,
quizzes
,
status
:
quizzes
.
length
>
0
?
'
quiz_phase
'
:
'
pending
'
,
type
:
'
group
'
,
...
...
Frontend/src/app/components/layout/layout.css
View file @
68e5aeae
...
...
@@ -438,6 +438,27 @@
margin
:
-8px
-12px
-12px
-8px
;
/* Compensate padding to maintain perfect visual alignment */
}
.scrollable-options
{
max-height
:
242px
;
}
.modal-options
::-webkit-scrollbar
{
width
:
6px
;
}
.modal-options
::-webkit-scrollbar-track
{
background
:
transparent
;
}
.modal-options
::-webkit-scrollbar-thumb
{
background
:
var
(
--border-color
);
border-radius
:
3px
;
}
.modal-options
::-webkit-scrollbar-thumb:hover
{
background
:
var
(
--text-muted
);
}
.glassy-option
{
display
:
flex
;
align-items
:
center
;
...
...
@@ -518,3 +539,83 @@
0
%
{
opacity
:
0
;
transform
:
scale
(
0.95
)
translateY
(
10px
);
}
100
%
{
opacity
:
1
;
transform
:
scale
(
1
)
translateY
(
0
);
}
}
/* Confirm Modal Specifics */
.confirm-modal
{
max-width
:
400px
;
text-align
:
center
;
align-items
:
center
;
}
.confirm-modal
.modal-header
{
width
:
100%
;
margin-bottom
:
16px
;
}
.confirm-modal-body
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
16px
;
margin-bottom
:
24px
;
}
.confirm-icon-wrapper
{
width
:
64px
;
height
:
64px
;
border-radius
:
50%
;
background
:
var
(
--danger-light
);
color
:
var
(
--danger
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
margin-bottom
:
8px
;
}
.confirm-icon-wrapper
.material-symbols-rounded
{
font-size
:
32px
;
}
.confirm-text
{
font-size
:
15px
;
color
:
var
(
--text-secondary
);
line-height
:
1.5
;
margin
:
0
;
}
.confirm-modal-actions
{
display
:
flex
;
gap
:
12px
;
width
:
100%
;
}
.confirm-modal-actions
.btn
{
flex
:
1
;
padding
:
12px
;
border-radius
:
12px
;
font-size
:
14px
;
font-weight
:
600
;
cursor
:
pointer
;
border
:
none
;
transition
:
all
0.2s
ease
;
}
.confirm-modal-actions
.btn-secondary
{
background
:
var
(
--bg-hover
);
color
:
var
(
--text-primary
);
border
:
1px
solid
var
(
--border-color
);
}
.confirm-modal-actions
.btn-secondary
:hover
{
background
:
var
(
--bg-tertiary
);
}
.confirm-modal-actions
.btn-danger
{
background
:
var
(
--danger
);
color
:
#fff
;
}
.confirm-modal-actions
.btn-danger
:hover
{
background
:
var
(
--danger-border
);
box-shadow
:
0
4px
12px
rgba
(
239
,
68
,
68
,
0.2
);
}
Frontend/src/app/components/layout/layout.html
View file @
68e5aeae
...
...
@@ -103,7 +103,7 @@
</button>
</div>
<div
class=
"modal-options"
>
<div
class=
"modal-options"
[class.scrollable-options]=
"authService.getUserRole() === 'admin'"
>
<a
[routerLink]=
"getUsersRoute()"
class=
"glassy-option"
(click)=
"closeManageUsersPopup()"
>
<div
class=
"option-icon-wrapper"
>
<span
class=
"material-symbols-rounded block-icon"
>
people
</span>
...
...
@@ -218,4 +218,30 @@
</div>
</div>
</div>
}
<!-- Logout Confirmation Modal -->
@if (showLogoutConfirm) {
<div
class=
"glassy-overlay"
(click)=
"showLogoutConfirm = false"
>
<div
class=
"glassy-modal confirm-modal animate-fade-in"
(click)=
"$event.stopPropagation()"
>
<div
class=
"modal-header"
>
<h2>
Confirm Logout
</h2>
<button
class=
"close-btn"
(click)=
"showLogoutConfirm = false"
>
<span
class=
"material-symbols-rounded"
>
close
</span>
</button>
</div>
<div
class=
"confirm-modal-body"
>
<div
class=
"confirm-icon-wrapper"
>
<span
class=
"material-symbols-rounded"
>
logout
</span>
</div>
<p
class=
"confirm-text"
>
Are you sure you want to sign out of QuizMaster?
</p>
</div>
<div
class=
"confirm-modal-actions"
>
<button
class=
"btn btn-secondary"
(click)=
"showLogoutConfirm = false"
>
Cancel
</button>
<button
class=
"btn btn-danger"
(click)=
"confirmLogout()"
>
Sign Out
</button>
</div>
</div>
</div>
}
\ No newline at end of file
Frontend/src/app/components/layout/layout.ts
View file @
68e5aeae
...
...
@@ -26,6 +26,7 @@ export class LayoutComponent {
showThemeMenu
=
false
;
mobileSidebarOpen
=
false
;
showLogoutConfirm
=
false
;
themes
:
{
id
:
ThemeMode
;
label
:
string
;
icon
:
string
}[]
=
[
{
id
:
'
light
'
,
label
:
'
Light
'
,
icon
:
'
light_mode
'
},
...
...
@@ -107,6 +108,11 @@ export class LayoutComponent {
}
logout
():
void
{
this
.
showLogoutConfirm
=
true
;
}
confirmLogout
():
void
{
this
.
showLogoutConfirm
=
false
;
this
.
authService
.
logout
();
}
...
...
Frontend/src/app/pages/admin/dashboard/dashboard.css
View file @
68e5aeae
...
...
@@ -110,24 +110,63 @@
/* Quick Actions */
.actions-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
4
,
1
fr
);
grid-template-columns
:
repeat
(
auto-fit
,
minmax
(
240px
,
1
fr
)
);
gap
:
16px
;
}
.action-card
{
display
:
flex
;
align-items
:
flex-start
;
align-items
:
center
;
gap
:
16px
;
text-decoration
:
none
;
color
:
inherit
;
height
:
100%
;
padding
:
18px
!important
;
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
transition
:
all
0.3s
cubic-bezier
(
0.4
,
0
,
0.2
,
1
);
position
:
relative
;
overflow
:
hidden
;
}
.action-card
::before
{
content
:
''
;
position
:
absolute
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
background
:
linear-gradient
(
135deg
,
rgba
(
102
,
126
,
234
,
0.05
)
0%
,
transparent
100%
);
opacity
:
0
;
transition
:
opacity
0.3s
;
}
.action-card
:hover::before
{
opacity
:
1
;
}
.action-card
:hover
{
border-color
:
var
(
--accent-primary
);
box-shadow
:
0
12px
30px
rgba
(
102
,
126
,
234
,
0.12
);
transform
:
translateY
(
-4px
);
}
.action-icon
{
font-size
:
28px
;
width
:
48px
;
height
:
48px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background
:
rgba
(
102
,
126
,
234
,
0.1
);
color
:
var
(
--accent-primary
);
border-radius
:
12px
;
font-size
:
24px
!important
;
flex-shrink
:
0
;
margin-top
:
2px
;
transition
:
all
0.3s
;
}
.action-card
:hover
.action-icon
{
background
:
var
(
--accent-primary
);
color
:
#fff
;
transform
:
scale
(
1.1
)
rotate
(
-5deg
);
}
.action-info
{
...
...
Frontend/src/app/pages/admin/group-interview/group-interview.html
View file @
68e5aeae
...
...
@@ -257,8 +257,11 @@
<input
class=
"form-input"
[(ngModel)]=
"newGroupInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Date of Interview
</label>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newGroupInterview.dateOfInterview"
>
<label
class=
"form-label"
>
Date
&
Time
</label>
<div
style=
"display: flex; gap: 8px;"
>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newGroupInterview.dateOfInterview"
style=
"flex: 1;"
>
<input
class=
"form-input"
type=
"time"
[(ngModel)]=
"newGroupInterview.timeOfInterview"
style=
"flex: 1;"
>
</div>
</div>
</div>
<div
class=
"form-row"
>
...
...
Frontend/src/app/pages/admin/group-interview/group-interview.ts
View file @
68e5aeae
...
...
@@ -145,7 +145,8 @@ export class GroupInterviewComponent implements OnInit {
position
:
''
,
techStack
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
]
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
};
}
...
...
Frontend/src/app/pages/admin/hr-users/hr-users.css
View file @
68e5aeae
...
...
@@ -23,8 +23,8 @@
.btn-primary
:hover:not
(
:disabled
)
{
transform
:
translateY
(
-2px
);
box-shadow
:
0
6px
20px
rgba
(
102
,
126
,
234
,
0.4
);
}
.btn-primary
:disabled
{
opacity
:
0.6
;
cursor
:
not-allowed
;
transform
:
none
;
box-shadow
:
none
;
}
.groups-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
auto-fill
,
minmax
(
320px
,
1
fr
));
gap
:
16px
;
}
.group-card
{
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
padding
:
20px
;
display
:
flex
;
align-items
:
stretch
;
gap
:
16px
;
transition
:
all
0.3s
;
}
.groups-grid
{
column-count
:
2
;
column-
gap
:
16px
;
}
.group-card
{
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
padding
:
20px
;
display
:
inline-flex
;
width
:
100%
;
align-items
:
center
;
gap
:
16px
;
transition
:
all
0.3s
;
break-inside
:
avoid
;
margin-bottom
:
16px
;
box-sizing
:
border-box
;
}
.group-card
:hover
{
border-color
:
var
(
--accent-primary
);
transform
:
translateY
(
-3px
);
box-shadow
:
var
(
--shadow-md
);
}
.group-icon-wrapper
{
width
:
50px
;
height
:
50px
;
border-radius
:
12px
;
background
:
linear-gradient
(
135deg
,
rgba
(
102
,
126
,
234
,
0.1
)
0%
,
rgba
(
102
,
126
,
234
,
0.2
)
100%
);
color
:
var
(
--accent-primary
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
}
.group-icon-wrapper
.material-symbols-rounded
{
font-size
:
26px
;
}
...
...
@@ -49,8 +49,10 @@
.empty-state
h3
{
color
:
var
(
--text-primary
);
margin
:
0
0
8px
;
font-size
:
18px
;
}
.empty-state
p
{
color
:
var
(
--text-muted
);
margin
:
0
;
font-size
:
14px
;
}
@media
(
max-width
:
768px
)
{
.groups-grid
{
column-count
:
1
;
}
}
@media
(
max-width
:
600px
)
{
.page-container
{
padding
:
20px
16px
;
}
.row-align
{
flex-direction
:
column
;
align-items
:
stretch
;
}
.groups-grid
{
grid-template-columns
:
1
fr
;
}
}
Frontend/src/app/pages/admin/hr-users/hr-users.html
View file @
68e5aeae
...
...
@@ -74,7 +74,7 @@
<p
style=
"margin: 4px 0 0; font-size: 13px; color: var(--text-muted);"
>
{{ user.email }}
</p>
}
</div>
<div
class=
"group-actions"
style=
"display: flex; gap: 8px;"
>
<div
class=
"group-actions"
style=
"display: flex; gap: 8px;
align-items: center;
"
>
@if (editingId() === user._id) {
<button
class=
"action-btn text-success"
(click)=
"saveEdit(user._id)"
title=
"Save"
><span
class=
"material-symbols-rounded"
>
check
</span></button>
<button
class=
"action-btn text-danger"
(click)=
"cancelEdit()"
title=
"Cancel"
><span
class=
"material-symbols-rounded"
>
close
</span></button>
...
...
Frontend/src/app/pages/admin/individual-interview/individual-interview.html
View file @
68e5aeae
...
...
@@ -184,8 +184,11 @@
<input
class=
"form-input"
[(ngModel)]=
"newInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Date of Interview
</label>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newInterview.dateOfInterview"
>
<label
class=
"form-label"
>
Date
&
Time
</label>
<div
style=
"display: flex; gap: 8px;"
>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newInterview.dateOfInterview"
style=
"flex: 1;"
>
<input
class=
"form-input"
type=
"time"
[(ngModel)]=
"newInterview.timeOfInterview"
style=
"flex: 1;"
>
</div>
</div>
</div>
...
...
Frontend/src/app/pages/admin/individual-interview/individual-interview.ts
View file @
68e5aeae
...
...
@@ -34,6 +34,7 @@ export class IndividualInterviewComponent implements OnInit {
techStack
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
,
quizIds
:
[]
as
string
[]
};
...
...
@@ -95,7 +96,7 @@ export class IndividualInterviewComponent implements OnInit {
});
this
.
newInterview
=
{
candidateId
:
''
,
assignedInterviewers
:
[],
assignedHRs
:
[],
assignedPMs
:
[],
position
:
''
,
techStack
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
quizIds
:
[]
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
,
quizIds
:
[]
};
this
.
showCreateModal
.
set
(
true
);
}
...
...
Frontend/src/app/pages/admin/manage-groups/manage-groups.css
View file @
68e5aeae
...
...
@@ -39,7 +39,7 @@
.group-lane
:hover
{
border-color
:
rgba
(
102
,
126
,
234
,
0.4
);
transform
:
translateY
(
-4px
);
box-shadow
:
0
12px
24px
rgba
(
0
,
0
,
0
,
0.1
);
}
.group-lane.cdk-drop-list-receiving
{
border-color
:
var
(
--accent-primary
);
background
:
rgba
(
102
,
126
,
234
,
0.05
);
box-shadow
:
0
0
0
2px
rgba
(
102
,
126
,
234
,
0.2
);
transform
:
scale
(
1.02
);
z-index
:
10
;
}
.group-lane-header
{
padding
:
18px
20px
;
background
:
rgba
(
102
,
126
,
234
,
0.05
);
border-bottom
:
1px
solid
transparent
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
transition
:
background
0.3s
;
pointer-events
:
none
;
}
.group-lane-header
{
padding
:
18px
20px
;
background
:
rgba
(
102
,
126
,
234
,
0.05
);
border-bottom
:
1px
solid
transparent
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
transition
:
background
0.3s
;
pointer-events
:
auto
;
}
.group-lane.expanded
.group-lane-header
{
background
:
rgba
(
102
,
126
,
234
,
0.1
);
border-bottom
:
1px
solid
var
(
--border-color
);
}
.group-actions
{
pointer-events
:
auto
;
}
.group-lane-body
{
cursor
:
default
;
overflow
:
hidden
;
}
...
...
Frontend/src/app/pages/admin/manage-groups/manage-groups.ts
View file @
68e5aeae
...
...
@@ -75,6 +75,9 @@ export class ManageGroupsComponent implements OnInit {
if
(
event
)
{
event
.
stopPropagation
();
}
if
(
this
.
editingGroup
())
{
return
;
// Do nothing if we are editing a group name
}
if
(
this
.
expandedGroup
()
===
groupName
)
{
this
.
expandedGroup
.
set
(
null
);
}
else
{
...
...
Frontend/src/app/pages/candidate/take-quiz/take-quiz.css
View file @
68e5aeae
...
...
@@ -88,7 +88,8 @@
display
:
flex
;
align-items
:
center
;
gap
:
6px
;
flex
:
0
1
350px
;
width
:
260px
;
flex-shrink
:
0
;
min-width
:
0
;
overflow
:
hidden
;
}
...
...
@@ -102,7 +103,14 @@
cursor
:
pointer
;
transition
:
all
0.2s
;
padding
:
0
;
}
.dots-scroll-btn
:hover
{
background
:
var
(
--bg-hover
);
border-color
:
var
(
--border-strong
);
color
:
var
(
--text-primary
);
}
.dots-scroll-btn
:hover:not
(
:disabled
)
{
background
:
var
(
--bg-hover
);
border-color
:
var
(
--border-strong
);
color
:
var
(
--text-primary
);
}
.dots-scroll-btn
:disabled
{
opacity
:
0.35
;
cursor
:
not-allowed
;
background
:
var
(
--bg-card
);
border-color
:
var
(
--border-color
);
color
:
var
(
--text-muted
);
}
.dots-scroll-btn
.material-symbols-rounded
{
font-size
:
20px
;
}
.question-dots
{
display
:
flex
;
gap
:
6px
;
...
...
Frontend/src/app/pages/candidate/take-quiz/take-quiz.html
View file @
68e5aeae
...
...
@@ -95,15 +95,15 @@
</button>
<div
class=
"dots-wrapper"
>
<button
class=
"dots-scroll-btn"
(click)=
"scrollDots(-1)"
aria-label=
"Scroll left"
>
<button
class=
"dots-scroll-btn"
(click)=
"scrollDots(-1)"
[disabled]=
"!canScrollLeft()"
aria-label=
"Scroll left"
>
<span
class=
"material-symbols-rounded"
>
chevron_left
</span>
</button>
<div
class=
"question-dots"
#dotsContainer
>
<div
class=
"question-dots"
#dotsContainer
(scroll)=
"updateScrollButtons()"
>
@for (q of questions(); track q._id; let i = $index) {
<button
class=
"dot"
[class.active]=
"i === currentIndex()"
[class.answered]=
"isAnswered(i)"
(click)=
"goTo(i)"
>
{{ i + 1 }}
</button>
}
</div>
<button
class=
"dots-scroll-btn"
(click)=
"scrollDots(1)"
aria-label=
"Scroll right"
>
<button
class=
"dots-scroll-btn"
(click)=
"scrollDots(1)"
[disabled]=
"!canScrollRight()"
aria-label=
"Scroll right"
>
<span
class=
"material-symbols-rounded"
>
chevron_right
</span>
</button>
</div>
...
...
Frontend/src/app/pages/candidate/take-quiz/take-quiz.ts
View file @
68e5aeae
...
...
@@ -39,6 +39,9 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
violationCount
=
signal
(
0
);
isFullscreen
=
signal
(
false
);
canScrollLeft
=
signal
(
false
);
canScrollRight
=
signal
(
false
);
private
antiCheatSubs
:
Subscription
[]
=
[];
// ─────────────────────────────────────────────────────
...
...
@@ -144,6 +147,8 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
this
.
startTimer
();
// Start anti-cheat after quiz loads
this
.
startAntiCheat
();
// Scroll active dot into view and initialize scroll buttons
this
.
scrollActiveDotIntoView
();
},
error
:
(
err
)
=>
{
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to load quiz
'
);
...
...
@@ -201,7 +206,10 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
// ─────────────────────────────────────────────────────
goTo
(
index
:
number
):
void
{
if
(
index
>=
0
&&
index
<
this
.
questions
().
length
)
this
.
currentIndex
.
set
(
index
);
if
(
index
>=
0
&&
index
<
this
.
questions
().
length
)
{
this
.
currentIndex
.
set
(
index
);
this
.
scrollActiveDotIntoView
();
}
}
prev
():
void
{
this
.
goTo
(
this
.
currentIndex
()
-
1
);
}
...
...
@@ -209,7 +217,44 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
scrollDots
(
direction
:
number
):
void
{
const
el
=
this
.
dotsContainer
?.
nativeElement
;
if
(
el
)
el
.
scrollBy
({
left
:
direction
*
120
,
behavior
:
'
smooth
'
});
if
(
el
)
{
el
.
scrollBy
({
left
:
direction
*
120
,
behavior
:
'
smooth
'
});
setTimeout
(()
=>
this
.
updateScrollButtons
(),
300
);
}
}
updateScrollButtons
():
void
{
const
el
=
this
.
dotsContainer
?.
nativeElement
;
if
(
el
)
{
// Allow a small tolerance for rounding errors in scroll position
this
.
canScrollLeft
.
set
(
el
.
scrollLeft
>
1
);
this
.
canScrollRight
.
set
(
el
.
scrollLeft
+
el
.
clientWidth
<
el
.
scrollWidth
-
1
);
}
}
scrollActiveDotIntoView
():
void
{
setTimeout
(()
=>
{
const
container
=
this
.
dotsContainer
?.
nativeElement
;
if
(
!
container
)
return
;
const
activeDot
=
container
.
querySelector
(
'
.dot.active
'
)
as
HTMLElement
;
if
(
!
activeDot
)
return
;
const
containerWidth
=
container
.
clientWidth
;
const
dotLeft
=
activeDot
.
offsetLeft
;
const
dotWidth
=
activeDot
.
clientWidth
;
// Center the active dot
const
targetScrollLeft
=
dotLeft
-
(
containerWidth
/
2
)
+
(
dotWidth
/
2
);
container
.
scrollTo
({
left
:
targetScrollLeft
,
behavior
:
'
smooth
'
});
// Update buttons after smooth scroll animation is likely finished
setTimeout
(()
=>
this
.
updateScrollButtons
(),
300
);
},
50
);
}
// ─────────────────────────────────────────────────────
...
...
Frontend/src/app/pages/hr/dashboard/dashboard.css
View file @
68e5aeae
...
...
@@ -110,24 +110,63 @@
/* Quick Actions */
.actions-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
4
,
1
fr
);
grid-template-columns
:
repeat
(
auto-fit
,
minmax
(
240px
,
1
fr
)
);
gap
:
16px
;
}
.action-card
{
display
:
flex
;
align-items
:
flex-start
;
align-items
:
center
;
gap
:
16px
;
text-decoration
:
none
;
color
:
inherit
;
height
:
100%
;
padding
:
18px
!important
;
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
transition
:
all
0.3s
cubic-bezier
(
0.4
,
0
,
0.2
,
1
);
position
:
relative
;
overflow
:
hidden
;
}
.action-card
::before
{
content
:
''
;
position
:
absolute
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
background
:
linear-gradient
(
135deg
,
rgba
(
102
,
126
,
234
,
0.05
)
0%
,
transparent
100%
);
opacity
:
0
;
transition
:
opacity
0.3s
;
}
.action-card
:hover::before
{
opacity
:
1
;
}
.action-card
:hover
{
border-color
:
var
(
--accent-primary
);
box-shadow
:
0
12px
30px
rgba
(
102
,
126
,
234
,
0.12
);
transform
:
translateY
(
-4px
);
}
.action-icon
{
font-size
:
28px
;
width
:
48px
;
height
:
48px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background
:
rgba
(
102
,
126
,
234
,
0.1
);
color
:
var
(
--accent-primary
);
border-radius
:
12px
;
font-size
:
24px
!important
;
flex-shrink
:
0
;
margin-top
:
2px
;
transition
:
all
0.3s
;
}
.action-card
:hover
.action-icon
{
background
:
var
(
--accent-primary
);
color
:
#fff
;
transform
:
scale
(
1.1
)
rotate
(
-5deg
);
}
.action-info
{
...
...
@@ -147,7 +186,7 @@
margin
:
0
;
display
:
-webkit-box
;
-webkit-line-clamp
:
2
;
-webkit-box-orient
:
vertical
;
-webkit-box-orient
:
vertical
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
min-height
:
38px
;
...
...
Frontend/src/app/pages/hr/group-interview/group-interview.html
View file @
68e5aeae
...
...
@@ -258,8 +258,11 @@
<input
class=
"form-input"
[(ngModel)]=
"newGroupInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Date of Interview
</label>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newGroupInterview.dateOfInterview"
>
<label
class=
"form-label"
>
Date
&
Time
</label>
<div
style=
"display: flex; gap: 8px;"
>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newGroupInterview.dateOfInterview"
style=
"flex: 1;"
>
<input
class=
"form-input"
type=
"time"
[(ngModel)]=
"newGroupInterview.timeOfInterview"
style=
"flex: 1;"
>
</div>
</div>
</div>
<div
class=
"form-row"
>
...
...
Frontend/src/app/pages/hr/group-interview/group-interview.ts
View file @
68e5aeae
...
...
@@ -145,7 +145,8 @@ export class HRGroupInterviewComponent implements OnInit {
position
:
''
,
techStack
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
]
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
};
}
...
...
Frontend/src/app/pages/hr/individual-interview/individual-interview.html
View file @
68e5aeae
...
...
@@ -184,8 +184,11 @@
<input
class=
"form-input"
[(ngModel)]=
"newInterview.source"
placeholder=
"e.g., LinkedIn, Campus"
>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Date of Interview
</label>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newInterview.dateOfInterview"
>
<label
class=
"form-label"
>
Date
&
Time
</label>
<div
style=
"display: flex; gap: 8px;"
>
<input
class=
"form-input"
type=
"date"
[(ngModel)]=
"newInterview.dateOfInterview"
style=
"flex: 1;"
>
<input
class=
"form-input"
type=
"time"
[(ngModel)]=
"newInterview.timeOfInterview"
style=
"flex: 1;"
>
</div>
</div>
</div>
...
...
Frontend/src/app/pages/hr/individual-interview/individual-interview.ts
View file @
68e5aeae
...
...
@@ -34,6 +34,7 @@ export class HRIndividualInterviewComponent implements OnInit {
techStack
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
,
quizIds
:
[]
as
string
[]
};
...
...
@@ -95,7 +96,7 @@ export class HRIndividualInterviewComponent implements OnInit {
});
this
.
newInterview
=
{
candidateId
:
''
,
assignedInterviewers
:
[],
assignedHRs
:
[],
assignedPMs
:
[],
position
:
''
,
techStack
:
''
,
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
quizIds
:
[]
source
:
''
,
dateOfInterview
:
new
Date
().
toISOString
().
split
(
'
T
'
)[
0
],
timeOfInterview
:
''
,
quizIds
:
[]
};
this
.
showCreateModal
.
set
(
true
);
}
...
...
Frontend/src/app/pages/hr/manage-groups/manage-groups.css
View file @
68e5aeae
...
...
@@ -39,7 +39,7 @@
.group-lane
:hover
{
border-color
:
rgba
(
102
,
126
,
234
,
0.4
);
transform
:
translateY
(
-4px
);
box-shadow
:
0
12px
24px
rgba
(
0
,
0
,
0
,
0.1
);
}
.group-lane.cdk-drop-list-receiving
{
border-color
:
var
(
--accent-primary
);
background
:
rgba
(
102
,
126
,
234
,
0.05
);
box-shadow
:
0
0
0
2px
rgba
(
102
,
126
,
234
,
0.2
);
transform
:
scale
(
1.02
);
z-index
:
10
;
}
.group-lane-header
{
padding
:
18px
20px
;
background
:
rgba
(
102
,
126
,
234
,
0.05
);
border-bottom
:
1px
solid
transparent
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
transition
:
background
0.3s
;
pointer-events
:
none
;
}
.group-lane-header
{
padding
:
18px
20px
;
background
:
rgba
(
102
,
126
,
234
,
0.05
);
border-bottom
:
1px
solid
transparent
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
transition
:
background
0.3s
;
pointer-events
:
auto
;
}
.group-lane.expanded
.group-lane-header
{
background
:
rgba
(
102
,
126
,
234
,
0.1
);
border-bottom
:
1px
solid
var
(
--border-color
);
}
.group-actions
{
pointer-events
:
auto
;
}
.group-lane-body
{
cursor
:
default
;
overflow
:
hidden
;
}
...
...
Frontend/src/app/pages/hr/manage-groups/manage-groups.ts
View file @
68e5aeae
...
...
@@ -70,6 +70,9 @@ export class HRManageGroupsComponent {
if
(
event
)
{
event
.
stopPropagation
();
}
if
(
this
.
editingGroup
())
{
return
;
// Do nothing if we are editing a group name
}
if
(
this
.
expandedGroup
()
===
groupName
)
{
this
.
expandedGroup
.
set
(
null
);
}
else
{
...
...
Frontend/src/styles.css
View file @
68e5aeae
...
...
@@ -381,6 +381,7 @@ ul, ol {
.btn-primary
:hover:not
(
:disabled
)
{
transform
:
translateY
(
-1px
);
box-shadow
:
0
4px
16px
rgba
(
var
(
--accent-primary-rgb
),
0.35
);
color
:
#fff
;
}
.btn-outline
{
...
...
@@ -403,6 +404,7 @@ ul, ol {
.btn-danger
:hover:not
(
:disabled
)
{
background
:
#dc2626
;
transform
:
translateY
(
-1px
);
color
:
#fff
;
}
.btn-ghost
{
...
...
ITL_Intern_Evaluation_Form.html
deleted
100644 → 0
View file @
99f0042f
This diff is collapsed.
Click to expand it.
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