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
Rishikumar
Hire-Guru
Commits
c2e9bafa
Commit
c2e9bafa
authored
Apr 28, 2026
by
AravindR-K
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor : Changes made in edit page and user-history page
parent
d2d978c5
Changes
19
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
1319 additions
and
580 deletions
+1319
-580
.vscode/settings.json
.vscode/settings.json
+1
-0
Backend/routes/admin.js
Backend/routes/admin.js
+1
-1
Backend/routes/hr.js
Backend/routes/hr.js
+1
-1
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.css
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.css
+175
-15
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.html
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.html
+57
-54
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.ts
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.ts
+84
-49
Frontend/src/app/pages/admin/quizzes/quizzes.css
Frontend/src/app/pages/admin/quizzes/quizzes.css
+46
-0
Frontend/src/app/pages/admin/quizzes/quizzes.html
Frontend/src/app/pages/admin/quizzes/quizzes.html
+56
-2
Frontend/src/app/pages/admin/quizzes/quizzes.ts
Frontend/src/app/pages/admin/quizzes/quizzes.ts
+56
-3
Frontend/src/app/pages/admin/user-history/user-history.css
Frontend/src/app/pages/admin/user-history/user-history.css
+346
-328
Frontend/src/app/pages/admin/user-history/user-history.html
Frontend/src/app/pages/admin/user-history/user-history.html
+10
-1
Frontend/src/app/pages/admin/user-history/user-history.ts
Frontend/src/app/pages/admin/user-history/user-history.ts
+8
-0
Frontend/src/app/pages/hr/edit-quiz/edit-quiz.css
Frontend/src/app/pages/hr/edit-quiz/edit-quiz.css
+175
-15
Frontend/src/app/pages/hr/edit-quiz/edit-quiz.html
Frontend/src/app/pages/hr/edit-quiz/edit-quiz.html
+54
-55
Frontend/src/app/pages/hr/edit-quiz/edit-quiz.ts
Frontend/src/app/pages/hr/edit-quiz/edit-quiz.ts
+82
-50
Frontend/src/app/pages/hr/quizzes/quizzes.css
Frontend/src/app/pages/hr/quizzes/quizzes.css
+46
-0
Frontend/src/app/pages/hr/quizzes/quizzes.html
Frontend/src/app/pages/hr/quizzes/quizzes.html
+57
-3
Frontend/src/app/pages/hr/quizzes/quizzes.ts
Frontend/src/app/pages/hr/quizzes/quizzes.ts
+56
-3
Frontend/src/app/pages/hr/user-history/user-history.ts
Frontend/src/app/pages/hr/user-history/user-history.ts
+8
-0
No files found.
.vscode/settings.json
0 → 100644
View file @
c2e9bafa
{}
\ No newline at end of file
Backend/routes/admin.js
View file @
c2e9bafa
...
...
@@ -686,7 +686,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
type
:
q
.
correctAnswers
.
length
>
1
?
'
mcq
'
:
'
single
'
}));
await
Question
.
insertMany
(
questions
);
await
Question
.
insertMany
(
question
Doc
s
);
updateData
.
totalQuestions
=
questions
.
length
;
}
...
...
Backend/routes/hr.js
View file @
c2e9bafa
...
...
@@ -655,7 +655,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
type
:
q
.
correctAnswers
.
length
>
1
?
'
mcq
'
:
'
single
'
}));
await
Question
.
insertMany
(
questions
);
await
Question
.
insertMany
(
question
Doc
s
);
updateData
.
totalQuestions
=
questions
.
length
;
}
...
...
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.css
View file @
c2e9bafa
.page-container
{
padding
:
32px
40px
;
max-width
:
9
0
0px
;
}
.page-container
{
padding
:
32px
40px
;
max-width
:
9
6
0px
;
}
.page-header
{
margin-bottom
:
28px
;
}
.page-header
h1
{
font-size
:
26px
;
font-weight
:
700
;
color
:
var
(
--text-primary
);
margin
:
8px
0
0
;
}
.back-link
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
6px
;
font-size
:
13px
;
color
:
var
(
--text-secondary
);
font-weight
:
500
;
}
.back-link
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
6px
;
font-size
:
13px
;
color
:
var
(
--text-secondary
);
font-weight
:
500
;
text-decoration
:
none
;
}
.back-link
:hover
{
color
:
var
(
--accent-primary
);
}
.loading-center
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
padding
:
80px
0
;
gap
:
16px
;
}
.loading-center
p
{
color
:
var
(
--text-muted
);
}
.form-card
{
margin-bottom
:
24px
;
}
.quiz-form
{
display
:
flex
;
flex-direction
:
column
;
gap
:
20px
;
}
.form-group
{
display
:
flex
;
flex-direction
:
column
;
flex
:
1
;
}
.form-row
{
display
:
flex
;
gap
:
16px
;
}
/* Questions Header */
.questions-header
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
margin
:
0
0
20px
;
}
.questions-header
h2
{
font-size
:
18px
;
font-weight
:
600
;
color
:
var
(
--text-primary
);
margin
:
0
;
}
.questions-header
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
margin
:
24px
0
16px
;
}
.questions-
header
h2
{
font-size
:
18px
;
font-weight
:
600
;
color
:
var
(
--text-primary
)
;
}
/* Questions List */
.questions-
list
{
display
:
flex
;
flex-direction
:
column
;
gap
:
20px
;
}
.question-card
{
margin-bottom
:
16px
;
display
:
flex
;
flex-direction
:
column
;
gap
:
14px
;
}
.question-header
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
}
.q-number
{
font-size
:
13px
;
font-weight
:
700
;
color
:
var
(
--accent-primary
);
background
:
var
(
--accent-primary-light
);
padding
:
4px
12px
;
border-radius
:
var
(
--radius-full
);
}
/* Question Card — matches submission-detail card style */
.question-card
{
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
padding
:
24px
;
border-left
:
4px
solid
var
(
--accent-primary
);
transition
:
box-shadow
0.2s
ease
,
border-color
0.2s
ease
;
}
.question-card
:hover
{
box-shadow
:
0
4px
16px
rgba
(
0
,
0
,
0
,
0.06
);
}
/* Question Header */
.q-header
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
margin-bottom
:
16px
;
}
.q-number
{
background
:
rgba
(
var
(
--accent-primary-rgb
),
0.15
);
color
:
var
(
--accent-primary
);
padding
:
4px
14px
;
border-radius
:
8px
;
font-weight
:
700
;
font-size
:
13px
;
flex-shrink
:
0
;
}
.q-type-badge
{
background
:
var
(
--bg-input
);
color
:
var
(
--text-secondary
);
padding
:
4px
10px
;
border-radius
:
8px
;
font-size
:
11px
;
font-weight
:
600
;
text-transform
:
uppercase
;
letter-spacing
:
0.3px
;
}
.delete-q-btn
{
margin-left
:
auto
;
background
:
transparent
;
border
:
1px
solid
var
(
--danger-border
);
color
:
var
(
--danger
);
width
:
34px
;
height
:
34px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
border-radius
:
8px
;
cursor
:
pointer
;
transition
:
all
0.2s
;
}
.delete-q-btn
.material-symbols-rounded
{
font-size
:
18px
;
}
.delete-q-btn
:hover
{
background
:
var
(
--danger
);
color
:
#fff
;
border-color
:
var
(
--danger
);
}
/* Editable Question Text */
.q-text-wrap
{
margin-bottom
:
16px
;
}
.q-text-input
{
width
:
100%
;
padding
:
12px
16px
;
background
:
var
(
--bg-input
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
10px
;
font-size
:
15px
;
line-height
:
1.5
;
color
:
var
(
--text-primary
);
font-family
:
inherit
;
transition
:
border-color
0.2s
;
}
.q-text-input
:focus
{
outline
:
none
;
border-color
:
var
(
--accent-primary
);
box-shadow
:
0
0
0
3px
rgba
(
var
(
--accent-primary-rgb
),
0.1
);
}
.q-text-input
::placeholder
{
color
:
var
(
--text-muted
);
}
/* Options Grid — same 2-col layout as submission-detail */
.options-grid
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
10px
;
margin-bottom
:
16px
;
}
.options-grid
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
10px
;
}
.option-input-wrap
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.option-letter
{
font-size
:
12px
;
font-weight
:
700
;
color
:
var
(
--text-muted
);
width
:
20px
;
text-align
:
center
;
flex-shrink
:
0
;
}
.option
{
padding
:
0
;
border-radius
:
10px
;
background
:
var
(
--bg-input
);
border
:
1px
solid
var
(
--border-color
);
transition
:
all
0.2s
;
cursor
:
pointer
;
position
:
relative
;
}
.option
:hover
{
border-color
:
var
(
--accent-primary
);
background
:
rgba
(
var
(
--accent-primary-rgb
),
0.04
);
}
.option.correct-answer
{
background
:
rgba
(
74
,
222
,
128
,
0.1
);
border-color
:
rgba
(
74
,
222
,
128
,
0.4
);
}
.option-inner
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
padding
:
2px
4px
2px
14px
;
}
.option-marker
{
font-weight
:
700
;
font-size
:
16px
;
color
:
var
(
--success
);
width
:
18px
;
flex-shrink
:
0
;
}
.option-input
{
flex
:
1
;
padding
:
10px
12px
;
background
:
transparent
;
border
:
none
;
font-size
:
14px
;
color
:
var
(
--text-primary
);
font-family
:
inherit
;
outline
:
none
;
min-width
:
0
;
}
.option-input
::placeholder
{
color
:
var
(
--text-muted
);
}
/* Correct Answer Row */
.correct-answer-row
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
12px
16px
;
background
:
rgba
(
74
,
222
,
128
,
0.06
);
border
:
1px
solid
rgba
(
74
,
222
,
128
,
0.2
);
border-radius
:
10px
;
}
.correct-icon
{
color
:
var
(
--success
);
font-size
:
20px
;
}
.correct-label
{
font-size
:
13px
;
font-weight
:
600
;
color
:
var
(
--text-secondary
);
white-space
:
nowrap
;
}
.correct-select
{
flex
:
1
;
padding
:
6px
12px
;
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
8px
;
font-size
:
13px
;
color
:
var
(
--text-primary
);
font-family
:
inherit
;
cursor
:
pointer
;
}
.correct-select
:focus
{
outline
:
none
;
border-color
:
var
(
--success
);
}
/* Save Button */
.save-btn
{
width
:
100%
;
margin-top
:
24px
;
}
@media
(
max-width
:
768px
)
{
.page-container
{
padding
:
20px
16px
;
}
.
form-row
,
.options-grid
{
flex-direction
:
column
;
grid-template-columns
:
1
fr
;
}
.
options-grid
{
grid-template-columns
:
1
fr
;
}
}
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.html
View file @
c2e9bafa
...
...
@@ -19,32 +19,6 @@
@if (loading()) {
<div
class=
"loading-center"
><div
class=
"spinner spinner-lg"
></div><p>
Loading quiz...
</p></div>
} @else {
<div
class=
"card card-padding form-card"
>
<div
class=
"quiz-form"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Quiz Title
</label>
<input
class=
"form-input"
[(ngModel)]=
"title"
placeholder=
"Quiz title"
>
</div>
<div
class=
"form-row"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Timer (min)
</label>
<input
class=
"form-input"
type=
"number"
[(ngModel)]=
"timer"
min=
"1"
>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Difficulty
</label>
<select
class=
"form-select"
[(ngModel)]=
"difficulty"
>
<option
value=
"easy"
>
Easy
</option>
<option
value=
"medium"
>
Medium
</option>
<option
value=
"hard"
>
Hard
</option>
</select>
</div>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Category
</label>
<input
class=
"form-input"
[(ngModel)]=
"category"
placeholder=
"e.g. Java, Angular, Data Structures"
>
</div>
</div>
</div>
@if (!locked()) {
<div
class=
"questions-header"
>
...
...
@@ -54,37 +28,66 @@
</button>
</div>
@for (q of questions(); track $index; let i = $index) {
<div
class=
"card card-padding question-card"
>
<div
class=
"question-header"
>
<span
class=
"q-number"
>
Q{{ i + 1 }}
</span>
<button
class=
"btn btn-ghost btn-sm"
(click)=
"removeQuestion(i)"
>
<span
class=
"material-symbols-rounded"
>
delete
</span>
</button>
</div>
<div
class=
"form-group"
>
<input
class=
"form-input"
[ngModel]=
"q.question"
(ngModelChange)=
"updateQuestion(i, 'question', $event)"
placeholder=
"Question text"
>
</div>
<div
class=
"options-grid"
>
@for (opt of q.options; track $index; let j = $index) {
<div
class=
"option-input-wrap"
>
<span
class=
"option-letter"
>
{{ ['A','B','C','D'][j] }}
</span>
<input
class=
"form-input"
[ngModel]=
"opt"
(ngModelChange)=
"updateOption(i, j, $event)"
placeholder=
"Option {{ j+1 }}"
>
</div>
}
</div>
<div
class=
"form-row"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Correct Answer
</label>
<input
class=
"form-input"
[ngModel]=
"q.correctAnswer"
(ngModelChange)=
"updateQuestion(i, 'correctAnswer', $event)"
placeholder=
"Correct answer text"
>
<div
class=
"questions-list"
>
@for (q of questions(); track $index; let i = $index) {
<div
class=
"question-card"
>
<!-- Question Header -->
<div
class=
"q-header"
>
<span
class=
"q-number"
>
Q{{ i + 1 }}
</span>
<span
class=
"q-type-badge"
>
{{ q.type === 'mcq' ? 'MCQ' : 'SINGLE' }}
</span>
<button
class=
"delete-q-btn"
(click)=
"removeQuestion(i)"
title=
"Delete question"
>
<span
class=
"material-symbols-rounded"
>
delete
</span>
</button>
</div>
<div
class=
"form-group"
style=
"max-width: 120px;"
>
<label
class=
"form-label"
>
Marks
</label>
<input
class=
"form-input"
type=
"number"
[ngModel]=
"q.marks"
(ngModelChange)=
"updateQuestion(i, 'marks', $event)"
min=
"1"
>
<!-- Question Text (editable) -->
<div
class=
"q-text-wrap"
>
<input
class=
"q-text-input"
[ngModel]=
"q.question"
(ngModelChange)=
"updateQuestion(i, 'question', $event)"
placeholder=
"Enter question text..."
>
</div>
<!-- Options Grid -->
<div
class=
"options-grid"
>
@for (opt of q.options; track $index; let j = $index) {
<div
class=
"option"
[class.correct-answer]=
"q.correctAnswer && opt && q.correctAnswer === opt && opt !== ''"
(click)=
"opt ? setCorrectAnswer(i, opt) : null"
>
<div
class=
"option-inner"
>
<span
class=
"option-marker"
>
@if (q.correctAnswer
&&
opt
&&
q.correctAnswer === opt
&&
opt !== '') {
✓
}
</span>
<input
class=
"option-input"
[ngModel]=
"opt"
(ngModelChange)=
"updateOption(i, j, $event)"
placeholder=
"Option {{ ['A','B','C','D'][j] }}"
(click)=
"$event.stopPropagation()"
>
</div>
</div>
}
</div>
<!-- Correct Answer Selector -->
<div
class=
"correct-answer-row"
>
<span
class=
"material-symbols-rounded correct-icon"
>
check_circle
</span>
<label
class=
"correct-label"
>
Correct Answer:
</label>
<select
class=
"correct-select"
[ngModel]=
"q.correctAnswer"
(ngModelChange)=
"setCorrectAnswer(i, $event)"
>
<option
value=
""
>
-- Select --
</option>
@for (opt of q.options; track $index; let j = $index) {
@if (opt) {
<option
[value]=
"opt"
>
{{ ['A','B','C','D'][j] }}. {{ opt }}
</option>
}
}
</select>
</div>
</div>
</div>
}
}
</div>
}
<button
class=
"btn btn-primary btn-lg save-btn"
(click)=
"onSave()"
[disabled]=
"saving()"
>
...
...
Frontend/src/app/pages/admin/edit-quiz/edit-quiz.ts
View file @
c2e9bafa
...
...
@@ -37,15 +37,35 @@ export class EditQuizComponent implements OnInit {
}
loadQuiz
():
void
{
const
overrides
=
history
.
state
?.
quizOverrides
;
this
.
quizService
.
getAdminQuiz
(
this
.
quizId
).
subscribe
({
next
:
(
res
)
=>
{
const
q
=
res
.
quiz
;
this
.
title
=
q
.
title
;
this
.
timer
=
q
.
timer
;
this
.
category
=
q
.
category
||
''
;
this
.
difficulty
=
q
.
difficulty
||
'
medium
'
;
this
.
questions
.
set
(
q
.
questions
||
[]);
this
.
locked
.
set
(
res
.
hasAttempts
||
false
);
this
.
title
=
overrides
?
overrides
.
title
:
q
.
title
;
this
.
timer
=
overrides
?
overrides
.
timer
:
q
.
timer
;
this
.
category
=
overrides
?
overrides
.
category
:
(
q
.
category
||
''
);
this
.
difficulty
=
overrides
?
overrides
.
difficulty
:
(
q
.
difficulty
||
'
medium
'
);
// Questions come from res.questions (separate from quiz object)
const
rawQuestions
=
res
.
questions
||
[];
const
mapped
=
rawQuestions
.
map
((
rq
:
any
)
=>
{
// correctAnswers stores indices as strings like ["0"]
// Map them back to the actual option text for editing
const
correctIndex
=
rq
.
correctAnswers
?.[
0
]
?
parseInt
(
rq
.
correctAnswers
[
0
])
:
-
1
;
const
correctAnswer
=
(
correctIndex
>=
0
&&
rq
.
options
[
correctIndex
])
?
rq
.
options
[
correctIndex
]
:
''
;
return
{
_id
:
rq
.
_id
,
question
:
rq
.
question
,
options
:
[...
rq
.
options
],
correctAnswer
:
correctAnswer
,
type
:
rq
.
type
||
'
single
'
};
});
this
.
questions
.
set
(
mapped
);
this
.
locked
.
set
((
res
.
attemptCount
||
0
)
>
0
);
this
.
loading
.
set
(
false
);
},
error
:
(
err
)
=>
{
...
...
@@ -54,52 +74,50 @@ export class EditQuizComponent implements OnInit {
}
});
}
onSave
():
void
{
if
(
!
this
.
title
.
trim
())
{
this
.
error
.
set
(
'
Title is required
'
);
return
;
}
this
.
saving
.
set
(
true
);
this
.
error
.
set
(
''
);
onSave
():
void
{
if
(
!
this
.
title
.
trim
())
{
this
.
error
.
set
(
'
Title is required
'
);
return
;
}
// 🔥 Format questions correctly
const
formattedQuestions
=
this
.
questions
().
map
(
q
=>
{
const
options
=
q
.
options
;
this
.
saving
.
set
(
true
);
this
.
error
.
set
(
''
);
const
correctIndex
=
options
.
findIndex
(
(
opt
:
string
)
=>
opt
==
q
.
correctAnswer
);
const
formattedQuestions
=
this
.
questions
().
map
(
q
=>
{
const
correctIndex
=
q
.
options
.
findIndex
(
(
opt
:
string
)
=>
opt
===
q
.
correctAnswer
);
return
{
question
:
q
.
question
,
options
,
correctAnswers
:
[
correctIndex
.
toString
()],
type
:
'
single
'
return
{
question
:
q
.
question
,
options
:
q
.
options
,
correctAnswers
:
[
correctIndex
.
toString
()],
type
:
'
single
'
};
});
const
data
=
{
title
:
this
.
title
,
timer
:
this
.
timer
,
category
:
this
.
category
,
difficulty
:
this
.
difficulty
,
questions
:
formattedQuestions
};
});
// ✅ closes map()
// 🔥 Final data
const
data
=
{
title
:
this
.
title
,
timer
:
this
.
timer
,
category
:
this
.
category
,
difficulty
:
this
.
difficulty
,
questions
:
formattedQuestions
};
// ✅ closes data object
this
.
quizService
.
updateQuiz
(
this
.
quizId
,
data
).
subscribe
({
next
:
()
=>
{
this
.
saving
.
set
(
false
);
this
.
success
.
set
(
'
Quiz updated successfully!
'
);
setTimeout
(()
=>
this
.
router
.
navigate
([
'
/admin/quizzes
'
]),
1200
);
},
error
:
(
err
)
=>
{
this
.
saving
.
set
(
false
);
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to update quiz
'
);
}
});
// ✅ closes subscribe
}
// ✅ closes function
this
.
quizService
.
updateQuiz
(
this
.
quizId
,
data
).
subscribe
({
next
:
()
=>
{
this
.
saving
.
set
(
false
);
this
.
success
.
set
(
'
Quiz updated successfully!
'
);
setTimeout
(()
=>
this
.
router
.
navigate
([
'
/admin/quizzes
'
]),
1200
);
},
error
:
(
err
)
=>
{
this
.
saving
.
set
(
false
);
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to update quiz
'
);
}
});
}
updateQuestion
(
index
:
number
,
field
:
string
,
value
:
any
):
void
{
const
q
=
[...
this
.
questions
()];
q
[
index
]
=
{
...
q
[
index
],
[
field
]:
value
};
...
...
@@ -108,9 +126,22 @@ onSave(): void {
updateOption
(
qIndex
:
number
,
optIndex
:
number
,
value
:
string
):
void
{
const
q
=
[...
this
.
questions
()];
const
oldOption
=
q
[
qIndex
].
options
[
optIndex
];
const
opts
=
[...(
q
[
qIndex
].
options
||
[])];
opts
[
optIndex
]
=
value
;
q
[
qIndex
]
=
{
...
q
[
qIndex
],
options
:
opts
};
// If the old option was the correct answer, update correctAnswer to the new value
const
updated
=
{
...
q
[
qIndex
],
options
:
opts
};
if
(
q
[
qIndex
].
correctAnswer
===
oldOption
)
{
updated
.
correctAnswer
=
value
;
}
q
[
qIndex
]
=
updated
;
this
.
questions
.
set
(
q
);
}
setCorrectAnswer
(
qIndex
:
number
,
optionText
:
string
):
void
{
const
q
=
[...
this
.
questions
()];
q
[
qIndex
]
=
{
...
q
[
qIndex
],
correctAnswer
:
optionText
};
this
.
questions
.
set
(
q
);
}
...
...
@@ -121,7 +152,11 @@ onSave(): void {
addQuestion
():
void
{
this
.
questions
.
set
([...
this
.
questions
(),
{
question
:
''
,
options
:
[
''
,
''
,
''
,
''
],
correctAnswer
:
''
,
marks
:
1
question
:
''
,
options
:
[
''
,
''
,
''
,
''
],
correctAnswer
:
''
,
type
:
'
single
'
}]);
setTimeout
(()
=>
{
const
cards
=
document
.
querySelectorAll
(
'
.question-card
'
);
cards
[
cards
.
length
-
1
]?.
scrollIntoView
({
behavior
:
'
smooth
'
,
block
:
'
center
'
});
},
50
);
}
}
Frontend/src/app/pages/admin/quizzes/quizzes.css
View file @
c2e9bafa
...
...
@@ -47,3 +47,49 @@
.page-container
{
padding
:
20px
16px
;
}
.quiz-grid
{
grid-template-columns
:
1
fr
;
}
}
/* Modal Overlay Styles */
.modal-overlay
{
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100vw
;
height
:
100vh
;
background
:
rgba
(
0
,
0
,
0
,
0.4
);
backdrop-filter
:
blur
(
4px
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
z-index
:
1000
;
animation
:
fadeIn
0.2s
ease-out
;
}
.modal-container
{
background
:
var
(
--bg-card
);
border-radius
:
var
(
--radius-lg
);
padding
:
32px
;
width
:
100%
;
max-width
:
600px
;
box-shadow
:
0
20px
25px
-5px
rgba
(
0
,
0
,
0
,
0.1
),
0
10px
10px
-5px
rgba
(
0
,
0
,
0
,
0.04
);
border
:
1px
solid
var
(
--border-color
);
animation
:
slideUp
0.3s
cubic-bezier
(
0.16
,
1
,
0.3
,
1
);
}
.modal-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
24px
;
}
.modal-header
h2
{
font-size
:
20px
;
font-weight
:
700
;
margin
:
0
;
color
:
var
(
--text-primary
);
}
@keyframes
slideUp
{
from
{
opacity
:
0
;
transform
:
translateY
(
20px
);
}
to
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
}
Frontend/src/app/pages/admin/quizzes/quizzes.html
View file @
c2e9bafa
...
...
@@ -61,9 +61,9 @@
{{ quiz.attemptCount }} attempt{{ quiz.attemptCount > 1 ? 's' : '' }}
</span>
} @else {
<
a
[routerLink]=
"['/admin/quiz', quiz._id, 'edit']"
class=
"btn btn-outline btn-sm
"
>
<
button
class=
"btn btn-outline btn-sm"
(click)=
"openEditPopup(quiz)
"
>
<span
class=
"material-symbols-rounded"
>
edit
</span>
Edit
</
a
>
</
button
>
<button
class=
"btn btn-danger btn-sm"
(click)=
"deleteQuiz(quiz._id)"
>
<span
class=
"material-symbols-rounded"
>
delete
</span>
Delete
</button>
...
...
@@ -74,3 +74,57 @@
</div>
}
</div>
<!-- Edit Quiz Modal -->
@if (showEditPopup()) {
<div
class=
"modal-overlay"
(click)=
"closeEditPopup()"
>
<div
class=
"modal-container"
(click)=
"$event.stopPropagation()"
>
<div
class=
"modal-header"
>
<h2>
Edit Quiz Details
</h2>
<button
class=
"icon-btn"
(click)=
"closeEditPopup()"
>
<span
class=
"material-symbols-rounded"
>
close
</span>
</button>
</div>
<div
class=
"modal-body quiz-form"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Quiz Title
</label>
<input
class=
"form-input"
[(ngModel)]=
"editQuizForm.title"
placeholder=
"Quiz title"
>
</div>
<div
class=
"form-row"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Timer (min)
</label>
<input
class=
"form-input"
type=
"number"
[(ngModel)]=
"editQuizForm.timer"
min=
"1"
>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Difficulty
</label>
<select
class=
"form-select"
[(ngModel)]=
"editQuizForm.difficulty"
>
<option
value=
"easy"
>
Easy
</option>
<option
value=
"medium"
>
Medium
</option>
<option
value=
"hard"
>
Hard
</option>
</select>
</div>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Category
</label>
<input
class=
"form-input"
[(ngModel)]=
"editQuizForm.category"
placeholder=
"e.g. Java, Angular"
>
</div>
</div>
<div
class=
"modal-footer"
style=
"display: flex; gap: 12px; margin-top: 24px; justify-content: flex-end;"
>
<button
class=
"btn btn-outline"
(click)=
"editQuestions()"
>
<span
class=
"material-symbols-rounded"
>
edit_square
</span>
Edit Questions
</button>
<button
class=
"btn btn-primary"
(click)=
"saveBasicChanges()"
[disabled]=
"savingPopup()"
>
@if (savingPopup()) {
<div
class=
"spinner"
></div>
Saving...
} @else {
<span
class=
"material-symbols-rounded"
>
save
</span>
Save Changes
}
</button>
</div>
</div>
</div>
}
Frontend/src/app/pages/admin/quizzes/quizzes.ts
View file @
c2e9bafa
import
{
Component
,
OnInit
,
signal
}
from
'
@angular/core
'
;
import
{
CommonModule
}
from
'
@angular/common
'
;
import
{
RouterLink
}
from
'
@angular/router
'
;
import
{
RouterLink
,
Router
}
from
'
@angular/router
'
;
import
{
FormsModule
}
from
'
@angular/forms
'
;
import
{
QuizService
}
from
'
../../../services/quiz.service
'
;
@
Component
({
selector
:
'
app-admin-quizzes
'
,
standalone
:
true
,
imports
:
[
CommonModule
,
RouterLink
],
imports
:
[
CommonModule
,
RouterLink
,
FormsModule
],
templateUrl
:
'
./quizzes.html
'
,
styleUrl
:
'
./quizzes.css
'
})
...
...
@@ -14,8 +15,18 @@ export class AdminQuizzesComponent implements OnInit {
quizzes
=
signal
<
any
[]
>
([]);
loading
=
signal
(
true
);
error
=
signal
(
''
);
showEditPopup
=
signal
(
false
);
editQuizForm
=
{
_id
:
''
,
title
:
''
,
timer
:
10
,
difficulty
:
'
medium
'
,
category
:
''
};
savingPopup
=
signal
(
false
);
constructor
(
private
quizService
:
QuizService
)
{}
constructor
(
private
quizService
:
QuizService
,
private
router
:
Router
)
{}
ngOnInit
():
void
{
this
.
loadQuizzes
();
...
...
@@ -45,4 +56,46 @@ export class AdminQuizzesComponent implements OnInit {
default
:
return
'
badge-primary
'
;
}
}
openEditPopup
(
quiz
:
any
):
void
{
this
.
editQuizForm
=
{
_id
:
quiz
.
_id
,
title
:
quiz
.
title
,
timer
:
quiz
.
timer
,
difficulty
:
quiz
.
difficulty
,
category
:
quiz
.
category
};
this
.
showEditPopup
.
set
(
true
);
}
closeEditPopup
():
void
{
this
.
showEditPopup
.
set
(
false
);
}
saveBasicChanges
():
void
{
this
.
savingPopup
.
set
(
true
);
// Don't send questions, only update basic details
this
.
quizService
.
updateQuiz
(
this
.
editQuizForm
.
_id
,
this
.
editQuizForm
).
subscribe
({
next
:
()
=>
{
this
.
savingPopup
.
set
(
false
);
this
.
closeEditPopup
();
this
.
loadQuizzes
();
},
error
:
(
err
)
=>
{
this
.
savingPopup
.
set
(
false
);
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to save changes
'
);
setTimeout
(()
=>
this
.
error
.
set
(
''
),
3000
);
}
});
}
editQuestions
():
void
{
// Navigate to edit-quiz page with the pending changes in history.state
this
.
router
.
navigate
([
'
/admin/quiz
'
,
this
.
editQuizForm
.
_id
,
'
edit
'
],
{
state
:
{
quizOverrides
:
{
...
this
.
editQuizForm
}
}
});
this
.
closeEditPopup
();
}
}
Frontend/src/app/pages/admin/user-history/user-history.css
View file @
c2e9bafa
This diff is collapsed.
Click to expand it.
Frontend/src/app/pages/admin/user-history/user-history.html
View file @
c2e9bafa
...
...
@@ -49,7 +49,9 @@
<table
class=
"history-table"
>
<thead>
<tr>
<th>
Quiz
</th>
<th>
Quiz Name
</th>
<th>
Topic
</th>
<th>
Candidate's comfort Level
</th>
<th>
Score
</th>
<th>
Percentage
</th>
<th>
Time Taken
</th>
...
...
@@ -61,6 +63,8 @@
@for (sub of submissions(); track sub._id) {
<tr>
<td
class=
"quiz-name"
>
{{ sub.quizId?.title || 'Deleted Quiz' }}
</td>
<td>
{{ sub.quizId?.category || 'N/A' }}
</td>
<td>
{{ getComfortLevel(sub.quizId?.category) }}
</td>
<td><span
class=
"score-badge"
>
{{ sub.score }}/{{ sub.totalMarks }}
</span></td>
<td>
<div
class=
"percent-bar"
>
...
...
@@ -83,6 +87,11 @@
</div>
}
}
<div
class=
"evaulation-summary"
>
<button>
Evaluate Summary
</button>
</div>
</div>
</div>
...
...
Frontend/src/app/pages/admin/user-history/user-history.ts
View file @
c2e9bafa
...
...
@@ -84,6 +84,14 @@ export class UserHistoryComponent implements OnInit {
return
`
${
m
}
m
${
s
}
s`
;
}
getComfortLevel
(
topic
:
string
):
string
{
const
u
=
this
.
user
();
if
(
!
u
||
!
u
.
topicsOfInterest
||
!
topic
)
return
'
N/A
'
;
const
interest
=
u
.
topicsOfInterest
.
find
((
t
:
any
)
=>
t
.
topic
.
toLowerCase
()
===
topic
.
toLowerCase
());
return
interest
?
`
${
interest
.
comfortLevel
}
%`
:
'
N/A
'
;
}
logout
():
void
{
this
.
authService
.
logout
();
}
...
...
Frontend/src/app/pages/hr/edit-quiz/edit-quiz.css
View file @
c2e9bafa
.page-container
{
padding
:
32px
40px
;
max-width
:
9
0
0px
;
}
.page-container
{
padding
:
32px
40px
;
max-width
:
9
6
0px
;
}
.page-header
{
margin-bottom
:
28px
;
}
.page-header
h1
{
font-size
:
26px
;
font-weight
:
700
;
color
:
var
(
--text-primary
);
margin
:
8px
0
0
;
}
.back-link
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
6px
;
font-size
:
13px
;
color
:
var
(
--text-secondary
);
font-weight
:
500
;
}
.back-link
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
6px
;
font-size
:
13px
;
color
:
var
(
--text-secondary
);
font-weight
:
500
;
text-decoration
:
none
;
}
.back-link
:hover
{
color
:
var
(
--accent-primary
);
}
.loading-center
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
padding
:
80px
0
;
gap
:
16px
;
}
.loading-center
p
{
color
:
var
(
--text-muted
);
}
.form-card
{
margin-bottom
:
24px
;
}
.quiz-form
{
display
:
flex
;
flex-direction
:
column
;
gap
:
20px
;
}
.form-group
{
display
:
flex
;
flex-direction
:
column
;
flex
:
1
;
}
.form-row
{
display
:
flex
;
gap
:
16px
;
}
/* Questions Header */
.questions-header
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
margin
:
0
0
20px
;
}
.questions-header
h2
{
font-size
:
18px
;
font-weight
:
600
;
color
:
var
(
--text-primary
);
margin
:
0
;
}
.questions-header
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
margin
:
24px
0
16px
;
}
.questions-
header
h2
{
font-size
:
18px
;
font-weight
:
600
;
color
:
var
(
--text-primary
)
;
}
/* Questions List */
.questions-
list
{
display
:
flex
;
flex-direction
:
column
;
gap
:
20px
;
}
.question-card
{
margin-bottom
:
16px
;
display
:
flex
;
flex-direction
:
column
;
gap
:
14px
;
}
.question-header
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
}
.q-number
{
font-size
:
13px
;
font-weight
:
700
;
color
:
var
(
--accent-primary
);
background
:
var
(
--accent-primary-light
);
padding
:
4px
12px
;
border-radius
:
var
(
--radius-full
);
}
/* Question Card */
.question-card
{
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
16px
;
padding
:
24px
;
border-left
:
4px
solid
var
(
--accent-primary
);
transition
:
box-shadow
0.2s
ease
,
border-color
0.2s
ease
;
}
.question-card
:hover
{
box-shadow
:
0
4px
16px
rgba
(
0
,
0
,
0
,
0.06
);
}
/* Question Header */
.q-header
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
margin-bottom
:
16px
;
}
.q-number
{
background
:
rgba
(
var
(
--accent-primary-rgb
),
0.15
);
color
:
var
(
--accent-primary
);
padding
:
4px
14px
;
border-radius
:
8px
;
font-weight
:
700
;
font-size
:
13px
;
flex-shrink
:
0
;
}
.q-type-badge
{
background
:
var
(
--bg-input
);
color
:
var
(
--text-secondary
);
padding
:
4px
10px
;
border-radius
:
8px
;
font-size
:
11px
;
font-weight
:
600
;
text-transform
:
uppercase
;
letter-spacing
:
0.3px
;
}
.delete-q-btn
{
margin-left
:
auto
;
background
:
transparent
;
border
:
1px
solid
var
(
--danger-border
);
color
:
var
(
--danger
);
width
:
34px
;
height
:
34px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
border-radius
:
8px
;
cursor
:
pointer
;
transition
:
all
0.2s
;
}
.delete-q-btn
.material-symbols-rounded
{
font-size
:
18px
;
}
.delete-q-btn
:hover
{
background
:
var
(
--danger
);
color
:
#fff
;
border-color
:
var
(
--danger
);
}
/* Editable Question Text */
.q-text-wrap
{
margin-bottom
:
16px
;
}
.q-text-input
{
width
:
100%
;
padding
:
12px
16px
;
background
:
var
(
--bg-input
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
10px
;
font-size
:
15px
;
line-height
:
1.5
;
color
:
var
(
--text-primary
);
font-family
:
inherit
;
transition
:
border-color
0.2s
;
}
.q-text-input
:focus
{
outline
:
none
;
border-color
:
var
(
--accent-primary
);
box-shadow
:
0
0
0
3px
rgba
(
var
(
--accent-primary-rgb
),
0.1
);
}
.q-text-input
::placeholder
{
color
:
var
(
--text-muted
);
}
/* Options Grid */
.options-grid
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
10px
;
margin-bottom
:
16px
;
}
.options-grid
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
10px
;
}
.option-input-wrap
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.option-letter
{
font-size
:
12px
;
font-weight
:
700
;
color
:
var
(
--text-muted
);
width
:
20px
;
text-align
:
center
;
flex-shrink
:
0
;
}
.option
{
padding
:
0
;
border-radius
:
10px
;
background
:
var
(
--bg-input
);
border
:
1px
solid
var
(
--border-color
);
transition
:
all
0.2s
;
cursor
:
pointer
;
position
:
relative
;
}
.option
:hover
{
border-color
:
var
(
--accent-primary
);
background
:
rgba
(
var
(
--accent-primary-rgb
),
0.04
);
}
.option.correct-answer
{
background
:
rgba
(
74
,
222
,
128
,
0.1
);
border-color
:
rgba
(
74
,
222
,
128
,
0.4
);
}
.option-inner
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
padding
:
2px
4px
2px
14px
;
}
.option-marker
{
font-weight
:
700
;
font-size
:
16px
;
color
:
var
(
--success
);
width
:
18px
;
flex-shrink
:
0
;
}
.option-input
{
flex
:
1
;
padding
:
10px
12px
;
background
:
transparent
;
border
:
none
;
font-size
:
14px
;
color
:
var
(
--text-primary
);
font-family
:
inherit
;
outline
:
none
;
min-width
:
0
;
}
.option-input
::placeholder
{
color
:
var
(
--text-muted
);
}
/* Correct Answer Row */
.correct-answer-row
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
12px
16px
;
background
:
rgba
(
74
,
222
,
128
,
0.06
);
border
:
1px
solid
rgba
(
74
,
222
,
128
,
0.2
);
border-radius
:
10px
;
}
.correct-icon
{
color
:
var
(
--success
);
font-size
:
20px
;
}
.correct-label
{
font-size
:
13px
;
font-weight
:
600
;
color
:
var
(
--text-secondary
);
white-space
:
nowrap
;
}
.correct-select
{
flex
:
1
;
padding
:
6px
12px
;
background
:
var
(
--bg-card
);
border
:
1px
solid
var
(
--border-color
);
border-radius
:
8px
;
font-size
:
13px
;
color
:
var
(
--text-primary
);
font-family
:
inherit
;
cursor
:
pointer
;
}
.correct-select
:focus
{
outline
:
none
;
border-color
:
var
(
--success
);
}
/* Save Button */
.save-btn
{
width
:
100%
;
margin-top
:
24px
;
}
@media
(
max-width
:
768px
)
{
.page-container
{
padding
:
20px
16px
;
}
.
form-row
,
.options-grid
{
flex-direction
:
column
;
grid-template-columns
:
1
fr
;
}
.
options-grid
{
grid-template-columns
:
1
fr
;
}
}
Frontend/src/app/pages/hr/edit-quiz/edit-quiz.html
View file @
c2e9bafa
<div
class=
"page-container animate-fade-in"
>
<div
class=
"page-header"
>
<a
routerLink=
"/
admin
/quizzes"
class=
"back-link"
>
<a
routerLink=
"/
hr
/quizzes"
class=
"back-link"
>
<span
class=
"material-symbols-rounded"
>
arrow_back
</span>
Back to Quizzes
</a>
<h1>
Edit Quiz
</h1>
...
...
@@ -19,32 +19,6 @@
@if (loading()) {
<div
class=
"loading-center"
><div
class=
"spinner spinner-lg"
></div><p>
Loading quiz...
</p></div>
} @else {
<div
class=
"card card-padding form-card"
>
<div
class=
"quiz-form"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Quiz Title
</label>
<input
class=
"form-input"
[(ngModel)]=
"title"
placeholder=
"Quiz title"
>
</div>
<div
class=
"form-row"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Timer (min)
</label>
<input
class=
"form-input"
type=
"number"
[(ngModel)]=
"timer"
min=
"1"
>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Difficulty
</label>
<select
class=
"form-select"
[(ngModel)]=
"difficulty"
>
<option
value=
"easy"
>
Easy
</option>
<option
value=
"medium"
>
Medium
</option>
<option
value=
"hard"
>
Hard
</option>
</select>
</div>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Category
</label>
<input
class=
"form-input"
[(ngModel)]=
"category"
placeholder=
"e.g. Java, Angular, Data Structures"
>
</div>
</div>
</div>
@if (!locked()) {
<div
class=
"questions-header"
>
...
...
@@ -54,37 +28,62 @@
</button>
</div>
@for (q of questions(); track $index; let i = $index) {
<div
class=
"card card-padding question-card"
>
<div
class=
"question-header"
>
<span
class=
"q-number"
>
Q{{ i + 1 }}
</span>
<button
class=
"btn btn-ghost btn-sm"
(click)=
"removeQuestion(i)"
>
<span
class=
"material-symbols-rounded"
>
delete
</span>
</button>
</div>
<div
class=
"form-group"
>
<input
class=
"form-input"
[ngModel]=
"q.question"
(ngModelChange)=
"updateQuestion(i, 'question', $event)"
placeholder=
"Question text"
>
</div>
<div
class=
"options-grid"
>
@for (opt of q.options; track $index; let j = $index) {
<div
class=
"option-input-wrap"
>
<span
class=
"option-letter"
>
{{ ['A','B','C','D'][j] }}
</span>
<input
class=
"form-input"
[ngModel]=
"opt"
(ngModelChange)=
"updateOption(i, j, $event)"
placeholder=
"Option {{ j+1 }}"
>
</div>
}
</div>
<div
class=
"form-row"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Correct Answer
</label>
<input
class=
"form-input"
[ngModel]=
"q.correctAnswer"
(ngModelChange)=
"updateQuestion(i, 'correctAnswer', $event)"
placeholder=
"Correct answer text"
>
<div
class=
"questions-list"
>
@for (q of questions(); track $index; let i = $index) {
<div
class=
"question-card"
>
<div
class=
"q-header"
>
<span
class=
"q-number"
>
Q{{ i + 1 }}
</span>
<span
class=
"q-type-badge"
>
{{ q.type === 'mcq' ? 'MCQ' : 'SINGLE' }}
</span>
<button
class=
"delete-q-btn"
(click)=
"removeQuestion(i)"
title=
"Delete question"
>
<span
class=
"material-symbols-rounded"
>
delete
</span>
</button>
</div>
<div
class=
"form-group"
style=
"max-width: 120px;"
>
<label
class=
"form-label"
>
Marks
</label>
<input
class=
"form-input"
type=
"number"
[ngModel]=
"q.marks"
(ngModelChange)=
"updateQuestion(i, 'marks', $event)"
min=
"1"
>
<div
class=
"q-text-wrap"
>
<input
class=
"q-text-input"
[ngModel]=
"q.question"
(ngModelChange)=
"updateQuestion(i, 'question', $event)"
placeholder=
"Enter question text..."
>
</div>
<div
class=
"options-grid"
>
@for (opt of q.options; track $index; let j = $index) {
<div
class=
"option"
[class.correct-answer]=
"q.correctAnswer && opt && q.correctAnswer === opt && opt !== ''"
(click)=
"opt ? setCorrectAnswer(i, opt) : null"
>
<div
class=
"option-inner"
>
<span
class=
"option-marker"
>
@if (q.correctAnswer
&&
opt
&&
q.correctAnswer === opt
&&
opt !== '') {
✓
}
</span>
<input
class=
"option-input"
[ngModel]=
"opt"
(ngModelChange)=
"updateOption(i, j, $event)"
placeholder=
"Option {{ ['A','B','C','D'][j] }}"
(click)=
"$event.stopPropagation()"
>
</div>
</div>
}
</div>
<div
class=
"correct-answer-row"
>
<span
class=
"material-symbols-rounded correct-icon"
>
check_circle
</span>
<label
class=
"correct-label"
>
Correct Answer:
</label>
<select
class=
"correct-select"
[ngModel]=
"q.correctAnswer"
(ngModelChange)=
"setCorrectAnswer(i, $event)"
>
<option
value=
""
>
-- Select --
</option>
@for (opt of q.options; track $index; let j = $index) {
@if (opt) {
<option
[value]=
"opt"
>
{{ ['A','B','C','D'][j] }}. {{ opt }}
</option>
}
}
</select>
</div>
</div>
</div>
}
}
</div>
}
<button
class=
"btn btn-primary btn-lg save-btn"
(click)=
"onSave()"
[disabled]=
"saving()"
>
...
...
Frontend/src/app/pages/hr/edit-quiz/edit-quiz.ts
View file @
c2e9bafa
...
...
@@ -10,7 +10,7 @@ import { QuizService } from '../../../services/quiz.service';
templateUrl
:
'
./edit-quiz.html
'
,
styleUrl
:
'
./edit-quiz.css
'
,
})
export
class
HREditQuizComponent
{
export
class
HREditQuizComponent
implements
OnInit
{
quizId
=
''
;
title
=
''
;
timer
=
30
;
...
...
@@ -36,15 +36,33 @@ export class HREditQuizComponent {
}
loadQuiz
():
void
{
const
overrides
=
history
.
state
?.
quizOverrides
;
this
.
quizService
.
getHRQuiz
(
this
.
quizId
).
subscribe
({
next
:
(
res
)
=>
{
const
q
=
res
.
quiz
;
this
.
title
=
q
.
title
;
this
.
timer
=
q
.
timer
;
this
.
category
=
q
.
category
||
''
;
this
.
difficulty
=
q
.
difficulty
||
'
medium
'
;
this
.
questions
.
set
(
q
.
questions
||
[]);
this
.
locked
.
set
(
res
.
hasAttempts
||
false
);
this
.
title
=
overrides
?
overrides
.
title
:
q
.
title
;
this
.
timer
=
overrides
?
overrides
.
timer
:
q
.
timer
;
this
.
category
=
overrides
?
overrides
.
category
:
(
q
.
category
||
''
);
this
.
difficulty
=
overrides
?
overrides
.
difficulty
:
(
q
.
difficulty
||
'
medium
'
);
// Questions come from res.questions (separate from quiz object)
const
rawQuestions
=
res
.
questions
||
[];
const
mapped
=
rawQuestions
.
map
((
rq
:
any
)
=>
{
const
correctIndex
=
rq
.
correctAnswers
?.[
0
]
?
parseInt
(
rq
.
correctAnswers
[
0
])
:
-
1
;
const
correctAnswer
=
(
correctIndex
>=
0
&&
rq
.
options
[
correctIndex
])
?
rq
.
options
[
correctIndex
]
:
''
;
return
{
_id
:
rq
.
_id
,
question
:
rq
.
question
,
options
:
[...
rq
.
options
],
correctAnswer
:
correctAnswer
,
type
:
rq
.
type
||
'
single
'
};
});
this
.
questions
.
set
(
mapped
);
this
.
locked
.
set
((
res
.
attemptCount
||
0
)
>
0
);
this
.
loading
.
set
(
false
);
},
error
:
(
err
)
=>
{
...
...
@@ -53,52 +71,50 @@ export class HREditQuizComponent {
}
});
}
onSave
():
void
{
if
(
!
this
.
title
.
trim
())
{
this
.
error
.
set
(
'
Title is required
'
);
return
;
}
this
.
saving
.
set
(
true
);
this
.
error
.
set
(
''
);
onSave
():
void
{
if
(
!
this
.
title
.
trim
())
{
this
.
error
.
set
(
'
Title is required
'
);
return
;
}
// 🔥 Format questions correctly
const
formattedQuestions
=
this
.
questions
().
map
(
q
=>
{
const
options
=
q
.
options
;
this
.
saving
.
set
(
true
);
this
.
error
.
set
(
''
);
const
correctIndex
=
options
.
findIndex
(
(
opt
:
string
)
=>
opt
==
q
.
correctAnswer
);
const
formattedQuestions
=
this
.
questions
().
map
(
q
=>
{
const
correctIndex
=
q
.
options
.
findIndex
(
(
opt
:
string
)
=>
opt
===
q
.
correctAnswer
);
return
{
question
:
q
.
question
,
options
,
correctAnswers
:
[
correctIndex
.
toString
()],
type
:
'
single
'
return
{
question
:
q
.
question
,
options
:
q
.
options
,
correctAnswers
:
[
correctIndex
.
toString
()],
type
:
'
single
'
};
});
const
data
=
{
title
:
this
.
title
,
timer
:
this
.
timer
,
category
:
this
.
category
,
difficulty
:
this
.
difficulty
,
questions
:
formattedQuestions
};
});
// ✅ closes map()
// 🔥 Final data
const
data
=
{
title
:
this
.
title
,
timer
:
this
.
timer
,
category
:
this
.
category
,
difficulty
:
this
.
difficulty
,
questions
:
formattedQuestions
};
// ✅ closes data object
this
.
quizService
.
updateHRQuiz
(
this
.
quizId
,
data
).
subscribe
({
next
:
()
=>
{
this
.
saving
.
set
(
false
);
this
.
success
.
set
(
'
Quiz updated successfully!
'
);
setTimeout
(()
=>
this
.
router
.
navigate
([
'
/hr/quizzes
'
]),
1200
);
},
error
:
(
err
)
=>
{
this
.
saving
.
set
(
false
);
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to update quiz
'
);
}
});
// ✅ closes subscribe
}
// ✅ closes function
this
.
quizService
.
updateHRQuiz
(
this
.
quizId
,
data
).
subscribe
({
next
:
()
=>
{
this
.
saving
.
set
(
false
);
this
.
success
.
set
(
'
Quiz updated successfully!
'
);
setTimeout
(()
=>
this
.
router
.
navigate
([
'
/hr/quizzes
'
]),
1200
);
},
error
:
(
err
)
=>
{
this
.
saving
.
set
(
false
);
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to update quiz
'
);
}
});
}
updateQuestion
(
index
:
number
,
field
:
string
,
value
:
any
):
void
{
const
q
=
[...
this
.
questions
()];
q
[
index
]
=
{
...
q
[
index
],
[
field
]:
value
};
...
...
@@ -107,9 +123,21 @@ onSave(): void {
updateOption
(
qIndex
:
number
,
optIndex
:
number
,
value
:
string
):
void
{
const
q
=
[...
this
.
questions
()];
const
oldOption
=
q
[
qIndex
].
options
[
optIndex
];
const
opts
=
[...(
q
[
qIndex
].
options
||
[])];
opts
[
optIndex
]
=
value
;
q
[
qIndex
]
=
{
...
q
[
qIndex
],
options
:
opts
};
const
updated
=
{
...
q
[
qIndex
],
options
:
opts
};
if
(
q
[
qIndex
].
correctAnswer
===
oldOption
)
{
updated
.
correctAnswer
=
value
;
}
q
[
qIndex
]
=
updated
;
this
.
questions
.
set
(
q
);
}
setCorrectAnswer
(
qIndex
:
number
,
optionText
:
string
):
void
{
const
q
=
[...
this
.
questions
()];
q
[
qIndex
]
=
{
...
q
[
qIndex
],
correctAnswer
:
optionText
};
this
.
questions
.
set
(
q
);
}
...
...
@@ -120,7 +148,11 @@ onSave(): void {
addQuestion
():
void
{
this
.
questions
.
set
([...
this
.
questions
(),
{
question
:
''
,
options
:
[
''
,
''
,
''
,
''
],
correctAnswer
:
''
,
marks
:
1
question
:
''
,
options
:
[
''
,
''
,
''
,
''
],
correctAnswer
:
''
,
type
:
'
single
'
}]);
setTimeout
(()
=>
{
const
cards
=
document
.
querySelectorAll
(
'
.question-card
'
);
cards
[
cards
.
length
-
1
]?.
scrollIntoView
({
behavior
:
'
smooth
'
,
block
:
'
center
'
});
},
50
);
}
}
Frontend/src/app/pages/hr/quizzes/quizzes.css
View file @
c2e9bafa
...
...
@@ -47,3 +47,49 @@
.page-container
{
padding
:
20px
16px
;
}
.quiz-grid
{
grid-template-columns
:
1
fr
;
}
}
/* Modal Overlay Styles */
.modal-overlay
{
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100vw
;
height
:
100vh
;
background
:
rgba
(
0
,
0
,
0
,
0.4
);
backdrop-filter
:
blur
(
4px
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
z-index
:
1000
;
animation
:
fadeIn
0.2s
ease-out
;
}
.modal-container
{
background
:
var
(
--bg-card
);
border-radius
:
var
(
--radius-lg
);
padding
:
32px
;
width
:
100%
;
max-width
:
600px
;
box-shadow
:
0
20px
25px
-5px
rgba
(
0
,
0
,
0
,
0.1
),
0
10px
10px
-5px
rgba
(
0
,
0
,
0
,
0.04
);
border
:
1px
solid
var
(
--border-color
);
animation
:
slideUp
0.3s
cubic-bezier
(
0.16
,
1
,
0.3
,
1
);
}
.modal-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
24px
;
}
.modal-header
h2
{
font-size
:
20px
;
font-weight
:
700
;
margin
:
0
;
color
:
var
(
--text-primary
);
}
@keyframes
slideUp
{
from
{
opacity
:
0
;
transform
:
translateY
(
20px
);
}
to
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
}
Frontend/src/app/pages/hr/quizzes/quizzes.html
View file @
c2e9bafa
...
...
@@ -52,7 +52,7 @@
</div>
</div>
<div
class=
"quiz-card-actions"
>
<a
[routerLink]=
"['/
admin
/quiz', quiz._id, 'assign']"
class=
"btn btn-primary btn-sm"
>
<a
[routerLink]=
"['/
hr
/quiz', quiz._id, 'assign']"
class=
"btn btn-primary btn-sm"
>
<span
class=
"material-symbols-rounded"
>
person_add
</span>
Assign
</a>
@if (quiz.attemptCount > 0) {
...
...
@@ -61,9 +61,9 @@
{{ quiz.attemptCount }} attempt{{ quiz.attemptCount > 1 ? 's' : '' }}
</span>
} @else {
<
a
[routerLink]=
"['/admin/quiz', quiz._id, 'edit']"
class=
"btn btn-outline btn-sm
"
>
<
button
class=
"btn btn-outline btn-sm"
(click)=
"openEditPopup(quiz)
"
>
<span
class=
"material-symbols-rounded"
>
edit
</span>
Edit
</
a
>
</
button
>
<button
class=
"btn btn-danger btn-sm"
(click)=
"deleteQuiz(quiz._id)"
>
<span
class=
"material-symbols-rounded"
>
delete
</span>
Delete
</button>
...
...
@@ -74,3 +74,57 @@
</div>
}
</div>
<!-- Edit Quiz Modal -->
@if (showEditPopup()) {
<div
class=
"modal-overlay"
(click)=
"closeEditPopup()"
>
<div
class=
"modal-container"
(click)=
"$event.stopPropagation()"
>
<div
class=
"modal-header"
>
<h2>
Edit Quiz Details
</h2>
<button
class=
"icon-btn"
(click)=
"closeEditPopup()"
>
<span
class=
"material-symbols-rounded"
>
close
</span>
</button>
</div>
<div
class=
"modal-body quiz-form"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Quiz Title
</label>
<input
class=
"form-input"
[(ngModel)]=
"editQuizForm.title"
placeholder=
"Quiz title"
>
</div>
<div
class=
"form-row"
>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Timer (min)
</label>
<input
class=
"form-input"
type=
"number"
[(ngModel)]=
"editQuizForm.timer"
min=
"1"
>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Difficulty
</label>
<select
class=
"form-select"
[(ngModel)]=
"editQuizForm.difficulty"
>
<option
value=
"easy"
>
Easy
</option>
<option
value=
"medium"
>
Medium
</option>
<option
value=
"hard"
>
Hard
</option>
</select>
</div>
</div>
<div
class=
"form-group"
>
<label
class=
"form-label"
>
Category
</label>
<input
class=
"form-input"
[(ngModel)]=
"editQuizForm.category"
placeholder=
"e.g. Java, Angular"
>
</div>
</div>
<div
class=
"modal-footer"
style=
"display: flex; gap: 12px; margin-top: 24px; justify-content: flex-end;"
>
<button
class=
"btn btn-outline"
(click)=
"editQuestions()"
>
<span
class=
"material-symbols-rounded"
>
edit_square
</span>
Edit Questions
</button>
<button
class=
"btn btn-primary"
(click)=
"saveBasicChanges()"
[disabled]=
"savingPopup()"
>
@if (savingPopup()) {
<div
class=
"spinner"
></div>
Saving...
} @else {
<span
class=
"material-symbols-rounded"
>
save
</span>
Save Changes
}
</button>
</div>
</div>
</div>
}
Frontend/src/app/pages/hr/quizzes/quizzes.ts
View file @
c2e9bafa
import
{
Component
,
OnInit
,
signal
}
from
'
@angular/core
'
;
import
{
CommonModule
}
from
'
@angular/common
'
;
import
{
RouterLink
}
from
'
@angular/router
'
;
import
{
RouterLink
,
Router
}
from
'
@angular/router
'
;
import
{
FormsModule
}
from
'
@angular/forms
'
;
import
{
QuizService
}
from
'
../../../services/quiz.service
'
;
@
Component
({
selector
:
'
app-hr-quizzes
'
,
standalone
:
true
,
imports
:
[
CommonModule
,
RouterLink
],
imports
:
[
CommonModule
,
RouterLink
,
FormsModule
],
templateUrl
:
'
./quizzes.html
'
,
styleUrl
:
'
./quizzes.css
'
})
...
...
@@ -15,7 +16,17 @@ quizzes = signal<any[]>([]);
loading
=
signal
(
true
);
error
=
signal
(
''
);
constructor
(
private
quizService
:
QuizService
)
{}
showEditPopup
=
signal
(
false
);
editQuizForm
=
{
_id
:
''
,
title
:
''
,
timer
:
10
,
difficulty
:
'
medium
'
,
category
:
''
};
savingPopup
=
signal
(
false
);
constructor
(
private
quizService
:
QuizService
,
private
router
:
Router
)
{}
ngOnInit
():
void
{
this
.
loadQuizzes
();
...
...
@@ -45,4 +56,46 @@ quizzes = signal<any[]>([]);
default
:
return
'
badge-primary
'
;
}
}
openEditPopup
(
quiz
:
any
):
void
{
this
.
editQuizForm
=
{
_id
:
quiz
.
_id
,
title
:
quiz
.
title
,
timer
:
quiz
.
timer
,
difficulty
:
quiz
.
difficulty
,
category
:
quiz
.
category
};
this
.
showEditPopup
.
set
(
true
);
}
closeEditPopup
():
void
{
this
.
showEditPopup
.
set
(
false
);
}
saveBasicChanges
():
void
{
this
.
savingPopup
.
set
(
true
);
// Don't send questions, only update basic details
this
.
quizService
.
updateHRQuiz
(
this
.
editQuizForm
.
_id
,
this
.
editQuizForm
).
subscribe
({
next
:
()
=>
{
this
.
savingPopup
.
set
(
false
);
this
.
closeEditPopup
();
this
.
loadQuizzes
();
},
error
:
(
err
)
=>
{
this
.
savingPopup
.
set
(
false
);
this
.
error
.
set
(
err
.
error
?.
message
||
'
Failed to save changes
'
);
setTimeout
(()
=>
this
.
error
.
set
(
''
),
3000
);
}
});
}
editQuestions
():
void
{
// Navigate to edit-quiz page with the pending changes in history.state
this
.
router
.
navigate
([
'
/hr/quiz
'
,
this
.
editQuizForm
.
_id
,
'
edit
'
],
{
state
:
{
quizOverrides
:
{
...
this
.
editQuizForm
}
}
});
this
.
closeEditPopup
();
}
}
Frontend/src/app/pages/hr/user-history/user-history.ts
View file @
c2e9bafa
...
...
@@ -83,6 +83,14 @@ export class HRUserHistoryComponent {
return
`
${
m
}
m
${
s
}
s`
;
}
getComfortLevel
(
topic
:
string
):
string
{
const
u
=
this
.
user
();
if
(
!
u
||
!
u
.
topicsOfInterest
||
!
topic
)
return
'
N/A
'
;
const
interest
=
u
.
topicsOfInterest
.
find
((
t
:
any
)
=>
t
.
topic
.
toLowerCase
()
===
topic
.
toLowerCase
());
return
interest
?
`
${
interest
.
comfortLevel
}
%`
:
'
N/A
'
;
}
logout
():
void
{
this
.
authService
.
logout
();
}
...
...
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