Commit 68e5aeae authored by Aravind RK's avatar Aravind RK

Refactor: minor ui bug fixes and changes

parent 99f0042f
...@@ -25,6 +25,7 @@ const interviewSchema = new mongoose.Schema({ ...@@ -25,6 +25,7 @@ const interviewSchema = new mongoose.Schema({
techStack: { type: String, default: '', trim: true }, techStack: { type: String, default: '', trim: true },
source: { type: String, default: '', trim: true }, source: { type: String, default: '', trim: true },
dateOfInterview: { type: Date, default: Date.now }, dateOfInterview: { type: Date, default: Date.now },
timeOfInterview: { type: String, default: '' },
// Quiz assignments (aptitude + technical) // Quiz assignments (aptitude + technical)
quizzes: [{ quizzes: [{
......
const express = require('express'); 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 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');
const { protect, authorize } = require('../middleware/auth'); const Interview = require('../models/Interview');
const upload = require('../middleware/upload'); const { protect, authorize } = require('../middleware/auth');
const router = express.Router(); const upload = require('../middleware/upload');
const router = express.Router();
// All admin routes require authentication + admin role // All admin routes require authentication + admin role
router.use(protect, authorize('admin')); router.use(protect, authorize('admin'));
...@@ -852,6 +853,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt ...@@ -852,6 +853,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await Group.findOneAndUpdate({ name: oldName }, { name: newName.trim() }); await Group.findOneAndUpdate({ name: oldName }, { name: newName.trim() });
await User.updateMany({ group: oldName }, { group: newName.trim() }); await User.updateMany({ group: oldName }, { group: newName.trim() });
await Quiz.updateMany({ assignedGroups: oldName }, { $set: { "assignedGroups.$": newName.trim() } }); await Quiz.updateMany({ assignedGroups: oldName }, { $set: { "assignedGroups.$": newName.trim() } });
await Interview.updateMany({ groupId: oldName }, { groupId: newName.trim() });
res.json({ message: 'Group updated successfully' }); res.json({ message: 'Group updated successfully' });
} catch (error) { } catch (error) {
...@@ -869,6 +871,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt ...@@ -869,6 +871,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await Group.deleteOne({ name }); await Group.deleteOne({ name });
await User.updateMany({ group: name }, { group: 'General' }); await User.updateMany({ group: name }, { group: 'General' });
await Quiz.updateMany({ assignedGroups: name }, { $pull: { assignedGroups: name } }); await Quiz.updateMany({ assignedGroups: name }, { $pull: { assignedGroups: name } });
await Interview.updateMany({ groupId: name }, { groupId: 'General' });
res.json({ message: 'Group deleted successfully' }); res.json({ message: 'Group deleted successfully' });
} catch (error) { } catch (error) {
......
...@@ -23,8 +23,29 @@ router.get('/interviews', async (req, res) => { ...@@ -23,8 +23,29 @@ router.get('/interviews', async (req, res) => {
.populate('createdBy', 'name') .populate('createdBy', 'name')
.populate('quizzes.quizId', 'title timer totalQuestions category difficulty') .populate('quizzes.quizId', 'title timer totalQuestions category difficulty')
.sort({ createdAt: -1 }); .sort({ createdAt: -1 });
const now = new Date();
res.json({ interviews }); // Filter out interviews scheduled in the future
const activeInterviews = interviews.filter(inv => {
if (!inv.dateOfInterview) return true; // If no date, show it
const invDate = new Date(inv.dateOfInterview);
// If time is provided, parse and set it on the date object
if (inv.timeOfInterview) {
const [hours, minutes] = inv.timeOfInterview.split(':').map(Number);
if (!isNaN(hours) && !isNaN(minutes)) {
invDate.setHours(hours, minutes, 0, 0);
}
} else {
// If no time is provided, we can assume it's available at the start of the day
invDate.setHours(0, 0, 0, 0);
}
return now >= invDate;
});
res.json({ interviews: activeInterviews });
} catch (error) { } catch (error) {
res.status(500).json({ message: 'Server error', error: error.message }); res.status(500).json({ message: 'Server error', error: error.message });
} }
...@@ -198,7 +219,7 @@ router.post('/quiz/:quizId/submit', async (req, res) => { ...@@ -198,7 +219,7 @@ router.post('/quiz/:quizId/submit', async (req, res) => {
// Update Interview if applicable // Update Interview if applicable
const Interview = require('../models/Interview'); const Interview = require('../models/Interview');
await Interview.findOneAndUpdate( const updatedInterview = await Interview.findOneAndUpdate(
{ candidateId: req.user._id, 'quizzes.quizId': quizId }, { candidateId: req.user._id, 'quizzes.quizId': quizId },
{ {
$set: { $set: {
...@@ -208,9 +229,18 @@ router.post('/quiz/:quizId/submit', async (req, res) => { ...@@ -208,9 +229,18 @@ router.post('/quiz/:quizId/submit', async (req, res) => {
'quizzes.$.completed': true, 'quizzes.$.completed': true,
'quizzes.$.violation': !!violation 'quizzes.$.violation': !!violation
} }
} },
{ new: true }
); );
if (updatedInterview) {
const allCompleted = updatedInterview.quizzes.every(q => q.completed);
if (allCompleted && (updatedInterview.status === 'quiz_phase' || updatedInterview.status === 'pending')) {
updatedInterview.status = 'coding_phase';
await updatedInterview.save();
}
}
res.status(201).json({ res.status(201).json({
message: 'Quiz submitted successfully', message: 'Quiz submitted successfully',
result: { result: {
...@@ -426,7 +456,8 @@ router.delete('/profile/resume', async (req, res) => { ...@@ -426,7 +456,8 @@ router.delete('/profile/resume', async (req, res) => {
await user.save(); await user.save();
} }
res.json({ message: 'Resume deleted successfully', user: { res.json({
message: 'Resume deleted successfully', user: {
_id: user._id, _id: user._id,
name: user.name, name: user.name,
email: user.email, email: user.email,
...@@ -434,7 +465,8 @@ router.delete('/profile/resume', async (req, res) => { ...@@ -434,7 +465,8 @@ router.delete('/profile/resume', async (req, res) => {
resume: user.resume, resume: user.resume,
level: user.level, level: user.level,
topicsOfInterest: user.topicsOfInterest topicsOfInterest: user.topicsOfInterest
} }); }
});
} catch (error) { } catch (error) {
res.status(500).json({ message: 'Server error', error: error.message }); res.status(500).json({ message: 'Server error', error: error.message });
} }
......
const express = require('express'); 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');
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
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');
const Interview = require('../models/Interview');
const { protect, authorize } = require('../middleware/auth'); const { protect, authorize } = require('../middleware/auth');
const upload = require('../middleware/upload'); const upload = require('../middleware/upload');
const router = express.Router(); const router = express.Router();
...@@ -816,6 +817,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt ...@@ -816,6 +817,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await Group.findOneAndUpdate({ name: oldName }, { name: newName.trim() }); await Group.findOneAndUpdate({ name: oldName }, { name: newName.trim() });
await User.updateMany({ group: oldName }, { group: newName.trim() }); await User.updateMany({ group: oldName }, { group: newName.trim() });
await Quiz.updateMany({ assignedGroups: oldName }, { $set: { "assignedGroups.$": newName.trim() } }); await Quiz.updateMany({ assignedGroups: oldName }, { $set: { "assignedGroups.$": newName.trim() } });
await Interview.updateMany({ groupId: oldName }, { groupId: newName.trim() });
res.json({ message: 'Group updated successfully' }); res.json({ message: 'Group updated successfully' });
} catch (error) { } catch (error) {
...@@ -833,6 +835,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt ...@@ -833,6 +835,7 @@ IMPORTANT: The "correct" value must be the EXACT TEXT of the option, not the opt
await Group.deleteOne({ name }); await Group.deleteOne({ name });
await User.updateMany({ group: name }, { group: 'General' }); await User.updateMany({ group: name }, { group: 'General' });
await Quiz.updateMany({ assignedGroups: name }, { $pull: { assignedGroups: name } }); await Quiz.updateMany({ assignedGroups: name }, { $pull: { assignedGroups: name } });
await Interview.updateMany({ groupId: name }, { groupId: 'General' });
res.json({ message: 'Group deleted successfully' }); res.json({ message: 'Group deleted successfully' });
} catch (error) { } catch (error) {
......
...@@ -19,7 +19,7 @@ router.use(protect); ...@@ -19,7 +19,7 @@ router.use(protect);
// ============================================================ // ============================================================
router.post('/', authorize('admin', 'hr', 'pm'), async (req, res) => { router.post('/', authorize('admin', 'hr', 'pm'), async (req, res) => {
try { try {
const { candidateId, interviewerId, assignedInterviewers, assignedHRs, assignedPMs, position, techStack, source, dateOfInterview, quizIds } = req.body; const { candidateId, interviewerId, assignedInterviewers, assignedHRs, assignedPMs, position, techStack, source, dateOfInterview, timeOfInterview, quizIds } = req.body;
// Use interviewerId as fallback, or assignedInterviewers[0] as interviewerId for backward compatibility // Use interviewerId as fallback, or assignedInterviewers[0] as interviewerId for backward compatibility
const mainInterviewerId = interviewerId || (assignedInterviewers && assignedInterviewers.length > 0 ? assignedInterviewers[0] : null); const mainInterviewerId = interviewerId || (assignedInterviewers && assignedInterviewers.length > 0 ? assignedInterviewers[0] : null);
...@@ -56,6 +56,7 @@ router.post('/', authorize('admin', 'hr', 'pm'), async (req, res) => { ...@@ -56,6 +56,7 @@ router.post('/', authorize('admin', 'hr', 'pm'), async (req, res) => {
techStack: techStack || '', techStack: techStack || '',
source: source || '', source: source || '',
dateOfInterview: dateOfInterview || new Date(), dateOfInterview: dateOfInterview || new Date(),
timeOfInterview: timeOfInterview || '',
quizzes, quizzes,
status: quizzes.length > 0 ? 'quiz_phase' : 'pending', status: quizzes.length > 0 ? 'quiz_phase' : 'pending',
type: 'individual', type: 'individual',
...@@ -222,7 +223,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => { ...@@ -222,7 +223,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => {
try { try {
const { const {
groupName, assignedInterviewers, assignedHRs, assignedPMs, groupName, assignedInterviewers, assignedHRs, assignedPMs,
position, techStack, source, dateOfInterview, quizSets position, techStack, source, dateOfInterview, timeOfInterview, quizSets
} = req.body; } = req.body;
if (!groupName || !position) { if (!groupName || !position) {
...@@ -286,6 +287,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => { ...@@ -286,6 +287,7 @@ router.post('/group', authorize('admin', 'hr', 'pm'), async (req, res) => {
techStack: techStack || '', techStack: techStack || '',
source: source || '', source: source || '',
dateOfInterview: dateOfInterview || new Date(), dateOfInterview: dateOfInterview || new Date(),
timeOfInterview: timeOfInterview || '',
quizzes, quizzes,
status: quizzes.length > 0 ? 'quiz_phase' : 'pending', status: quizzes.length > 0 ? 'quiz_phase' : 'pending',
type: 'group', type: 'group',
......
...@@ -438,6 +438,27 @@ ...@@ -438,6 +438,27 @@
margin: -8px -12px -12px -8px; /* Compensate padding to maintain perfect visual alignment */ margin: -8px -12px -12px -8px; /* Compensate padding to maintain perfect visual alignment */
} }
.scrollable-options {
max-height: 242px;
}
.modal-options::-webkit-scrollbar {
width: 6px;
}
.modal-options::-webkit-scrollbar-track {
background: transparent;
}
.modal-options::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.modal-options::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
.glassy-option { .glassy-option {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -518,3 +539,83 @@ ...@@ -518,3 +539,83 @@
0% { opacity: 0; transform: scale(0.95) translateY(10px); } 0% { opacity: 0; transform: scale(0.95) translateY(10px); }
100% { opacity: 1; transform: scale(1) translateY(0); } 100% { opacity: 1; transform: scale(1) translateY(0); }
} }
/* Confirm Modal Specifics */
.confirm-modal {
max-width: 400px;
text-align: center;
align-items: center;
}
.confirm-modal .modal-header {
width: 100%;
margin-bottom: 16px;
}
.confirm-modal-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.confirm-icon-wrapper {
width: 64px;
height: 64px;
border-radius: 50%;
background: var(--danger-light);
color: var(--danger);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.confirm-icon-wrapper .material-symbols-rounded {
font-size: 32px;
}
.confirm-text {
font-size: 15px;
color: var(--text-secondary);
line-height: 1.5;
margin: 0;
}
.confirm-modal-actions {
display: flex;
gap: 12px;
width: 100%;
}
.confirm-modal-actions .btn {
flex: 1;
padding: 12px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s ease;
}
.confirm-modal-actions .btn-secondary {
background: var(--bg-hover);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.confirm-modal-actions .btn-secondary:hover {
background: var(--bg-tertiary);
}
.confirm-modal-actions .btn-danger {
background: var(--danger);
color: #fff;
}
.confirm-modal-actions .btn-danger:hover {
background: var(--danger-border);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
}
...@@ -103,7 +103,7 @@ ...@@ -103,7 +103,7 @@
</button> </button>
</div> </div>
<div class="modal-options"> <div class="modal-options" [class.scrollable-options]="authService.getUserRole() === 'admin'">
<a [routerLink]="getUsersRoute()" class="glassy-option" (click)="closeManageUsersPopup()"> <a [routerLink]="getUsersRoute()" class="glassy-option" (click)="closeManageUsersPopup()">
<div class="option-icon-wrapper"> <div class="option-icon-wrapper">
<span class="material-symbols-rounded block-icon">people</span> <span class="material-symbols-rounded block-icon">people</span>
...@@ -219,3 +219,29 @@ ...@@ -219,3 +219,29 @@
</div> </div>
</div> </div>
} }
<!-- Logout Confirmation Modal -->
@if (showLogoutConfirm) {
<div class="glassy-overlay" (click)="showLogoutConfirm = false">
<div class="glassy-modal confirm-modal animate-fade-in" (click)="$event.stopPropagation()">
<div class="modal-header">
<h2>Confirm Logout</h2>
<button class="close-btn" (click)="showLogoutConfirm = false">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="confirm-modal-body">
<div class="confirm-icon-wrapper">
<span class="material-symbols-rounded">logout</span>
</div>
<p class="confirm-text">Are you sure you want to sign out of QuizMaster?</p>
</div>
<div class="confirm-modal-actions">
<button class="btn btn-secondary" (click)="showLogoutConfirm = false">Cancel</button>
<button class="btn btn-danger" (click)="confirmLogout()">Sign Out</button>
</div>
</div>
</div>
}
\ No newline at end of file
...@@ -26,6 +26,7 @@ export class LayoutComponent { ...@@ -26,6 +26,7 @@ export class LayoutComponent {
showThemeMenu = false; showThemeMenu = false;
mobileSidebarOpen = false; mobileSidebarOpen = false;
showLogoutConfirm = false;
themes: { id: ThemeMode; label: string; icon: string }[] = [ themes: { id: ThemeMode; label: string; icon: string }[] = [
{ id: 'light', label: 'Light', icon: 'light_mode' }, { id: 'light', label: 'Light', icon: 'light_mode' },
...@@ -107,6 +108,11 @@ export class LayoutComponent { ...@@ -107,6 +108,11 @@ export class LayoutComponent {
} }
logout(): void { logout(): void {
this.showLogoutConfirm = true;
}
confirmLogout(): void {
this.showLogoutConfirm = false;
this.authService.logout(); this.authService.logout();
} }
......
...@@ -110,24 +110,63 @@ ...@@ -110,24 +110,63 @@
/* Quick Actions */ /* Quick Actions */
.actions-grid { .actions-grid {
display: grid; display: grid;
grid-template-columns: repeat(4,1fr); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px; gap: 16px;
} }
.action-card { .action-card {
display: flex; display: flex;
align-items: flex-start; align-items: center;
gap: 16px; gap: 16px;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
height: 100%; height: 100%;
padding: 18px !important;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.action-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, rgba(102,126,234,0.05) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.3s;
}
.action-card:hover::before {
opacity: 1;
}
.action-card:hover {
border-color: var(--accent-primary);
box-shadow: 0 12px 30px rgba(102,126,234,0.12);
transform: translateY(-4px);
} }
.action-icon { .action-icon {
font-size: 28px; width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(102,126,234,0.1);
color: var(--accent-primary); color: var(--accent-primary);
border-radius: 12px;
font-size: 24px !important;
flex-shrink: 0; flex-shrink: 0;
margin-top: 2px; transition: all 0.3s;
}
.action-card:hover .action-icon {
background: var(--accent-primary);
color: #fff;
transform: scale(1.1) rotate(-5deg);
} }
.action-info { .action-info {
......
...@@ -257,8 +257,11 @@ ...@@ -257,8 +257,11 @@
<input class="form-input" [(ngModel)]="newGroupInterview.source" placeholder="e.g., LinkedIn, Campus"> <input class="form-input" [(ngModel)]="newGroupInterview.source" placeholder="e.g., LinkedIn, Campus">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Date of Interview</label> <label class="form-label">Date & Time</label>
<input class="form-input" type="date" [(ngModel)]="newGroupInterview.dateOfInterview"> <div style="display: flex; gap: 8px;">
<input class="form-input" type="date" [(ngModel)]="newGroupInterview.dateOfInterview" style="flex: 1;">
<input class="form-input" type="time" [(ngModel)]="newGroupInterview.timeOfInterview" style="flex: 1;">
</div>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
......
...@@ -145,7 +145,8 @@ export class GroupInterviewComponent implements OnInit { ...@@ -145,7 +145,8 @@ export class GroupInterviewComponent implements OnInit {
position: '', position: '',
techStack: '', techStack: '',
source: '', source: '',
dateOfInterview: new Date().toISOString().split('T')[0] dateOfInterview: new Date().toISOString().split('T')[0],
timeOfInterview: ''
}; };
} }
......
...@@ -23,8 +23,8 @@ ...@@ -23,8 +23,8 @@
.btn-primary:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102,126,234,0.4); } .btn-primary: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; } .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; }
.groups-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; } .groups-grid { column-count: 2; column-gap: 16px; }
.group-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 16px; padding: 20px; display: flex; align-items: stretch; gap: 16px; transition: all 0.3s; } .group-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 16px; padding: 20px; display: inline-flex; width: 100%; align-items: center; gap: 16px; transition: all 0.3s; break-inside: avoid; margin-bottom: 16px; box-sizing: border-box; }
.group-card:hover { border-color: var(--accent-primary); transform: translateY(-3px); box-shadow: var(--shadow-md); } .group-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 { 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-icon-wrapper .material-symbols-rounded { font-size: 26px; }
...@@ -49,8 +49,10 @@ ...@@ -49,8 +49,10 @@
.empty-state h3 { color: var(--text-primary); margin: 0 0 8px; font-size: 18px; } .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; } .empty-state p { color: var(--text-muted); margin: 0; font-size: 14px; }
@media (max-width: 768px) {
.groups-grid { column-count: 1; }
}
@media (max-width: 600px) { @media (max-width: 600px) {
.page-container { padding: 20px 16px; } .page-container { padding: 20px 16px; }
.row-align { flex-direction: column; align-items: stretch; } .row-align { flex-direction: column; align-items: stretch; }
.groups-grid { grid-template-columns: 1fr; }
} }
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
<p style="margin: 4px 0 0; font-size: 13px; color: var(--text-muted);">{{ user.email }}</p> <p style="margin: 4px 0 0; font-size: 13px; color: var(--text-muted);">{{ user.email }}</p>
} }
</div> </div>
<div class="group-actions" style="display: flex; gap: 8px;"> <div class="group-actions" style="display: flex; gap: 8px; align-items: center;">
@if (editingId() === user._id) { @if (editingId() === user._id) {
<button class="action-btn text-success" (click)="saveEdit(user._id)" title="Save"><span class="material-symbols-rounded">check</span></button> <button class="action-btn text-success" (click)="saveEdit(user._id)" title="Save"><span class="material-symbols-rounded">check</span></button>
<button class="action-btn text-danger" (click)="cancelEdit()" title="Cancel"><span class="material-symbols-rounded">close</span></button> <button class="action-btn text-danger" (click)="cancelEdit()" title="Cancel"><span class="material-symbols-rounded">close</span></button>
......
...@@ -184,8 +184,11 @@ ...@@ -184,8 +184,11 @@
<input class="form-input" [(ngModel)]="newInterview.source" placeholder="e.g., LinkedIn, Campus"> <input class="form-input" [(ngModel)]="newInterview.source" placeholder="e.g., LinkedIn, Campus">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Date of Interview</label> <label class="form-label">Date & Time</label>
<input class="form-input" type="date" [(ngModel)]="newInterview.dateOfInterview"> <div style="display: flex; gap: 8px;">
<input class="form-input" type="date" [(ngModel)]="newInterview.dateOfInterview" style="flex: 1;">
<input class="form-input" type="time" [(ngModel)]="newInterview.timeOfInterview" style="flex: 1;">
</div>
</div> </div>
</div> </div>
......
...@@ -34,6 +34,7 @@ export class IndividualInterviewComponent implements OnInit { ...@@ -34,6 +34,7 @@ export class IndividualInterviewComponent implements OnInit {
techStack: '', techStack: '',
source: '', source: '',
dateOfInterview: new Date().toISOString().split('T')[0], dateOfInterview: new Date().toISOString().split('T')[0],
timeOfInterview: '',
quizIds: [] as string[] quizIds: [] as string[]
}; };
...@@ -95,7 +96,7 @@ export class IndividualInterviewComponent implements OnInit { ...@@ -95,7 +96,7 @@ export class IndividualInterviewComponent implements OnInit {
}); });
this.newInterview = { this.newInterview = {
candidateId: '', assignedInterviewers: [], assignedHRs: [], assignedPMs: [], position: '', techStack: '', candidateId: '', assignedInterviewers: [], assignedHRs: [], assignedPMs: [], position: '', techStack: '',
source: '', dateOfInterview: new Date().toISOString().split('T')[0], quizIds: [] source: '', dateOfInterview: new Date().toISOString().split('T')[0], timeOfInterview: '', quizIds: []
}; };
this.showCreateModal.set(true); this.showCreateModal.set(true);
} }
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
.group-lane:hover { border-color: rgba(102,126,234,0.4); transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.1); } .group-lane:hover { border-color: rgba(102,126,234,0.4); transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.1); }
.group-lane.cdk-drop-list-receiving { border-color: var(--accent-primary); background: rgba(102,126,234,0.05); box-shadow: 0 0 0 2px rgba(102,126,234,0.2); transform: scale(1.02); z-index: 10; } .group-lane.cdk-drop-list-receiving { border-color: var(--accent-primary); background: rgba(102,126,234,0.05); box-shadow: 0 0 0 2px rgba(102,126,234,0.2); transform: scale(1.02); z-index: 10; }
.group-lane-header { padding: 18px 20px; background: rgba(102,126,234,0.05); border-bottom: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; transition: background 0.3s; pointer-events: none; } .group-lane-header { padding: 18px 20px; background: rgba(102,126,234,0.05); border-bottom: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; transition: background 0.3s; pointer-events: auto; }
.group-lane.expanded .group-lane-header { background: rgba(102,126,234,0.1); border-bottom: 1px solid var(--border-color); } .group-lane.expanded .group-lane-header { background: rgba(102,126,234,0.1); border-bottom: 1px solid var(--border-color); }
.group-actions { pointer-events: auto; } .group-actions { pointer-events: auto; }
.group-lane-body { cursor: default; overflow: hidden; } .group-lane-body { cursor: default; overflow: hidden; }
......
...@@ -75,6 +75,9 @@ export class ManageGroupsComponent implements OnInit { ...@@ -75,6 +75,9 @@ export class ManageGroupsComponent implements OnInit {
if (event) { if (event) {
event.stopPropagation(); event.stopPropagation();
} }
if (this.editingGroup()) {
return; // Do nothing if we are editing a group name
}
if (this.expandedGroup() === groupName) { if (this.expandedGroup() === groupName) {
this.expandedGroup.set(null); this.expandedGroup.set(null);
} else { } else {
......
...@@ -88,7 +88,8 @@ ...@@ -88,7 +88,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
flex: 0 1 350px; width: 260px;
flex-shrink: 0;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
} }
...@@ -102,7 +103,14 @@ ...@@ -102,7 +103,14 @@
cursor: pointer; transition: all 0.2s; cursor: pointer; transition: all 0.2s;
padding: 0; padding: 0;
} }
.dots-scroll-btn:hover { background: var(--bg-hover); border-color: var(--border-strong); color: var(--text-primary); } .dots-scroll-btn:hover:not(:disabled) { background: var(--bg-hover); border-color: var(--border-strong); color: var(--text-primary); }
.dots-scroll-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
background: var(--bg-card);
border-color: var(--border-color);
color: var(--text-muted);
}
.dots-scroll-btn .material-symbols-rounded { font-size: 20px; } .dots-scroll-btn .material-symbols-rounded { font-size: 20px; }
.question-dots { .question-dots {
display: flex; gap: 6px; display: flex; gap: 6px;
......
...@@ -95,15 +95,15 @@ ...@@ -95,15 +95,15 @@
</button> </button>
<div class="dots-wrapper"> <div class="dots-wrapper">
<button class="dots-scroll-btn" (click)="scrollDots(-1)" aria-label="Scroll left"> <button class="dots-scroll-btn" (click)="scrollDots(-1)" [disabled]="!canScrollLeft()" aria-label="Scroll left">
<span class="material-symbols-rounded">chevron_left</span> <span class="material-symbols-rounded">chevron_left</span>
</button> </button>
<div class="question-dots" #dotsContainer> <div class="question-dots" #dotsContainer (scroll)="updateScrollButtons()">
@for (q of questions(); track q._id; let i = $index) { @for (q of questions(); track q._id; let i = $index) {
<button class="dot" [class.active]="i === currentIndex()" [class.answered]="isAnswered(i)" (click)="goTo(i)">{{ i + 1 }}</button> <button class="dot" [class.active]="i === currentIndex()" [class.answered]="isAnswered(i)" (click)="goTo(i)">{{ i + 1 }}</button>
} }
</div> </div>
<button class="dots-scroll-btn" (click)="scrollDots(1)" aria-label="Scroll right"> <button class="dots-scroll-btn" (click)="scrollDots(1)" [disabled]="!canScrollRight()" aria-label="Scroll right">
<span class="material-symbols-rounded">chevron_right</span> <span class="material-symbols-rounded">chevron_right</span>
</button> </button>
</div> </div>
......
...@@ -39,6 +39,9 @@ export class TakeQuizComponent implements OnInit, OnDestroy { ...@@ -39,6 +39,9 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
violationCount = signal(0); violationCount = signal(0);
isFullscreen = signal(false); isFullscreen = signal(false);
canScrollLeft = signal(false);
canScrollRight = signal(false);
private antiCheatSubs: Subscription[] = []; private antiCheatSubs: Subscription[] = [];
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
...@@ -144,6 +147,8 @@ export class TakeQuizComponent implements OnInit, OnDestroy { ...@@ -144,6 +147,8 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
this.startTimer(); this.startTimer();
// Start anti-cheat after quiz loads // Start anti-cheat after quiz loads
this.startAntiCheat(); this.startAntiCheat();
// Scroll active dot into view and initialize scroll buttons
this.scrollActiveDotIntoView();
}, },
error: (err) => { error: (err) => {
this.error.set(err.error?.message || 'Failed to load quiz'); this.error.set(err.error?.message || 'Failed to load quiz');
...@@ -201,7 +206,10 @@ export class TakeQuizComponent implements OnInit, OnDestroy { ...@@ -201,7 +206,10 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
goTo(index: number): void { goTo(index: number): void {
if (index >= 0 && index < this.questions().length) this.currentIndex.set(index); if (index >= 0 && index < this.questions().length) {
this.currentIndex.set(index);
this.scrollActiveDotIntoView();
}
} }
prev(): void { this.goTo(this.currentIndex() - 1); } prev(): void { this.goTo(this.currentIndex() - 1); }
...@@ -209,7 +217,44 @@ export class TakeQuizComponent implements OnInit, OnDestroy { ...@@ -209,7 +217,44 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
scrollDots(direction: number): void { scrollDots(direction: number): void {
const el = this.dotsContainer?.nativeElement; const el = this.dotsContainer?.nativeElement;
if (el) el.scrollBy({ left: direction * 120, behavior: 'smooth' }); if (el) {
el.scrollBy({ left: direction * 120, behavior: 'smooth' });
setTimeout(() => this.updateScrollButtons(), 300);
}
}
updateScrollButtons(): void {
const el = this.dotsContainer?.nativeElement;
if (el) {
// Allow a small tolerance for rounding errors in scroll position
this.canScrollLeft.set(el.scrollLeft > 1);
this.canScrollRight.set(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
}
}
scrollActiveDotIntoView(): void {
setTimeout(() => {
const container = this.dotsContainer?.nativeElement;
if (!container) return;
const activeDot = container.querySelector('.dot.active') as HTMLElement;
if (!activeDot) return;
const containerWidth = container.clientWidth;
const dotLeft = activeDot.offsetLeft;
const dotWidth = activeDot.clientWidth;
// Center the active dot
const targetScrollLeft = dotLeft - (containerWidth / 2) + (dotWidth / 2);
container.scrollTo({
left: targetScrollLeft,
behavior: 'smooth'
});
// Update buttons after smooth scroll animation is likely finished
setTimeout(() => this.updateScrollButtons(), 300);
}, 50);
} }
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
......
...@@ -110,24 +110,63 @@ ...@@ -110,24 +110,63 @@
/* Quick Actions */ /* Quick Actions */
.actions-grid { .actions-grid {
display: grid; display: grid;
grid-template-columns: repeat(4,1fr); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px; gap: 16px;
} }
.action-card { .action-card {
display: flex; display: flex;
align-items: flex-start; align-items: center;
gap: 16px; gap: 16px;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
height: 100%; height: 100%;
padding: 18px !important;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.action-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, rgba(102,126,234,0.05) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.3s;
}
.action-card:hover::before {
opacity: 1;
}
.action-card:hover {
border-color: var(--accent-primary);
box-shadow: 0 12px 30px rgba(102,126,234,0.12);
transform: translateY(-4px);
} }
.action-icon { .action-icon {
font-size: 28px; width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(102,126,234,0.1);
color: var(--accent-primary); color: var(--accent-primary);
border-radius: 12px;
font-size: 24px !important;
flex-shrink: 0; flex-shrink: 0;
margin-top: 2px; transition: all 0.3s;
}
.action-card:hover .action-icon {
background: var(--accent-primary);
color: #fff;
transform: scale(1.1) rotate(-5deg);
} }
.action-info { .action-info {
......
...@@ -258,8 +258,11 @@ ...@@ -258,8 +258,11 @@
<input class="form-input" [(ngModel)]="newGroupInterview.source" placeholder="e.g., LinkedIn, Campus"> <input class="form-input" [(ngModel)]="newGroupInterview.source" placeholder="e.g., LinkedIn, Campus">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Date of Interview</label> <label class="form-label">Date & Time</label>
<input class="form-input" type="date" [(ngModel)]="newGroupInterview.dateOfInterview"> <div style="display: flex; gap: 8px;">
<input class="form-input" type="date" [(ngModel)]="newGroupInterview.dateOfInterview" style="flex: 1;">
<input class="form-input" type="time" [(ngModel)]="newGroupInterview.timeOfInterview" style="flex: 1;">
</div>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
......
...@@ -145,7 +145,8 @@ export class HRGroupInterviewComponent implements OnInit { ...@@ -145,7 +145,8 @@ export class HRGroupInterviewComponent implements OnInit {
position: '', position: '',
techStack: '', techStack: '',
source: '', source: '',
dateOfInterview: new Date().toISOString().split('T')[0] dateOfInterview: new Date().toISOString().split('T')[0],
timeOfInterview: ''
}; };
} }
......
...@@ -184,8 +184,11 @@ ...@@ -184,8 +184,11 @@
<input class="form-input" [(ngModel)]="newInterview.source" placeholder="e.g., LinkedIn, Campus"> <input class="form-input" [(ngModel)]="newInterview.source" placeholder="e.g., LinkedIn, Campus">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Date of Interview</label> <label class="form-label">Date & Time</label>
<input class="form-input" type="date" [(ngModel)]="newInterview.dateOfInterview"> <div style="display: flex; gap: 8px;">
<input class="form-input" type="date" [(ngModel)]="newInterview.dateOfInterview" style="flex: 1;">
<input class="form-input" type="time" [(ngModel)]="newInterview.timeOfInterview" style="flex: 1;">
</div>
</div> </div>
</div> </div>
......
...@@ -34,6 +34,7 @@ export class HRIndividualInterviewComponent implements OnInit { ...@@ -34,6 +34,7 @@ export class HRIndividualInterviewComponent implements OnInit {
techStack: '', techStack: '',
source: '', source: '',
dateOfInterview: new Date().toISOString().split('T')[0], dateOfInterview: new Date().toISOString().split('T')[0],
timeOfInterview: '',
quizIds: [] as string[] quizIds: [] as string[]
}; };
...@@ -95,7 +96,7 @@ export class HRIndividualInterviewComponent implements OnInit { ...@@ -95,7 +96,7 @@ export class HRIndividualInterviewComponent implements OnInit {
}); });
this.newInterview = { this.newInterview = {
candidateId: '', assignedInterviewers: [], assignedHRs: [], assignedPMs: [], position: '', techStack: '', candidateId: '', assignedInterviewers: [], assignedHRs: [], assignedPMs: [], position: '', techStack: '',
source: '', dateOfInterview: new Date().toISOString().split('T')[0], quizIds: [] source: '', dateOfInterview: new Date().toISOString().split('T')[0], timeOfInterview: '', quizIds: []
}; };
this.showCreateModal.set(true); this.showCreateModal.set(true);
} }
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
.group-lane:hover { border-color: rgba(102,126,234,0.4); transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.1); } .group-lane:hover { border-color: rgba(102,126,234,0.4); transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.1); }
.group-lane.cdk-drop-list-receiving { border-color: var(--accent-primary); background: rgba(102,126,234,0.05); box-shadow: 0 0 0 2px rgba(102,126,234,0.2); transform: scale(1.02); z-index: 10; } .group-lane.cdk-drop-list-receiving { border-color: var(--accent-primary); background: rgba(102,126,234,0.05); box-shadow: 0 0 0 2px rgba(102,126,234,0.2); transform: scale(1.02); z-index: 10; }
.group-lane-header { padding: 18px 20px; background: rgba(102,126,234,0.05); border-bottom: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; transition: background 0.3s; pointer-events: none; } .group-lane-header { padding: 18px 20px; background: rgba(102,126,234,0.05); border-bottom: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; transition: background 0.3s; pointer-events: auto; }
.group-lane.expanded .group-lane-header { background: rgba(102,126,234,0.1); border-bottom: 1px solid var(--border-color); } .group-lane.expanded .group-lane-header { background: rgba(102,126,234,0.1); border-bottom: 1px solid var(--border-color); }
.group-actions { pointer-events: auto; } .group-actions { pointer-events: auto; }
.group-lane-body { cursor: default; overflow: hidden; } .group-lane-body { cursor: default; overflow: hidden; }
......
...@@ -70,6 +70,9 @@ export class HRManageGroupsComponent { ...@@ -70,6 +70,9 @@ export class HRManageGroupsComponent {
if (event) { if (event) {
event.stopPropagation(); event.stopPropagation();
} }
if (this.editingGroup()) {
return; // Do nothing if we are editing a group name
}
if (this.expandedGroup() === groupName) { if (this.expandedGroup() === groupName) {
this.expandedGroup.set(null); this.expandedGroup.set(null);
} else { } else {
......
...@@ -381,6 +381,7 @@ ul, ol { ...@@ -381,6 +381,7 @@ ul, ol {
.btn-primary:hover:not(:disabled) { .btn-primary:hover:not(:disabled) {
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(var(--accent-primary-rgb), 0.35); box-shadow: 0 4px 16px rgba(var(--accent-primary-rgb), 0.35);
color: #fff;
} }
.btn-outline { .btn-outline {
...@@ -403,6 +404,7 @@ ul, ol { ...@@ -403,6 +404,7 @@ ul, ol {
.btn-danger:hover:not(:disabled) { .btn-danger:hover:not(:disabled) {
background: #dc2626; background: #dc2626;
transform: translateY(-1px); transform: translateY(-1px);
color: #fff;
} }
.btn-ghost { .btn-ghost {
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment