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
fd448421
Commit
fd448421
authored
Apr 15, 2026
by
AravindR-K
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat : Now you can Add, Delete and Edit Groups inorder to assign people the quiz
parent
d2648061
Changes
25
Hide whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
1066 additions
and
55 deletions
+1066
-55
Backend/models/Group.js
Backend/models/Group.js
+14
-0
Backend/routes/admin.js
Backend/routes/admin.js
+54
-9
Backend/routes/auth.js
Backend/routes/auth.js
+22
-2
Backend/routes/hr.js
Backend/routes/hr.js
+60
-2
Frontend/src/app/app.routes.ts
Frontend/src/app/app.routes.ts
+8
-0
Frontend/src/app/components/layout/layout.css
Frontend/src/app/components/layout/layout.css
+137
-0
Frontend/src/app/components/layout/layout.html
Frontend/src/app/components/layout/layout.html
+54
-8
Frontend/src/app/components/layout/layout.ts
Frontend/src/app/components/layout/layout.ts
+23
-0
Frontend/src/app/pages/admin/create-quiz/create-quiz.css
Frontend/src/app/pages/admin/create-quiz/create-quiz.css
+69
-0
Frontend/src/app/pages/admin/create-quiz/create-quiz.html
Frontend/src/app/pages/admin/create-quiz/create-quiz.html
+53
-0
Frontend/src/app/pages/admin/create-quiz/create-quiz.ts
Frontend/src/app/pages/admin/create-quiz/create-quiz.ts
+74
-2
Frontend/src/app/pages/admin/dashboard/dashboard.html
Frontend/src/app/pages/admin/dashboard/dashboard.html
+1
-1
Frontend/src/app/pages/admin/dashboard/dashboard.ts
Frontend/src/app/pages/admin/dashboard/dashboard.ts
+6
-1
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.css
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.css
+75
-0
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.html
...tend/src/app/pages/admin/generate-quiz/generate-quiz.html
+46
-16
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.ts
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.ts
+51
-7
Frontend/src/app/pages/admin/manage-groups/manage-groups.css
Frontend/src/app/pages/admin/manage-groups/manage-groups.css
+50
-0
Frontend/src/app/pages/admin/manage-groups/manage-groups.html
...tend/src/app/pages/admin/manage-groups/manage-groups.html
+72
-0
Frontend/src/app/pages/admin/manage-groups/manage-groups.ts
Frontend/src/app/pages/admin/manage-groups/manage-groups.ts
+114
-0
Frontend/src/app/pages/register/register.css
Frontend/src/app/pages/register/register.css
+9
-2
Frontend/src/app/pages/register/register.html
Frontend/src/app/pages/register/register.html
+19
-0
Frontend/src/app/pages/register/register.ts
Frontend/src/app/pages/register/register.ts
+12
-3
Frontend/src/app/services/auth.service.ts
Frontend/src/app/services/auth.service.ts
+6
-2
Frontend/src/app/services/quiz.service.ts
Frontend/src/app/services/quiz.service.ts
+29
-0
Frontend/src/app/services/ui.service.ts
Frontend/src/app/services/ui.service.ts
+8
-0
No files found.
Backend/models/Group.js
0 → 100644
View file @
fd448421
const
mongoose
=
require
(
'
mongoose
'
);
const
groupSchema
=
new
mongoose
.
Schema
({
name
:
{
type
:
String
,
required
:
[
true
,
'
Group name is required
'
],
unique
:
true
,
trim
:
true
}
},
{
timestamps
:
true
});
module
.
exports
=
mongoose
.
model
(
'
Group
'
,
groupSchema
);
Backend/routes/admin.js
View file @
fd448421
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
const
xlsx
=
require
(
'
xlsx
'
);
const
xlsx
=
require
(
'
xlsx
'
);
const
fs
=
require
(
'
fs
'
);
const
fs
=
require
(
'
fs
'
);
const
User
=
require
(
'
../models/User
'
);
const
User
=
require
(
'
../models/User
'
);
const
Group
=
require
(
'
../models/Group
'
);
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
...
@@ -190,8 +191,7 @@
...
@@ -190,8 +191,7 @@
if
(
!
req
.
file
)
{
if
(
!
req
.
file
)
{
return
res
.
status
(
400
).
json
({
message
:
'
Please upload an Excel file with questions
'
});
return
res
.
status
(
400
).
json
({
message
:
'
Please upload an Excel file with questions
'
});
}
}
console
.
log
(
"
FILE:
"
,
req
.
file
);
console
.
log
(
"
BODY:
"
,
req
.
body
);
// Parse Excel file
// Parse Excel file
const
workbook
=
xlsx
.
readFile
(
req
.
file
.
path
);
const
workbook
=
xlsx
.
readFile
(
req
.
file
.
path
);
const
sheetName
=
workbook
.
SheetNames
[
0
];
const
sheetName
=
workbook
.
SheetNames
[
0
];
...
@@ -473,8 +473,13 @@
...
@@ -473,8 +473,13 @@
// @access Admin
// @access Admin
router
.
get
(
'
/groups
'
,
async
(
req
,
res
)
=>
{
router
.
get
(
'
/groups
'
,
async
(
req
,
res
)
=>
{
try
{
try
{
const
groups
=
await
User
.
distinct
(
'
group
'
);
const
groups
=
await
Group
.
find
().
sort
({
name
:
1
});
res
.
json
({
groups
});
const
groupNames
=
groups
.
map
(
g
=>
g
.
name
);
// Fetch legacy groups from users just to be safe
const
userGroups
=
await
User
.
distinct
(
'
group
'
);
const
allGroups
=
[...
new
Set
([...
groupNames
,
...
userGroups
])].
filter
(
g
=>
g
&&
g
!==
'
General
'
);
res
.
json
({
groups
:
allGroups
});
}
catch
(
error
)
{
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
}
...
@@ -494,16 +499,56 @@
...
@@ -494,16 +499,56 @@
}
}
// Check if already exists
// Check if already exists
const
existing
=
await
User
.
findOne
({
group
:
name
.
trim
()
});
const
existing
=
await
Group
.
findOne
({
name
:
name
.
trim
()
});
if
(
existing
)
{
if
(
existing
)
{
return
res
.
status
(
400
).
json
({
message
:
'
Group already exists
'
});
return
res
.
status
(
400
).
json
({
message
:
'
Group already exists
'
});
}
}
// ⚠️ Since you're using group as string,
const
group
=
await
Group
.
create
({
name
:
name
.
trim
()
});
// we don't store separately — just return success
res
.
status
(
201
).
json
({
message
:
'
Group created successfully
'
,
group
:
group
.
name
});
res
.
status
(
201
).
json
({
message
:
'
Group created successfully
'
,
group
:
name
.
trim
()
});
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
});
// @route PUT /api/admin/groups/:oldName
// @desc Edit a group name
// @access Admin
router
.
put
(
'
/groups/:oldName
'
,
async
(
req
,
res
)
=>
{
try
{
const
{
oldName
}
=
req
.
params
;
const
{
newName
}
=
req
.
body
;
if
(
!
newName
||
!
newName
.
trim
())
return
res
.
status
(
400
).
json
({
message
:
'
New group name is required
'
});
const
existing
=
await
Group
.
findOne
({
name
:
newName
.
trim
()
});
if
(
existing
&&
existing
.
name
!==
oldName
)
{
return
res
.
status
(
400
).
json
({
message
:
'
A group with this name already exists
'
});
}
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
()
}
});
res
.
json
({
message
:
'
Group updated successfully
'
});
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
});
// @route DELETE /api/admin/groups/:name
// @desc Delete a group
// @access Admin
router
.
delete
(
'
/groups/:name
'
,
async
(
req
,
res
)
=>
{
try
{
const
{
name
}
=
req
.
params
;
await
Group
.
deleteOne
({
name
});
await
User
.
updateMany
({
group
:
name
},
{
group
:
'
General
'
});
await
Quiz
.
updateMany
({
assignedGroups
:
name
},
{
$pull
:
{
assignedGroups
:
name
}
});
res
.
json
({
message
:
'
Group deleted successfully
'
});
}
catch
(
error
)
{
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
}
...
...
Backend/routes/auth.js
View file @
fd448421
const
express
=
require
(
'
express
'
);
const
express
=
require
(
'
express
'
);
const
jwt
=
require
(
'
jsonwebtoken
'
);
const
jwt
=
require
(
'
jsonwebtoken
'
);
const
User
=
require
(
'
../models/User
'
);
const
User
=
require
(
'
../models/User
'
);
const
Group
=
require
(
'
../models/Group
'
);
const
{
protect
}
=
require
(
'
../middleware/auth
'
);
const
{
protect
}
=
require
(
'
../middleware/auth
'
);
const
router
=
express
.
Router
();
const
router
=
express
.
Router
();
...
@@ -16,7 +17,7 @@ const generateToken = (id) => {
...
@@ -16,7 +17,7 @@ const generateToken = (id) => {
// @access Public
// @access Public
router
.
post
(
'
/register
'
,
async
(
req
,
res
)
=>
{
router
.
post
(
'
/register
'
,
async
(
req
,
res
)
=>
{
try
{
try
{
const
{
name
,
email
,
password
}
=
req
.
body
;
const
{
name
,
email
,
password
,
group
}
=
req
.
body
;
// Validate input
// Validate input
if
(
!
name
||
!
email
||
!
password
)
{
if
(
!
name
||
!
email
||
!
password
)
{
...
@@ -30,7 +31,10 @@ router.post('/register', async (req, res) => {
...
@@ -30,7 +31,10 @@ router.post('/register', async (req, res) => {
}
}
// Create user (role defaults to 'candidate')
// Create user (role defaults to 'candidate')
const
user
=
await
User
.
create
({
name
,
email
,
password
,
role
:
'
candidate
'
});
const
userDbData
=
{
name
,
email
,
password
,
role
:
'
candidate
'
};
if
(
group
)
userDbData
.
group
=
group
;
const
user
=
await
User
.
create
(
userDbData
);
res
.
status
(
201
).
json
({
res
.
status
(
201
).
json
({
message
:
'
Registration successful
'
,
message
:
'
Registration successful
'
,
...
@@ -115,4 +119,20 @@ router.get('/me', protect, async (req, res) => {
...
@@ -115,4 +119,20 @@ router.get('/me', protect, async (req, res) => {
}
}
});
});
// @route GET /api/auth/groups
// @desc Get all groups for registration
// @access Public
router
.
get
(
'
/groups
'
,
async
(
req
,
res
)
=>
{
try
{
const
groups
=
await
Group
.
find
().
sort
({
name
:
1
});
const
groupNames
=
groups
.
map
(
g
=>
g
.
name
);
// Backward compatibility: fetch from User model too
const
userGroups
=
await
User
.
distinct
(
'
group
'
);
const
allGroups
=
[...
new
Set
([...
groupNames
,
...
userGroups
])].
filter
(
g
=>
g
&&
g
!==
'
General
'
);
res
.
json
({
groups
:
allGroups
});
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
});
module
.
exports
=
router
;
module
.
exports
=
router
;
Backend/routes/hr.js
View file @
fd448421
...
@@ -2,6 +2,7 @@ const express = require('express');
...
@@ -2,6 +2,7 @@ const express = require('express');
const
xlsx
=
require
(
'
xlsx
'
);
const
xlsx
=
require
(
'
xlsx
'
);
const
fs
=
require
(
'
fs
'
);
const
fs
=
require
(
'
fs
'
);
const
User
=
require
(
'
../models/User
'
);
const
User
=
require
(
'
../models/User
'
);
const
Group
=
require
(
'
../models/Group
'
);
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Quiz
=
require
(
'
../models/Quiz
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Question
=
require
(
'
../models/Question
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
const
Submission
=
require
(
'
../models/Submission
'
);
...
@@ -292,8 +293,65 @@ router.get('/categories', async (req, res) => {
...
@@ -292,8 +293,65 @@ router.get('/categories', async (req, res) => {
// @route GET /api/hr/groups
// @route GET /api/hr/groups
router
.
get
(
'
/groups
'
,
async
(
req
,
res
)
=>
{
router
.
get
(
'
/groups
'
,
async
(
req
,
res
)
=>
{
try
{
try
{
const
groups
=
await
User
.
distinct
(
'
group
'
);
const
groups
=
await
Group
.
find
().
sort
({
name
:
1
});
res
.
json
({
groups
});
const
groupNames
=
groups
.
map
(
g
=>
g
.
name
);
const
userGroups
=
await
User
.
distinct
(
'
group
'
);
const
allGroups
=
[...
new
Set
([...
groupNames
,
...
userGroups
])].
filter
(
g
=>
g
&&
g
!==
'
General
'
);
res
.
json
({
groups
:
allGroups
});
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
});
// @route POST /api/hr/groups
router
.
post
(
'
/groups
'
,
async
(
req
,
res
)
=>
{
try
{
const
{
name
}
=
req
.
body
;
if
(
!
name
||
!
name
.
trim
())
return
res
.
status
(
400
).
json
({
message
:
'
Group name is required
'
});
const
existing
=
await
Group
.
findOne
({
name
:
name
.
trim
()
});
if
(
existing
)
return
res
.
status
(
400
).
json
({
message
:
'
Group already exists
'
});
const
group
=
await
Group
.
create
({
name
:
name
.
trim
()
});
res
.
status
(
201
).
json
({
message
:
'
Group created successfully
'
,
group
:
group
.
name
});
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
});
// @route PUT /api/hr/groups/:oldName
router
.
put
(
'
/groups/:oldName
'
,
async
(
req
,
res
)
=>
{
try
{
const
{
oldName
}
=
req
.
params
;
const
{
newName
}
=
req
.
body
;
if
(
!
newName
||
!
newName
.
trim
())
return
res
.
status
(
400
).
json
({
message
:
'
New group name is required
'
});
const
existing
=
await
Group
.
findOne
({
name
:
newName
.
trim
()
});
if
(
existing
&&
existing
.
name
!==
oldName
)
{
return
res
.
status
(
400
).
json
({
message
:
'
A group with this name already exists
'
});
}
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
()
}
});
res
.
json
({
message
:
'
Group updated successfully
'
});
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
});
// @route DELETE /api/hr/groups/:name
router
.
delete
(
'
/groups/:name
'
,
async
(
req
,
res
)
=>
{
try
{
const
{
name
}
=
req
.
params
;
await
Group
.
deleteOne
({
name
});
await
User
.
updateMany
({
group
:
name
},
{
group
:
'
General
'
});
await
Quiz
.
updateMany
({
assignedGroups
:
name
},
{
$pull
:
{
assignedGroups
:
name
}
});
res
.
json
({
message
:
'
Group deleted successfully
'
});
}
catch
(
error
)
{
}
catch
(
error
)
{
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
res
.
status
(
500
).
json
({
message
:
'
Server error
'
,
error
:
error
.
message
});
}
}
...
...
Frontend/src/app/app.routes.ts
View file @
fd448421
...
@@ -37,6 +37,10 @@ export const routes: Routes = [
...
@@ -37,6 +37,10 @@ export const routes: Routes = [
path
:
'
users/:userId/history
'
,
path
:
'
users/:userId/history
'
,
loadComponent
:
()
=>
import
(
'
./pages/admin/user-history/user-history
'
).
then
(
m
=>
m
.
UserHistoryComponent
)
loadComponent
:
()
=>
import
(
'
./pages/admin/user-history/user-history
'
).
then
(
m
=>
m
.
UserHistoryComponent
)
},
},
{
path
:
'
manage-groups
'
,
loadComponent
:
()
=>
import
(
'
./pages/admin/manage-groups/manage-groups
'
).
then
(
m
=>
m
.
ManageGroupsComponent
)
},
{
{
path
:
'
submissions/:submissionId
'
,
path
:
'
submissions/:submissionId
'
,
loadComponent
:
()
=>
import
(
'
./pages/admin/submission-detail/submission-detail
'
).
then
(
m
=>
m
.
SubmissionDetailComponent
)
loadComponent
:
()
=>
import
(
'
./pages/admin/submission-detail/submission-detail
'
).
then
(
m
=>
m
.
SubmissionDetailComponent
)
...
@@ -79,6 +83,10 @@ export const routes: Routes = [
...
@@ -79,6 +83,10 @@ export const routes: Routes = [
path
:
'
candidates
'
,
path
:
'
candidates
'
,
loadComponent
:
()
=>
import
(
'
./pages/hr/candidates/candidates
'
).
then
(
m
=>
m
.
HRCandidatesComponent
)
loadComponent
:
()
=>
import
(
'
./pages/hr/candidates/candidates
'
).
then
(
m
=>
m
.
HRCandidatesComponent
)
},
},
{
path
:
'
manage-groups
'
,
loadComponent
:
()
=>
import
(
'
./pages/admin/manage-groups/manage-groups
'
).
then
(
m
=>
m
.
ManageGroupsComponent
)
},
{
{
path
:
'
candidates/:userId/history
'
,
path
:
'
candidates/:userId/history
'
,
loadComponent
:
()
=>
import
(
'
./pages/hr/candidate-history/candidate-history
'
).
then
(
m
=>
m
.
HRCandidateHistoryComponent
)
loadComponent
:
()
=>
import
(
'
./pages/hr/candidate-history/candidate-history
'
).
then
(
m
=>
m
.
HRCandidateHistoryComponent
)
...
...
Frontend/src/app/components/layout/layout.css
View file @
fd448421
...
@@ -352,3 +352,140 @@
...
@@ -352,3 +352,140 @@
margin-left
:
0
;
margin-left
:
0
;
}
}
}
}
/* Glassy Modal Styles */
.glassy-overlay
{
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100%
;
height
:
100%
;
background
:
rgba
(
0
,
0
,
0
,
0.4
);
backdrop-filter
:
blur
(
4px
);
z-index
:
1000
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
animation
:
fadeIn
0.2s
ease-out
forwards
;
}
.glassy-modal
{
background
:
var
(
--bg-card
);
/* Theme aware */
border
:
1px
solid
var
(
--border-color
);
border-radius
:
20px
;
width
:
90%
;
max-width
:
550px
;
padding
:
32px
;
box-shadow
:
0
15px
35px
rgba
(
0
,
0
,
0
,
0.2
),
inset
0
1px
0
rgba
(
255
,
255
,
255
,
0.1
);
backdrop-filter
:
blur
(
20px
);
position
:
relative
;
overflow
:
hidden
;
}
.modal-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
24px
;
}
.modal-header
h2
{
font-size
:
22px
;
font-weight
:
700
;
color
:
var
(
--text-primary
);
margin
:
0
;
}
.close-btn
{
background
:
transparent
;
border
:
none
;
color
:
var
(
--text-muted
);
cursor
:
pointer
;
padding
:
4px
;
border-radius
:
50%
;
transition
:
all
0.2s
;
display
:
flex
;
}
.close-btn
:hover
{
background
:
var
(
--bg-hover
);
color
:
var
(
--text-primary
);
}
.modal-options
{
display
:
flex
;
flex-direction
:
column
;
gap
:
16px
;
}
.glassy-option
{
display
:
flex
;
align-items
:
center
;
gap
:
20px
;
padding
:
20px
;
background
:
var
(
--bg-input
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
text-decoration
:
none
;
transition
:
all
0.3s
cubic-bezier
(
0.4
,
0
,
0.2
,
1
);
position
:
relative
;
}
.glassy-option
:hover
{
transform
:
translateY
(
-4px
);
background
:
var
(
--bg-hover
);
border-color
:
var
(
--accent-primary
);
box-shadow
:
0
10px
20px
rgba
(
0
,
0
,
0
,
0.1
);
}
.option-icon-wrapper
{
width
:
56px
;
height
:
56px
;
border-radius
:
14px
;
background
:
linear-gradient
(
135deg
,
var
(
--accent-primary
)
0%
,
#764ba2
100%
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
}
.block-icon
{
font-size
:
28px
;
color
:
white
;
}
.option-text
{
flex
:
1
;
}
.option-text
h3
{
margin
:
0
0
4px
0
;
font-size
:
18px
;
font-weight
:
600
;
color
:
var
(
--text-primary
);
}
.option-text
p
{
margin
:
0
;
font-size
:
13px
;
color
:
var
(
--text-muted
);
line-height
:
1.4
;
}
.arrow-icon
{
color
:
var
(
--text-muted
);
transition
:
transform
0.2s
;
}
.glassy-option
:hover
.arrow-icon
{
color
:
var
(
--accent-primary
);
transform
:
translateX
(
4px
);
}
.animate-fade-in
{
animation
:
modalScaleUp
0.3s
cubic-bezier
(
0.175
,
0.885
,
0.32
,
1.275
)
forwards
;
}
@keyframes
modalScaleUp
{
0
%
{
opacity
:
0
;
transform
:
scale
(
0.95
)
translateY
(
10px
);
}
100
%
{
opacity
:
1
;
transform
:
scale
(
1
)
translateY
(
0
);
}
}
Frontend/src/app/components/layout/layout.html
View file @
fd448421
...
@@ -37,14 +37,22 @@
...
@@ -37,14 +37,22 @@
<!-- Navigation -->
<!-- Navigation -->
<nav
class=
"sidebar-nav"
>
<nav
class=
"sidebar-nav"
>
@for (item of navItems(); track item.route) {
@for (item of navItems(); track item.route) {
<a
[routerLink]=
"item.route"
@if (item.route === '/admin/users' || item.route === '/hr/candidates') {
routerLinkActive=
"active"
<a
class=
"nav-item"
(click)=
"handleNavClick(item, $event)"
href=
"javascript:void(0)"
[routerLinkActiveOptions]=
"{exact: item.route.includes('dashboard')}"
[class.active]=
"router.url.includes('/admin/users') || router.url.includes('/hr/candidates') || router.url.includes('manage-groups')"
>
class=
"nav-item"
<span
class=
"material-symbols-rounded nav-icon"
>
{{ item.icon }}
</span>
(click)=
"mobileSidebarOpen = false"
>
<span
class=
"nav-label"
>
{{ item.label }}
</span>
<span
class=
"material-symbols-rounded nav-icon"
>
{{ item.icon }}
</span>
</a>
<span
class=
"nav-label"
>
{{ item.label }}
</span>
} @else {
</a>
<a
[routerLink]=
"item.route"
routerLinkActive=
"active"
[routerLinkActiveOptions]=
"{exact: item.route.includes('dashboard')}"
class=
"nav-item"
(click)=
"mobileSidebarOpen = false"
>
<span
class=
"material-symbols-rounded nav-icon"
>
{{ item.icon }}
</span>
<span
class=
"nav-label"
>
{{ item.label }}
</span>
</a>
}
}
}
</nav>
</nav>
...
@@ -86,3 +94,41 @@
...
@@ -86,3 +94,41 @@
<router-outlet
/>
<router-outlet
/>
</main>
</main>
</div>
</div>
<!-- Manage Users Glassy Popup -->
@if (uiService.showManageUsersPopup()) {
<div
class=
"glassy-overlay"
(click)=
"closeManageUsersPopup()"
>
<div
class=
"glassy-modal animate-fade-in"
(click)=
"$event.stopPropagation()"
>
<div
class=
"modal-header"
>
<h2>
Manage Users
&
Groups
</h2>
<button
class=
"close-btn"
(click)=
"closeManageUsersPopup()"
>
<span
class=
"material-symbols-rounded"
>
close
</span>
</button>
</div>
<div
class=
"modal-options"
>
<a
[routerLink]=
"getUsersRoute()"
class=
"glassy-option"
(click)=
"closeManageUsersPopup()"
>
<div
class=
"option-icon-wrapper"
>
<span
class=
"material-symbols-rounded block-icon"
>
people
</span>
</div>
<div
class=
"option-text"
>
<h3>
Student Users
</h3>
<p>
Review and manage candidate profiles and test histories
</p>
</div>
<span
class=
"material-symbols-rounded arrow-icon"
>
arrow_forward
</span>
</a>
<a
[routerLink]=
"getManageGroupsRoute()"
class=
"glassy-option"
(click)=
"closeManageUsersPopup()"
>
<div
class=
"option-icon-wrapper"
>
<span
class=
"material-symbols-rounded block-icon"
>
group_add
</span>
</div>
<div
class=
"option-text"
>
<h3>
Manage Groups
</h3>
<p>
Create and structure student categorization effectively
</p>
</div>
<span
class=
"material-symbols-rounded arrow-icon"
>
arrow_forward
</span>
</a>
</div>
</div>
</div>
}
Frontend/src/app/components/layout/layout.ts
View file @
fd448421
...
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
...
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import
{
RouterLink
,
RouterLinkActive
,
RouterOutlet
,
Router
}
from
'
@angular/router
'
;
import
{
RouterLink
,
RouterLinkActive
,
RouterOutlet
,
Router
}
from
'
@angular/router
'
;
import
{
AuthService
}
from
'
../../services/auth.service
'
;
import
{
AuthService
}
from
'
../../services/auth.service
'
;
import
{
ThemeService
,
ThemeMode
}
from
'
../../services/theme.service
'
;
import
{
ThemeService
,
ThemeMode
}
from
'
../../services/theme.service
'
;
import
{
UiService
}
from
'
../../services/ui.service
'
;
interface
NavItem
{
interface
NavItem
{
icon
:
string
;
icon
:
string
;
...
@@ -21,6 +22,7 @@ export class LayoutComponent {
...
@@ -21,6 +22,7 @@ export class LayoutComponent {
authService
=
inject
(
AuthService
);
authService
=
inject
(
AuthService
);
themeService
=
inject
(
ThemeService
);
themeService
=
inject
(
ThemeService
);
router
=
inject
(
Router
);
router
=
inject
(
Router
);
uiService
=
inject
(
UiService
);
showThemeMenu
=
false
;
showThemeMenu
=
false
;
mobileSidebarOpen
=
false
;
mobileSidebarOpen
=
false
;
...
@@ -88,4 +90,25 @@ export class LayoutComponent {
...
@@ -88,4 +90,25 @@ export class LayoutComponent {
logout
():
void
{
logout
():
void
{
this
.
authService
.
logout
();
this
.
authService
.
logout
();
}
}
handleNavClick
(
item
:
NavItem
,
event
:
Event
):
void
{
if
(
item
.
route
===
'
/admin/users
'
||
item
.
route
===
'
/hr/candidates
'
)
{
event
.
preventDefault
();
this
.
uiService
.
showManageUsersPopup
.
set
(
true
);
}
else
{
this
.
mobileSidebarOpen
=
false
;
}
}
closeManageUsersPopup
():
void
{
this
.
uiService
.
showManageUsersPopup
.
set
(
false
);
}
getUsersRoute
():
string
{
return
this
.
authService
.
getUserRole
()
===
'
hr
'
?
'
/hr/candidates
'
:
'
/admin/users
'
;
}
getManageGroupsRoute
():
string
{
return
this
.
authService
.
getUserRole
()
===
'
hr
'
?
'
/hr/manage-groups
'
:
'
/admin/manage-groups
'
;
}
}
}
Frontend/src/app/pages/admin/create-quiz/create-quiz.css
View file @
fd448421
...
@@ -25,3 +25,72 @@
...
@@ -25,3 +25,72 @@
.page-container
{
padding
:
20px
16px
;
}
.page-container
{
padding
:
20px
16px
;
}
.form-row
{
flex-direction
:
column
;
}
.form-row
{
flex-direction
:
column
;
}
}
}
.assignment-box
{
margin-top
:
12px
;
padding
:
16px
;
background
:
var
(
--bg-tertiary
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
var
(
--radius-md
);
box-shadow
:
var
(
--shadow-sm
);
}
.list-container
{
max-height
:
220px
;
overflow-y
:
auto
;
border
:
1px
solid
var
(
--border-color
);
border-radius
:
var
(
--radius-sm
);
background
:
var
(
--bg-secondary
);
display
:
flex
;
flex-direction
:
column
;
}
.list-item
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
padding
:
10px
14px
;
border-bottom
:
1px
solid
var
(
--border-color
);
cursor
:
pointer
;
transition
:
background
0.15s
ease
;
}
.list-item
:last-child
{
border-bottom
:
none
;
}
.list-item
:hover
{
background
:
var
(
--bg-hover
);
}
.list-item
input
[
type
=
"checkbox"
]
{
width
:
16px
;
height
:
16px
;
accent-color
:
var
(
--accent-primary
);
cursor
:
pointer
;
}
.item-details
{
display
:
flex
;
flex-direction
:
column
;
}
.item-name
{
font-size
:
14px
;
font-weight
:
500
;
color
:
var
(
--text-primary
);
}
.item-sub
{
font-size
:
12px
;
color
:
var
(
--text-muted
);
}
.empty-state
{
padding
:
20px
;
text-align
:
center
;
color
:
var
(
--text-muted
);
font-size
:
13px
;
font-style
:
italic
;
}
Frontend/src/app/pages/admin/create-quiz/create-quiz.html
View file @
fd448421
...
@@ -47,6 +47,59 @@
...
@@ -47,6 +47,59 @@
</div>
</div>
</div>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Assign To
</label>
<select
class=
"form-select"
[(ngModel)]=
"assignmentType"
name=
"assignmentType"
>
<option
value=
"all"
>
All Candidates
</option>
<option
value=
"users"
>
Individual People
</option>
<option
value=
"groups"
>
Specific Groups
</option>
</select>
</div>
@if (assignmentType === 'users') {
<div
class=
"form-group assignment-box"
>
<label
class=
"form-label"
>
Search Candidates
</label>
<input
class=
"form-input"
type=
"text"
[(ngModel)]=
"userSearchQuery"
name=
"userSearchQuery"
(input)=
"onUserSearch()"
placeholder=
"Search by name or email..."
>
<div
class=
"list-container mt-2"
>
@for (user of filteredUsers; track user._id) {
<div
class=
"list-item"
(click)=
"toggleUserSelection(user._id)"
>
<input
type=
"checkbox"
[checked]=
"selectedUsers.includes(user._id)"
(change)=
"toggleUserSelection(user._id)"
>
<div
class=
"item-details"
>
<span
class=
"item-name"
>
{{ user.name }}
</span>
<span
class=
"item-sub"
>
{{ user.email }}
</span>
</div>
</div>
}
@if (filteredUsers.length === 0) {
<div
class=
"empty-state"
>
No candidates found
</div>
}
</div>
</div>
}
@if (assignmentType === 'groups') {
<div
class=
"form-group assignment-box"
>
<label
class=
"form-label"
>
Select Groups
</label>
<div
class=
"list-container"
>
@for (group of availableGroups; track group) {
<div
class=
"list-item"
(click)=
"toggleGroupSelection(group)"
>
<input
type=
"checkbox"
[checked]=
"selectedGroups.includes(group)"
(change)=
"toggleGroupSelection(group)"
>
<div
class=
"item-details"
>
<span
class=
"item-name"
>
{{ group }}
</span>
</div>
</div>
}
@if (availableGroups.length === 0) {
<div
class=
"empty-state"
>
No groups found
</div>
}
</div>
</div>
}
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Questions File (Excel) *
</label>
<label
class=
"form-label"
>
Questions File (Excel) *
</label>
<div
class=
"file-upload"
(click)=
"fileInput.click()"
>
<div
class=
"file-upload"
(click)=
"fileInput.click()"
>
...
...
Frontend/src/app/pages/admin/create-quiz/create-quiz.ts
View file @
fd448421
import
{
Component
,
signal
}
from
'
@angular/core
'
;
import
{
Component
,
signal
,
OnInit
}
from
'
@angular/core
'
;
import
{
CommonModule
}
from
'
@angular/common
'
;
import
{
CommonModule
}
from
'
@angular/common
'
;
import
{
FormsModule
}
from
'
@angular/forms
'
;
import
{
FormsModule
}
from
'
@angular/forms
'
;
import
{
Router
,
RouterLink
}
from
'
@angular/router
'
;
import
{
Router
,
RouterLink
}
from
'
@angular/router
'
;
...
@@ -11,7 +11,7 @@ import { QuizService } from '../../../services/quiz.service';
...
@@ -11,7 +11,7 @@ import { QuizService } from '../../../services/quiz.service';
templateUrl
:
'
./create-quiz.html
'
,
templateUrl
:
'
./create-quiz.html
'
,
styleUrl
:
'
./create-quiz.css
'
styleUrl
:
'
./create-quiz.css
'
})
})
export
class
CreateQuizComponent
{
export
class
CreateQuizComponent
implements
OnInit
{
title
=
''
;
title
=
''
;
timer
=
30
;
timer
=
30
;
category
=
''
;
category
=
''
;
...
@@ -24,8 +24,69 @@ export class CreateQuizComponent {
...
@@ -24,8 +24,69 @@ export class CreateQuizComponent {
success
=
signal
(
''
);
success
=
signal
(
''
);
error
=
signal
(
''
);
error
=
signal
(
''
);
assignmentType
=
'
all
'
;
// 'all', 'users', 'groups'
availableUsers
:
any
[]
=
[];
filteredUsers
:
any
[]
=
[];
availableGroups
:
string
[]
=
[];
selectedUsers
:
string
[]
=
[];
selectedGroups
:
string
[]
=
[];
userSearchQuery
=
''
;
constructor
(
private
quizService
:
QuizService
,
private
router
:
Router
)
{}
constructor
(
private
quizService
:
QuizService
,
private
router
:
Router
)
{}
ngOnInit
():
void
{
this
.
fetchUsersAndGroups
();
}
fetchUsersAndGroups
():
void
{
// Fetch users (candidates)
this
.
quizService
.
getUsers
(
'
candidate
'
).
subscribe
({
next
:
(
res
)
=>
{
this
.
availableUsers
=
res
.
users
||
[];
this
.
filteredUsers
=
[...
this
.
availableUsers
];
},
error
:
(
err
)
=>
console
.
error
(
'
Failed to fetch users
'
,
err
)
});
// Fetch groups
this
.
quizService
.
getAdminGroups
().
subscribe
({
next
:
(
res
)
=>
{
this
.
availableGroups
=
res
.
groups
||
[];
},
error
:
(
err
)
=>
console
.
error
(
'
Failed to fetch groups
'
,
err
)
});
}
onUserSearch
():
void
{
if
(
!
this
.
userSearchQuery
.
trim
())
{
this
.
filteredUsers
=
[...
this
.
availableUsers
];
}
else
{
const
q
=
this
.
userSearchQuery
.
toLowerCase
();
this
.
filteredUsers
=
this
.
availableUsers
.
filter
(
u
=>
u
.
name
.
toLowerCase
().
includes
(
q
)
||
u
.
email
.
toLowerCase
().
includes
(
q
)
);
}
}
toggleUserSelection
(
userId
:
string
):
void
{
const
index
=
this
.
selectedUsers
.
indexOf
(
userId
);
if
(
index
>
-
1
)
{
this
.
selectedUsers
.
splice
(
index
,
1
);
}
else
{
this
.
selectedUsers
.
push
(
userId
);
}
}
toggleGroupSelection
(
group
:
string
):
void
{
const
index
=
this
.
selectedGroups
.
indexOf
(
group
);
if
(
index
>
-
1
)
{
this
.
selectedGroups
.
splice
(
index
,
1
);
}
else
{
this
.
selectedGroups
.
push
(
group
);
}
}
onFileSelected
(
event
:
Event
):
void
{
onFileSelected
(
event
:
Event
):
void
{
const
input
=
event
.
target
as
HTMLInputElement
;
const
input
=
event
.
target
as
HTMLInputElement
;
if
(
input
.
files
&&
input
.
files
.
length
>
0
)
{
if
(
input
.
files
&&
input
.
files
.
length
>
0
)
{
...
@@ -51,6 +112,17 @@ export class CreateQuizComponent {
...
@@ -51,6 +112,17 @@ export class CreateQuizComponent {
if
(
this
.
difficulty
)
formData
.
append
(
'
difficulty
'
,
this
.
difficulty
);
if
(
this
.
difficulty
)
formData
.
append
(
'
difficulty
'
,
this
.
difficulty
);
if
(
this
.
topic
)
formData
.
append
(
'
topic
'
,
this
.
topic
);
if
(
this
.
topic
)
formData
.
append
(
'
topic
'
,
this
.
topic
);
// Assignment handling
if
(
this
.
assignmentType
===
'
all
'
)
{
formData
.
append
(
'
assignToAll
'
,
'
true
'
);
}
else
if
(
this
.
assignmentType
===
'
users
'
)
{
formData
.
append
(
'
assignToAll
'
,
'
false
'
);
formData
.
append
(
'
assignees
'
,
JSON
.
stringify
(
this
.
selectedUsers
));
}
else
if
(
this
.
assignmentType
===
'
groups
'
)
{
formData
.
append
(
'
assignToAll
'
,
'
false
'
);
formData
.
append
(
'
assignedGroups
'
,
JSON
.
stringify
(
this
.
selectedGroups
));
}
this
.
quizService
.
createQuiz
(
formData
).
subscribe
({
this
.
quizService
.
createQuiz
(
formData
).
subscribe
({
next
:
(
res
)
=>
{
next
:
(
res
)
=>
{
this
.
loading
.
set
(
false
);
this
.
loading
.
set
(
false
);
...
...
Frontend/src/app/pages/admin/dashboard/dashboard.html
View file @
fd448421
...
@@ -70,7 +70,7 @@
...
@@ -70,7 +70,7 @@
<div
class=
"section"
>
<div
class=
"section"
>
<h2
class=
"section-title"
>
Quick Actions
</h2>
<h2
class=
"section-title"
>
Quick Actions
</h2>
<div
class=
"actions-grid"
>
<div
class=
"actions-grid"
>
<a
routerLink=
"/admin/users"
class=
"action-card card card-hover card-padding
"
>
<a
(click)=
"openUsersPopup()"
class=
"action-card card card-hover card-padding"
style=
"cursor: pointer;
"
>
<span
class=
"material-symbols-rounded action-icon"
>
group
</span>
<span
class=
"material-symbols-rounded action-icon"
>
group
</span>
<div
class=
"action-info"
>
<div
class=
"action-info"
>
<h3>
Manage Users
</h3>
<h3>
Manage Users
</h3>
...
...
Frontend/src/app/pages/admin/dashboard/dashboard.ts
View file @
fd448421
...
@@ -2,6 +2,7 @@ import { Component, OnInit, signal } from '@angular/core';
...
@@ -2,6 +2,7 @@ import { Component, OnInit, signal } from '@angular/core';
import
{
CommonModule
}
from
'
@angular/common
'
;
import
{
CommonModule
}
from
'
@angular/common
'
;
import
{
RouterLink
}
from
'
@angular/router
'
;
import
{
RouterLink
}
from
'
@angular/router
'
;
import
{
QuizService
}
from
'
../../../services/quiz.service
'
;
import
{
QuizService
}
from
'
../../../services/quiz.service
'
;
import
{
UiService
}
from
'
../../../services/ui.service
'
;
@
Component
({
@
Component
({
selector
:
'
app-admin-dashboard
'
,
selector
:
'
app-admin-dashboard
'
,
...
@@ -15,7 +16,7 @@ export class AdminDashboardComponent implements OnInit {
...
@@ -15,7 +16,7 @@ export class AdminDashboardComponent implements OnInit {
recentSubmissions
=
signal
<
any
[]
>
([]);
recentSubmissions
=
signal
<
any
[]
>
([]);
loading
=
signal
(
true
);
loading
=
signal
(
true
);
constructor
(
private
quizService
:
QuizService
)
{}
constructor
(
private
quizService
:
QuizService
,
private
uiService
:
UiService
)
{}
ngOnInit
():
void
{
ngOnInit
():
void
{
this
.
quizService
.
getAdminStats
().
subscribe
({
this
.
quizService
.
getAdminStats
().
subscribe
({
...
@@ -27,4 +28,8 @@ export class AdminDashboardComponent implements OnInit {
...
@@ -27,4 +28,8 @@ export class AdminDashboardComponent implements OnInit {
error
:
()
=>
this
.
loading
.
set
(
false
)
error
:
()
=>
this
.
loading
.
set
(
false
)
});
});
}
}
openUsersPopup
():
void
{
this
.
uiService
.
showManageUsersPopup
.
set
(
true
);
}
}
}
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.css
View file @
fd448421
...
@@ -89,3 +89,78 @@
...
@@ -89,3 +89,78 @@
.dashboard-layout
{
flex-direction
:
column
;
}
.dashboard-layout
{
flex-direction
:
column
;
}
.form-row
{
grid-template-columns
:
1
fr
;
}
.form-row
{
grid-template-columns
:
1
fr
;
}
}
}
.form-select
{
width
:
100%
;
padding
:
14px
16px
;
background
:
rgba
(
255
,
255
,
255
,
0.06
);
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0.12
);
border-radius
:
12px
;
color
:
#fff
;
font-size
:
15px
;
outline
:
none
;
transition
:
all
0.3s
;
font-family
:
inherit
;
box-sizing
:
border-box
;
}
.form-select
:focus
{
border-color
:
#667eea
;
box-shadow
:
0
0
0
3px
rgba
(
102
,
126
,
234
,
0.15
);
}
.assignment-box
{
margin-top
:
12px
;
padding
:
16px
;
background
:
rgba
(
255
,
255
,
255
,
0.02
);
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0.08
);
border-radius
:
12px
;
}
.list-container
{
max-height
:
220px
;
overflow-y
:
auto
;
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0.08
);
border-radius
:
8px
;
background
:
rgba
(
0
,
0
,
0
,
0.2
);
display
:
flex
;
flex-direction
:
column
;
}
.mt-2
{
margin-top
:
8px
;
}
.list-item
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
padding
:
10px
14px
;
border-bottom
:
1px
solid
rgba
(
255
,
255
,
255
,
0.05
);
cursor
:
pointer
;
transition
:
background
0.15s
ease
;
}
.list-item
:last-child
{
border-bottom
:
none
;
}
.list-item
:hover
{
background
:
rgba
(
255
,
255
,
255
,
0.05
);
}
.list-item
input
[
type
=
"checkbox"
]
{
width
:
16px
;
height
:
16px
;
cursor
:
pointer
;
}
.item-details
{
display
:
flex
;
flex-direction
:
column
;
}
.item-name
{
font-size
:
14px
;
font-weight
:
500
;
color
:
#fff
;
}
.item-sub
{
font-size
:
12px
;
color
:
rgba
(
255
,
255
,
255
,
0.5
);
}
.empty-state
{
padding
:
20px
;
text-align
:
center
;
color
:
rgba
(
255
,
255
,
255
,
0.5
);
font-size
:
13px
;
font-style
:
italic
;
}
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.html
View file @
fd448421
...
@@ -64,26 +64,56 @@
...
@@ -64,26 +64,56 @@
</div>
</div>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label>
Assign Quiz
</label>
<label>
Assign Quiz To
</label>
<select
class=
"form-select"
[ngModel]=
"assignmentType()"
(ngModelChange)=
"assignmentType.set($event)"
name=
"assignmentType"
>
<option
value=
"all"
>
All Candidates
</option>
<option
value=
"users"
>
Individual People
</option>
<option
value=
"groups"
>
Specific Groups
</option>
</select>
</div>
<div
style=
"margin-bottom: 10px;"
>
@if (assignmentType() === 'users') {
<label>
<div
class=
"form-group assignment-box"
>
<input
type=
"checkbox"
[checked]=
"assignToAll()"
(change)=
"assignToAll.set(!assignToAll())"
/>
<label>
Search Candidates
</label>
Assign to All Users
<input
class=
"form-input"
type=
"text"
[ngModel]=
"userSearchQuery()"
(ngModelChange)=
"userSearchQuery.set($event); onUserSearch()"
name=
"userSearchQuery"
placeholder=
"Search by name or email..."
>
</label>
<div
class=
"list-container mt-2"
>
@for (user of filteredUsers; track user._id) {
<div
class=
"list-item"
(click)=
"toggleUserSelection(user._id)"
>
<input
type=
"checkbox"
[checked]=
"selectedUsers().includes(user._id)"
(change)=
"toggleUserSelection(user._id)"
>
<div
class=
"item-details"
>
<span
class=
"item-name"
>
{{ user.name }}
</span>
<span
class=
"item-sub"
>
{{ user.email }}
</span>
</div>
</div>
}
@if (filteredUsers.length === 0) {
<div
class=
"empty-state"
>
No candidates found
</div>
}
</div>
</div>
</div>
}
@if (!assignToAll()) {
@if (assignmentType() === 'groups') {
<div
class=
"group-list"
>
<div
class=
"form-group assignment-box"
>
@for (group of groups(); track group._id) {
<label>
Select Groups
</label>
<label
style=
"display:block; margin-bottom:6px;"
>
<div
class=
"list-container mt-2"
>
<input
type=
"checkbox"
[value]=
"group.name"
(change)=
"onGroupToggle(group.name, $event)"
/>
@for (group of groups(); track group) {
{{ group.name }}
<div
class=
"list-item"
(click)=
"onGroupToggle(group, {target: {checked: !selectedGroups().includes(group)}})"
>
</label>
<input
type=
"checkbox"
[checked]=
"selectedGroups().includes(group)"
}
(change)=
"onGroupToggle(group, $event)"
>
<div
class=
"item-details"
>
<span
class=
"item-name"
>
{{ group }}
</span>
</div>
</div>
}
@if (groups().length === 0) {
<div
class=
"empty-state"
>
No groups found
</div>
}
</div>
</div>
</div>
}
}
</div>
<button
mat-raised-button
color=
"primary"
type=
"submit"
class=
"btn btn-primary"
[disabled]=
"loading()"
>
<button
mat-raised-button
color=
"primary"
type=
"submit"
class=
"btn btn-primary"
[disabled]=
"loading()"
>
@if (loading()) {
@if (loading()) {
<span
class=
"spinner"
></span>
Creating Quiz...
<span
class=
"spinner"
></span>
Creating Quiz...
...
...
Frontend/src/app/pages/admin/generate-quiz/generate-quiz.ts
View file @
fd448421
...
@@ -18,9 +18,14 @@ export class GenerateQuizComponent implements OnInit {
...
@@ -18,9 +18,14 @@ export class GenerateQuizComponent implements OnInit {
timer
=
30
;
timer
=
30
;
selectedFile
:
File
|
null
=
null
;
selectedFile
:
File
|
null
=
null
;
fileName
=
signal
<
string
>
(
''
);
fileName
=
signal
<
string
>
(
''
);
groups
=
signal
<
any
[]
>
([]);
groups
=
signal
<
any
[]
>
([]);
// Actually used for groups now, let's keep it as string[] if the backend returns strings
availableUsers
:
any
[]
=
[];
filteredUsers
:
any
[]
=
[];
assignmentType
=
signal
<
string
>
(
'
all
'
);
// 'all', 'users', 'groups'
selectedUsers
=
signal
<
string
[]
>
([]);
selectedGroups
=
signal
<
string
[]
>
([]);
selectedGroups
=
signal
<
string
[]
>
([]);
assignToAll
=
signal
<
boolean
>
(
true
);
userSearchQuery
=
signal
<
string
>
(
''
);
loading
=
signal
<
boolean
>
(
false
);
loading
=
signal
<
boolean
>
(
false
);
success
=
signal
<
string
>
(
''
);
success
=
signal
<
string
>
(
''
);
error
=
signal
<
string
>
(
''
);
error
=
signal
<
string
>
(
''
);
...
@@ -32,16 +37,46 @@ export class GenerateQuizComponent implements OnInit {
...
@@ -32,16 +37,46 @@ export class GenerateQuizComponent implements OnInit {
ngOnInit
():
void
{
ngOnInit
():
void
{
this
.
loadQuizzes
();
this
.
loadQuizzes
();
this
.
fetchUsersAndGroups
();
}
fetchUsersAndGroups
():
void
{
this
.
quizService
.
getAdminGroups
().
subscribe
({
this
.
quizService
.
getAdminGroups
().
subscribe
({
next
:
(
res
)
=>
{
next
:
(
res
)
=>
{
this
.
groups
.
set
(
res
.
groups
||
[]);
this
.
groups
.
set
(
res
.
groups
||
[]);
},
},
error
:
()
=>
{
error
:
()
=>
console
.
log
(
"
Failed to load groups
"
)
console
.
log
(
"
Failed to load groups
"
);
});
}
this
.
quizService
.
getUsers
(
'
candidate
'
).
subscribe
({
next
:
(
res
)
=>
{
this
.
availableUsers
=
res
.
users
||
[];
this
.
filteredUsers
=
[...
this
.
availableUsers
];
},
error
:
()
=>
console
.
log
(
"
Failed to load users
"
)
});
});
}
}
onUserSearch
():
void
{
const
q
=
this
.
userSearchQuery
().
toLowerCase
().
trim
();
if
(
!
q
)
{
this
.
filteredUsers
=
[...
this
.
availableUsers
];
}
else
{
this
.
filteredUsers
=
this
.
availableUsers
.
filter
(
u
=>
u
.
name
.
toLowerCase
().
includes
(
q
)
||
u
.
email
.
toLowerCase
().
includes
(
q
)
);
}
}
toggleUserSelection
(
userId
:
string
):
void
{
const
selected
=
this
.
selectedUsers
();
if
(
selected
.
includes
(
userId
))
{
this
.
selectedUsers
.
set
(
selected
.
filter
(
id
=>
id
!==
userId
));
}
else
{
this
.
selectedUsers
.
set
([...
selected
,
userId
]);
}
}
loadQuizzes
():
void
{
loadQuizzes
():
void
{
this
.
quizService
.
getAdminQuizzes
().
subscribe
({
this
.
quizService
.
getAdminQuizzes
().
subscribe
({
next
:
(
res
)
=>
{
next
:
(
res
)
=>
{
...
@@ -82,8 +117,17 @@ export class GenerateQuizComponent implements OnInit {
...
@@ -82,8 +117,17 @@ export class GenerateQuizComponent implements OnInit {
formData
.
append
(
'
title
'
,
this
.
title
);
formData
.
append
(
'
title
'
,
this
.
title
);
formData
.
append
(
'
timer
'
,
this
.
timer
.
toString
());
formData
.
append
(
'
timer
'
,
this
.
timer
.
toString
());
formData
.
append
(
'
questionsFile
'
,
this
.
selectedFile
);
formData
.
append
(
'
questionsFile
'
,
this
.
selectedFile
);
formData
.
append
(
'
assignToAll
'
,
this
.
assignToAll
().
toString
());
const
aType
=
this
.
assignmentType
();
formData
.
append
(
'
assignedGroups
'
,
JSON
.
stringify
(
this
.
selectedGroups
()));
if
(
aType
===
'
all
'
)
{
formData
.
append
(
'
assignToAll
'
,
'
true
'
);
}
else
if
(
aType
===
'
users
'
)
{
formData
.
append
(
'
assignToAll
'
,
'
false
'
);
formData
.
append
(
'
assignees
'
,
JSON
.
stringify
(
this
.
selectedUsers
()));
}
else
if
(
aType
===
'
groups
'
)
{
formData
.
append
(
'
assignToAll
'
,
'
false
'
);
formData
.
append
(
'
assignedGroups
'
,
JSON
.
stringify
(
this
.
selectedGroups
()));
}
this
.
quizService
.
createQuiz
(
formData
).
subscribe
({
this
.
quizService
.
createQuiz
(
formData
).
subscribe
({
next
:
(
res
)
=>
{
next
:
(
res
)
=>
{
this
.
loading
.
set
(
false
);
this
.
loading
.
set
(
false
);
...
...
Frontend/src/app/pages/admin/manage-groups/manage-groups.css
0 → 100644
View file @
fd448421
.page-container
{
padding
:
32px
40px
;
max-width
:
900px
;
}
.page-header
{
margin-bottom
:
28px
;
}
.page-header
h1
{
font-size
:
26px
;
font-weight
:
700
;
color
:
var
(
--text-primary
);
margin
:
0
0
8px
;
}
.page-subtitle
{
color
:
var
(
--text-muted
);
font-size
:
14px
;
margin
:
0
;
}
.section-title
{
font-size
:
18px
;
font-weight
:
600
;
color
:
var
(
--text-primary
);
margin-bottom
:
20px
;
border-bottom
:
1px
solid
var
(
--border-color
);
padding-bottom
:
8px
;
}
.form-card
.section-title
{
margin-top
:
0
;
border
:
none
;
padding
:
0
;
margin-bottom
:
16px
;
}
.card
{
background
:
var
(
--bg-card
);
border-radius
:
var
(
--radius-lg
);
border
:
1px
solid
var
(
--border-color
);
}
.card-padding
{
padding
:
24px
;
}
.group-form
{
display
:
flex
;
flex-direction
:
column
;
}
.row-align
{
display
:
flex
;
gap
:
16px
;
align-items
:
center
;
}
.input-container
{
flex
:
1
;
position
:
relative
;
display
:
flex
;
align-items
:
center
;
}
.input-container
.block-icon
{
position
:
absolute
;
left
:
16px
;
color
:
var
(
--text-muted
);
font-size
:
20px
;
}
.form-input
{
width
:
100%
;
padding
:
14px
16px
14px
44px
;
background
:
var
(
--bg-input
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
12px
;
color
:
var
(
--text-primary
);
outline
:
none
;
transition
:
all
0.3s
;
font-size
:
15px
;
box-sizing
:
border-box
;
}
.form-input
:focus
{
border-color
:
var
(
--accent-primary
);
box-shadow
:
0
0
0
3px
rgba
(
102
,
126
,
234
,
0.15
);
}
.btn
{
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
8px
;
padding
:
14px
24px
;
border
:
none
;
border-radius
:
12px
;
font-weight
:
600
;
cursor
:
pointer
;
transition
:
all
0.3s
;
font-family
:
inherit
;
font-size
:
15px
;
}
.btn-primary
{
background
:
var
(
--accent-gradient
);
color
:
#fff
;
box-shadow
:
0
4px
15px
rgba
(
102
,
126
,
234
,
0.3
);
}
.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
(
260px
,
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
:
center
;
gap
:
16px
;
transition
:
all
0.3s
;
}
.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
;
}
.group-details
h4
{
margin
:
0
;
font-size
:
17px
;
font-weight
:
600
;
color
:
var
(
--text-primary
);
}
.alert
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
14px
20px
;
border-radius
:
12px
;
margin-bottom
:
24px
;
font-size
:
14px
;
font-weight
:
500
;
}
.alert-success
{
background
:
rgba
(
74
,
222
,
128
,
0.1
);
border
:
1px
solid
rgba
(
74
,
222
,
128
,
0.2
);
color
:
#22c55e
;
}
.alert-error
{
background
:
rgba
(
239
,
68
,
68
,
0.1
);
border
:
1px
solid
rgba
(
239
,
68
,
68
,
0.2
);
color
:
#ef4444
;
}
.spinner
{
width
:
18px
;
height
:
18px
;
border
:
2px
solid
rgba
(
255
,
255
,
255
,
0.3
);
border-top-color
:
#fff
;
border-radius
:
50%
;
animation
:
spin
0.6s
linear
infinite
;
}
.loader
{
width
:
40px
;
height
:
40px
;
border
:
3px
solid
rgba
(
102
,
126
,
234
,
0.2
);
border-top-color
:
var
(
--accent-primary
);
border-radius
:
50%
;
animation
:
spin
0.8s
linear
infinite
;
}
.loading-state
{
display
:
flex
;
justify-content
:
center
;
padding
:
40px
;
}
@keyframes
spin
{
100
%
{
transform
:
rotate
(
360deg
);
}
}
.empty-state
{
text-align
:
center
;
padding
:
60px
0
;
border
:
1px
dashed
var
(
--border-color
);
border-radius
:
16px
;
background
:
rgba
(
255
,
255
,
255
,
0.02
);
}
.empty-icon
{
font-size
:
48px
;
color
:
var
(
--text-muted
);
display
:
block
;
margin-bottom
:
12px
;
}
.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
:
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/manage-groups/manage-groups.html
0 → 100644
View file @
fd448421
<div
class=
"page-container animate-fade-in"
>
<div
class=
"page-header"
>
<h1>
Manage Groups
</h1>
<p
class=
"page-subtitle"
>
Create and oversee organizational groups for candidates
</p>
</div>
@if (error()) {
<div
class=
"alert alert-error"
><span
class=
"material-symbols-rounded"
>
error
</span>
{{ error() }}
</div>
}
@if (success()) {
<div
class=
"alert alert-success"
><span
class=
"material-symbols-rounded"
>
check_circle
</span>
{{ success() }}
</div>
}
<div
class=
"card card-padding form-card"
style=
"margin-bottom: 40px;"
>
<h2
class=
"section-title"
>
Create New Group
</h2>
<form
(ngSubmit)=
"createGroup()"
class=
"group-form"
>
<div
class=
"form-group row-align"
>
<div
class=
"input-container"
>
<span
class=
"material-symbols-rounded block-icon"
>
group_add
</span>
<input
class=
"form-input"
[ngModel]=
"newGroupName()"
(ngModelChange)=
"newGroupName.set($event)"
name=
"groupName"
placeholder=
"e.g., Spring Cohort 2026"
>
</div>
<button
type=
"submit"
class=
"btn btn-primary"
[disabled]=
"loading() || !newGroupName().trim()"
>
@if (loading()) {
<span
class=
"spinner"
></span>
} @else {
<span
class=
"material-symbols-rounded"
>
add
</span>
Add Group
}
</button>
</div>
</form>
</div>
<h2
class=
"section-title"
>
Existing Groups
</h2>
@if (loadingGroups()) {
<div
class=
"loading-state"
>
<div
class=
"loader"
></div>
</div>
} @else if (groups().length === 0) {
<div
class=
"empty-state"
>
<span
class=
"material-symbols-rounded empty-icon"
>
group_off
</span>
<h3>
No groups available
</h3>
<p>
Create your first group above
</p>
</div>
} @else {
<div
class=
"groups-grid"
>
@for (group of groups(); track group) {
<div
class=
"group-card"
>
<div
class=
"group-icon-wrapper"
>
<span
class=
"material-symbols-rounded"
>
group
</span>
</div>
<div
class=
"group-details"
style=
"flex: 1;"
>
@if (editingGroup() === group) {
<input
type=
"text"
class=
"form-input form-input-small"
[ngModel]=
"editName()"
(ngModelChange)=
"editName.set($event)"
autofocus
>
} @else {
<h4>
{{ group }}
</h4>
}
</div>
<div
class=
"group-actions"
>
@if (editingGroup() === group) {
<button
class=
"action-btn text-success"
(click)=
"saveEdit(group)"
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>
} @else {
<button
class=
"action-btn"
(click)=
"startEdit(group)"
title=
"Edit"
><span
class=
"material-symbols-rounded"
>
edit
</span></button>
<button
class=
"action-btn text-danger"
(click)=
"deleteGroup(group)"
title=
"Delete"
><span
class=
"material-symbols-rounded"
>
delete
</span></button>
}
</div>
</div>
}
</div>
}
</div>
Frontend/src/app/pages/admin/manage-groups/manage-groups.ts
0 → 100644
View file @
fd448421
import
{
Component
,
signal
,
OnInit
}
from
'
@angular/core
'
;
import
{
CommonModule
}
from
'
@angular/common
'
;
import
{
FormsModule
}
from
'
@angular/forms
'
;
import
{
RouterLink
}
from
'
@angular/router
'
;
import
{
QuizService
}
from
'
../../../services/quiz.service
'
;
@
Component
({
selector
:
'
app-manage-groups
'
,
standalone
:
true
,
imports
:
[
CommonModule
,
FormsModule
,
RouterLink
],
templateUrl
:
'
./manage-groups.html
'
,
styleUrl
:
'
./manage-groups.css
'
})
export
class
ManageGroupsComponent
implements
OnInit
{
groups
=
signal
<
string
[]
>
([]);
newGroupName
=
signal
<
string
>
(
''
);
loading
=
signal
<
boolean
>
(
false
);
error
=
signal
<
string
>
(
''
);
success
=
signal
<
string
>
(
''
);
loadingGroups
=
signal
<
boolean
>
(
true
);
editingGroup
=
signal
<
string
|
null
>
(
null
);
editName
=
signal
<
string
>
(
''
);
constructor
(
private
quizService
:
QuizService
)
{}
ngOnInit
():
void
{
this
.
loadGroups
();
}
loadGroups
():
void
{
this
.
loadingGroups
.
set
(
true
);
this
.
quizService
.
getGroups
().
subscribe
({
next
:
(
res
)
=>
{
this
.
groups
.
set
(
res
.
groups
||
[]);
this
.
loadingGroups
.
set
(
false
);
},
error
:
(
err
)
=>
{
this
.
error
.
set
(
'
Failed to load groups
'
);
this
.
loadingGroups
.
set
(
false
);
}
});
}
createGroup
():
void
{
if
(
!
this
.
newGroupName
().
trim
())
{
this
.
error
.
set
(
'
Group name cannot be blank
'
);
return
;
}
this
.
loading
.
set
(
true
);
this
.
error
.
set
(
''
);
this
.
success
.
set
(
''
);
this
.
quizService
.
createGroup
(
this
.
newGroupName
()).
subscribe
({
next
:
()
=>
{
this
.
success
.
set
(
'
Group created successfully!
'
);
this
.
loading
.
set
(
false
);
this
.
newGroupName
.
set
(
''
);
this
.
loadGroups
();
},
error
:
(
err
:
any
)
=>
{
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to create group
'
);
this
.
loading
.
set
(
false
);
}
});
}
startEdit
(
name
:
string
):
void
{
this
.
editingGroup
.
set
(
name
);
this
.
editName
.
set
(
name
);
}
cancelEdit
():
void
{
this
.
editingGroup
.
set
(
null
);
}
saveEdit
(
oldName
:
string
):
void
{
const
trimmed
=
this
.
editName
().
trim
();
if
(
!
trimmed
||
trimmed
===
oldName
)
{
this
.
cancelEdit
();
return
;
}
this
.
error
.
set
(
''
);
this
.
success
.
set
(
''
);
this
.
quizService
.
updateGroup
(
oldName
,
trimmed
).
subscribe
({
next
:
()
=>
{
this
.
success
.
set
(
'
Group updated successfully!
'
);
this
.
cancelEdit
();
this
.
loadGroups
();
},
error
:
(
err
)
=>
{
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to update group
'
);
}
});
}
deleteGroup
(
name
:
string
):
void
{
if
(
confirm
(
`Are you sure you want to delete the group "
${
name
}
"?\nUsers dynamically mapped to this group will lose group context.`
))
{
this
.
error
.
set
(
''
);
this
.
success
.
set
(
''
);
this
.
quizService
.
deleteGroup
(
name
).
subscribe
({
next
:
()
=>
{
this
.
success
.
set
(
'
Group deleted successfully!
'
);
this
.
loadGroups
();
},
error
:
(
err
)
=>
{
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to delete group
'
);
}
});
}
}
}
Frontend/src/app/pages/register/register.css
View file @
fd448421
...
@@ -237,7 +237,8 @@
...
@@ -237,7 +237,8 @@
transition
:
color
0.2s
;
transition
:
color
0.2s
;
}
}
.input-group
input
{
.input-group
input
,
.input-group
select
{
width
:
100%
;
width
:
100%
;
padding
:
13px
16px
13px
48px
;
padding
:
13px
16px
13px
48px
;
background
:
#f8f9fb
;
background
:
#f8f9fb
;
...
@@ -250,11 +251,17 @@
...
@@ -250,11 +251,17 @@
transition
:
all
0.2s
ease
;
transition
:
all
0.2s
ease
;
}
}
.input-group
select
{
cursor
:
pointer
;
appearance
:
auto
;
}
.input-group
input
::placeholder
{
.input-group
input
::placeholder
{
color
:
#b0b8c4
;
color
:
#b0b8c4
;
}
}
.input-group
input
:focus
{
.input-group
input
:focus
,
.input-group
select
:focus
{
border-color
:
#4f6ef7
;
border-color
:
#4f6ef7
;
background
:
#fff
;
background
:
#fff
;
box-shadow
:
0
0
0
3px
rgba
(
79
,
110
,
247
,
0.08
);
box-shadow
:
0
0
0
3px
rgba
(
79
,
110
,
247
,
0.08
);
...
...
Frontend/src/app/pages/register/register.html
View file @
fd448421
...
@@ -76,6 +76,25 @@
...
@@ -76,6 +76,25 @@
</div>
</div>
</div>
</div>
@if (availableGroups().length > 0) {
<div
class=
"field"
>
<label
for=
"group"
>
Student Group (Optional)
</label>
<div
class=
"input-group"
>
<span
class=
"material-symbols-rounded input-icon"
>
group
</span>
<select
id=
"group"
[(ngModel)]=
"group"
name=
"group"
>
<option
value=
""
disabled
selected
>
Select your group
</option>
@for (grp of availableGroups(); track grp) {
<option
[value]=
"grp"
>
{{ grp }}
</option>
}
</select>
</div>
</div>
}
<div
class=
"fields-row"
>
<div
class=
"fields-row"
>
<div
class=
"field"
>
<div
class=
"field"
>
<label
for=
"password"
>
Password
</label>
<label
for=
"password"
>
Password
</label>
...
...
Frontend/src/app/pages/register/register.ts
View file @
fd448421
import
{
Component
,
signal
}
from
'
@angular/core
'
;
import
{
Component
,
signal
,
OnInit
}
from
'
@angular/core
'
;
import
{
CommonModule
}
from
'
@angular/common
'
;
import
{
CommonModule
}
from
'
@angular/common
'
;
import
{
FormsModule
}
from
'
@angular/forms
'
;
import
{
FormsModule
}
from
'
@angular/forms
'
;
import
{
Router
,
RouterLink
}
from
'
@angular/router
'
;
import
{
Router
,
RouterLink
}
from
'
@angular/router
'
;
...
@@ -11,16 +11,25 @@ import { AuthService } from '../../services/auth.service';
...
@@ -11,16 +11,25 @@ import { AuthService } from '../../services/auth.service';
templateUrl
:
'
./register.html
'
,
templateUrl
:
'
./register.html
'
,
styleUrl
:
'
./register.css
'
styleUrl
:
'
./register.css
'
})
})
export
class
RegisterComponent
{
export
class
RegisterComponent
implements
OnInit
{
name
=
''
;
name
=
''
;
email
=
''
;
email
=
''
;
password
=
''
;
password
=
''
;
confirmPassword
=
''
;
confirmPassword
=
''
;
group
=
''
;
availableGroups
=
signal
<
string
[]
>
([]);
error
=
signal
<
string
>
(
''
);
error
=
signal
<
string
>
(
''
);
loading
=
signal
<
boolean
>
(
false
);
loading
=
signal
<
boolean
>
(
false
);
constructor
(
private
authService
:
AuthService
,
private
router
:
Router
)
{}
constructor
(
private
authService
:
AuthService
,
private
router
:
Router
)
{}
ngOnInit
():
void
{
this
.
authService
.
getRegistrationGroups
().
subscribe
({
next
:
(
res
)
=>
this
.
availableGroups
.
set
(
res
.
groups
||
[]),
error
:
()
=>
console
.
error
(
'
Failed to load groups for registration
'
)
});
}
onSubmit
():
void
{
onSubmit
():
void
{
if
(
!
this
.
name
||
!
this
.
email
||
!
this
.
password
||
!
this
.
confirmPassword
)
{
if
(
!
this
.
name
||
!
this
.
email
||
!
this
.
password
||
!
this
.
confirmPassword
)
{
this
.
error
.
set
(
'
Please fill in all fields
'
);
this
.
error
.
set
(
'
Please fill in all fields
'
);
...
@@ -40,7 +49,7 @@ export class RegisterComponent {
...
@@ -40,7 +49,7 @@ export class RegisterComponent {
this
.
loading
.
set
(
true
);
this
.
loading
.
set
(
true
);
this
.
error
.
set
(
''
);
this
.
error
.
set
(
''
);
this
.
authService
.
register
(
this
.
name
,
this
.
email
,
this
.
password
).
subscribe
({
this
.
authService
.
register
(
this
.
name
,
this
.
email
,
this
.
password
,
this
.
group
).
subscribe
({
next
:
()
=>
{
next
:
()
=>
{
this
.
loading
.
set
(
false
);
this
.
loading
.
set
(
false
);
alert
(
'
Registration successful! Please sign in.
'
);
alert
(
'
Registration successful! Please sign in.
'
);
...
...
Frontend/src/app/services/auth.service.ts
View file @
fd448421
...
@@ -39,8 +39,12 @@ export class AuthService {
...
@@ -39,8 +39,12 @@ export class AuthService {
}
}
}
}
register
(
name
:
string
,
email
:
string
,
password
:
string
):
Observable
<
AuthResponse
>
{
getRegistrationGroups
():
Observable
<
any
>
{
return
this
.
http
.
post
<
AuthResponse
>
(
`
${
this
.
apiUrl
}
/register`
,
{
name
,
email
,
password
});
return
this
.
http
.
get
(
`
${
this
.
apiUrl
}
/groups`
);
}
register
(
name
:
string
,
email
:
string
,
password
:
string
,
group
?:
string
):
Observable
<
AuthResponse
>
{
return
this
.
http
.
post
<
AuthResponse
>
(
`
${
this
.
apiUrl
}
/register`
,
{
name
,
email
,
password
,
group
});
}
}
login
(
email
:
string
,
password
:
string
):
Observable
<
AuthResponse
>
{
login
(
email
:
string
,
password
:
string
):
Observable
<
AuthResponse
>
{
...
...
Frontend/src/app/services/quiz.service.ts
View file @
fd448421
...
@@ -150,4 +150,33 @@
...
@@ -150,4 +150,33 @@
getResultDetails
(
submissionId
:
string
):
Observable
<
any
>
{
getResultDetails
(
submissionId
:
string
):
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/results/
${
submissionId
}
`
);
return
this
.
http
.
get
(
`
${
this
.
candidateUrl
}
/results/
${
submissionId
}
`
);
}
}
// ========== GENERIC GROUP MANAGEMENT ==========
private
getBaseUrl
():
string
{
const
userStr
=
sessionStorage
.
getItem
(
'
user
'
);
if
(
userStr
)
{
try
{
const
user
=
JSON
.
parse
(
userStr
);
if
(
user
.
role
===
'
hr
'
)
return
this
.
hrUrl
;
}
catch
(
e
)
{}
}
return
this
.
adminUrl
;
}
createGroup
(
name
:
string
):
Observable
<
any
>
{
return
this
.
http
.
post
(
`
${
this
.
getBaseUrl
()}
/groups`
,
{
name
});
}
getGroups
():
Observable
<
any
>
{
return
this
.
http
.
get
(
`
${
this
.
getBaseUrl
()}
/groups`
);
}
updateGroup
(
oldName
:
string
,
newName
:
string
):
Observable
<
any
>
{
return
this
.
http
.
put
(
`
${
this
.
getBaseUrl
()}
/groups/
${
encodeURIComponent
(
oldName
)}
`
,
{
newName
});
}
deleteGroup
(
name
:
string
):
Observable
<
any
>
{
return
this
.
http
.
delete
(
`
${
this
.
getBaseUrl
()}
/groups/
${
encodeURIComponent
(
name
)}
`
);
}
}
}
Frontend/src/app/services/ui.service.ts
0 → 100644
View file @
fd448421
import
{
Injectable
,
signal
}
from
'
@angular/core
'
;
@
Injectable
({
providedIn
:
'
root
'
})
export
class
UiService
{
showManageUsersPopup
=
signal
<
boolean
>
(
false
);
}
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