Commit eb8e8e8b authored by AravindR-K's avatar AravindR-K

Feat : Now you can add students to groups (Drag and Drop feature) + fixed major bugs

parent fd448421
...@@ -98,6 +98,58 @@ ...@@ -98,6 +98,58 @@
} }
}); });
// @route PUT /api/admin/users/:userId
// @desc Edit a user (specifically for HR users to edit email/password)
// @access Admin
router.put('/users/:userId', async (req, res) => {
try {
const { userId } = req.params;
const { email, password, name, group } = req.body;
const user = await User.findById(userId);
if (!user) return res.status(404).json({ message: 'User not found' });
// Ensure we don't accidentally edit an admin user here for safety
if (user.role === 'admin') {
return res.status(403).json({ message: 'Cannot edit admin users through this endpoint' });
}
if (email) user.email = email;
if (password) user.password = password; // Will be hashed by pre-save hook
if (name) user.name = name;
if (group !== undefined) user.group = group;
await user.save();
res.json({ message: 'User updated successfully', user: { id: user._id, name: user.name, email: user.email, role: user.role } });
} catch (error) {
if (error.code === 11000) {
return res.status(400).json({ message: 'Email already exists' });
}
res.status(500).json({ message: 'Server error', error: error.message });
}
});
// @route PUT /api/admin/users/:userId/group
// @desc Assign user to a group explicitly
// @access Admin
router.put('/users/:userId/group', async (req, res) => {
try {
const { userId } = req.params;
const { group } = req.body;
const user = await User.findById(userId);
if (!user) return res.status(404).json({ message: 'User not found' });
user.group = group || 'General';
await user.save();
res.json({ message: 'User assigned to group successfully', user });
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
// @route GET /api/admin/users/:userId/history // @route GET /api/admin/users/:userId/history
// @desc Get a user's test history // @desc Get a user's test history
// @access Admin // @access Admin
...@@ -602,10 +654,9 @@ ...@@ -602,10 +654,9 @@
const correctStr = correct.toString().trim(); const correctStr = correct.toString().trim();
const correctArr = correctStr.split(',').map(s => s.trim()); const correctArr = correctStr.split(',').map(s => s.trim());
// 🔥 convert text → index // Save exact literal text of correct answer
const correctAnswers = options const correctAnswers = options
.map((opt, index) => correctArr.includes(opt) ? index.toString() : null) .filter(opt => correctArr.includes(opt));
.filter(val => val !== null);
if (correctAnswers.length === 0) { if (correctAnswers.length === 0) {
throw new Error(`Correct answer "${correctStr}" not matching options`); throw new Error(`Correct answer "${correctStr}" not matching options`);
......
...@@ -357,6 +357,31 @@ router.delete('/groups/:name', async (req, res) => { ...@@ -357,6 +357,31 @@ router.delete('/groups/:name', async (req, res) => {
} }
}); });
// @route PUT /api/hr/users/:userId/group
// @desc Assign user to a group explicitly
// @access HR
router.put('/users/:userId/group', async (req, res) => {
try {
const { userId } = req.params;
const { group } = req.body;
const user = await User.findById(userId);
if (!user) return res.status(404).json({ message: 'User not found' });
// Only allow HR to modify candidates (optional security check)
if (user.role === 'admin' || user.role === 'hr') {
return res.status(403).json({ message: 'HR can only assign candidates to groups' });
}
user.group = group || 'General';
await user.save();
res.json({ message: 'User assigned to group successfully', user });
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
// @route GET /api/hr/stats // @route GET /api/hr/stats
router.get('/stats', async (req, res) => { router.get('/stats', async (req, res) => {
try { try {
......
...@@ -41,6 +41,10 @@ export const routes: Routes = [ ...@@ -41,6 +41,10 @@ export const routes: Routes = [
path: 'manage-groups', path: 'manage-groups',
loadComponent: () => import('./pages/admin/manage-groups/manage-groups').then(m => m.ManageGroupsComponent) loadComponent: () => import('./pages/admin/manage-groups/manage-groups').then(m => m.ManageGroupsComponent)
}, },
{
path: 'hr-users',
loadComponent: () => import('./pages/admin/hr-users/hr-users').then(m => m.AdminHrUsersComponent)
},
{ {
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)
...@@ -109,10 +113,6 @@ export const routes: Routes = [ ...@@ -109,10 +113,6 @@ export const routes: Routes = [
path: 'dashboard', path: 'dashboard',
loadComponent: () => import('./pages/candidate/dashboard/dashboard').then(m => m.CandidateDashboardComponent) loadComponent: () => import('./pages/candidate/dashboard/dashboard').then(m => m.CandidateDashboardComponent)
}, },
{
path: 'quiz/:quizId',
loadComponent: () => import('./pages/candidate/take-quiz/take-quiz').then(m => m.TakeQuizComponent)
},
{ {
path: 'profile', path: 'profile',
loadComponent: () => import('./pages/candidate/profile/profile').then(m => m.CandidateProfileComponent) loadComponent: () => import('./pages/candidate/profile/profile').then(m => m.CandidateProfileComponent)
...@@ -124,6 +124,13 @@ export const routes: Routes = [ ...@@ -124,6 +124,13 @@ export const routes: Routes = [
] ]
}, },
// Standalone Candidate Routes (Maximized, no sidebar)
{
path: 'candidate/quiz/:quizId',
canActivate: [candidateGuard],
loadComponent: () => import('./pages/candidate/take-quiz/take-quiz').then(m => m.TakeQuizComponent)
},
// Backward compat: old student routes redirect to candidate // Backward compat: old student routes redirect to candidate
{ path: 'student', redirectTo: '/candidate', pathMatch: 'prefix' }, { path: 'student', redirectTo: '/candidate', pathMatch: 'prefix' },
{ path: 'student/dashboard', redirectTo: '/candidate/dashboard' }, { path: 'student/dashboard', redirectTo: '/candidate/dashboard' },
......
...@@ -128,6 +128,19 @@ ...@@ -128,6 +128,19 @@
</div> </div>
<span class="material-symbols-rounded arrow-icon">arrow_forward</span> <span class="material-symbols-rounded arrow-icon">arrow_forward</span>
</a> </a>
@if (authService.getUserRole() === 'admin') {
<a routerLink="/admin/hr-users" class="glassy-option" (click)="closeManageUsersPopup()">
<div class="option-icon-wrapper">
<span class="material-symbols-rounded block-icon">assignment_ind</span>
</div>
<div class="option-text">
<h3>HR Users</h3>
<p>Manage HR staff platform access</p>
</div>
<span class="material-symbols-rounded arrow-icon">arrow_forward</span>
</a>
}
</div> </div>
</div> </div>
</div> </div>
......
.page-container { padding: 32px 40px; max-width: 1000px; }
.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: stretch; flex-wrap: wrap; }
.input-container { flex: 1; position: relative; display: flex; align-items: center; min-width: 220px; }
.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); }
.form-input-small { padding: 10px 14px; font-size: 14px; border-radius: 8px; }
.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(320px, 1fr)); 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: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); }
.action-btn { background: none; border: none; color: var(--text-muted); cursor: pointer; border-radius: 6px; padding: 6px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
.action-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.text-danger:hover { color: #ef4444; background: rgba(239, 68, 68, 0.1); }
.text-success:hover { color: #22c55e; background: rgba(34, 197, 94, 0.1); }
.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; opacity: 0.6; }
.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: 1fr; }
}
<div class="page-container animate-fade-in">
<div class="page-header">
<h1>HR Users</h1>
<p class="page-subtitle">Manage HR Staff platform access and identity</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">Add HR User</h2>
<form (ngSubmit)="createHRUser()" class="group-form">
<div class="form-group row-align">
<div class="input-container">
<span class="material-symbols-rounded block-icon">badge</span>
<input class="form-input" [ngModel]="newName()" (ngModelChange)="newName.set($event)" name="name" placeholder="Full Name" required>
</div>
<div class="input-container">
<span class="material-symbols-rounded block-icon">mail</span>
<input class="form-input" type="email" [ngModel]="newEmail()" (ngModelChange)="newEmail.set($event)" name="email" placeholder="Email Address" required>
</div>
<div class="input-container">
<span class="material-symbols-rounded block-icon">lock</span>
<input class="form-input" type="password" [ngModel]="newPassword()" (ngModelChange)="newPassword.set($event)" name="password" placeholder="Password" required>
</div>
<button type="submit" class="btn btn-primary" [disabled]="creating() || !newName().trim() || !newEmail().trim() || !newPassword().trim()">
@if (creating()) {
<span class="spinner"></span>
} @else {
<span class="material-symbols-rounded">person_add</span> Add HR
}
</button>
</div>
</form>
</div>
<h2 class="section-title">Existing HR Users</h2>
@if (loading()) {
<div class="loading-state">
<div class="loader"></div>
</div>
} @else if (hrUsers().length === 0) {
<div class="empty-state">
<span class="material-symbols-rounded empty-icon">manage_accounts</span>
<h3>No HR users available</h3>
<p>Add your first HR user above</p>
</div>
} @else {
<div class="groups-grid">
@for (user of hrUsers(); track user._id) {
<div class="group-card">
<div class="group-icon-wrapper">
<span class="material-symbols-rounded">admin_panel_settings</span>
</div>
<div class="group-details" style="flex: 1;">
@if (editingId() === user._id) {
<div style="display: flex; flex-direction: column; gap: 8px;">
<input type="text" class="form-input form-input-small" [ngModel]="editName()" (ngModelChange)="editName.set($event)" placeholder="Name" autofocus>
<input type="email" class="form-input form-input-small" [ngModel]="editEmail()" (ngModelChange)="editEmail.set($event)" placeholder="Email">
<input type="password" class="form-input form-input-small" [ngModel]="editPassword()" (ngModelChange)="editPassword.set($event)" placeholder="New Password (optional)">
</div>
} @else {
<h4>{{ user.name }}</h4>
<p style="margin: 4px 0 0; font-size: 13px; color: var(--text-muted);">{{ user.email }}</p>
}
</div>
<div class="group-actions" style="display: flex; gap: 8px;">
@if (editingId() === user._id) {
<button class="action-btn text-success" (click)="saveEdit(user._id)" title="Save"><span class="material-symbols-rounded">check</span></button>
<button class="action-btn text-danger" (click)="cancelEdit()" title="Cancel"><span class="material-symbols-rounded">close</span></button>
} @else {
<button class="action-btn" (click)="startEdit(user)" title="Edit"><span class="material-symbols-rounded">edit</span></button>
<button class="action-btn text-danger" (click)="deleteHRUser(user._id)" title="Delete"><span class="material-symbols-rounded">delete</span></button>
}
</div>
</div>
}
</div>
}
</div>
import { Component, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { AuthService } from '../../../services/auth.service';
import { QuizService } from '../../../services/quiz.service';
@Component({
selector: 'app-hr-users',
standalone: true,
imports: [CommonModule, FormsModule, RouterLink],
templateUrl: './hr-users.html',
styleUrl: './hr-users.css'
})
export class AdminHrUsersComponent implements OnInit {
hrUsers = signal<any[]>([]);
loading = signal<boolean>(true);
error = signal<string>('');
success = signal<string>('');
// Create New HR
newName = signal('');
newEmail = signal('');
newPassword = signal('');
creating = signal(false);
// Edit HR
editingId = signal<string | null>(null);
editName = signal('');
editEmail = signal('');
editPassword = signal('');
saving = signal(false);
constructor(private quizService: QuizService) {}
ngOnInit(): void {
this.loadHRUsers();
}
loadHRUsers(): void {
this.loading.set(true);
this.quizService.getUsers('hr').subscribe({
next: (res) => {
this.hrUsers.set(res.users);
this.loading.set(false);
},
error: () => this.loading.set(false)
});
}
createHRUser(): void {
if (!this.newName().trim() || !this.newEmail().trim() || !this.newPassword().trim()) {
this.error.set('Please fill out name, email, and password');
return;
}
this.creating.set(true);
this.error.set('');
this.success.set('');
this.quizService.createHRUser({
name: this.newName().trim(),
email: this.newEmail().trim(),
password: this.newPassword().trim()
}).subscribe({
next: () => {
this.success.set('HR User created successfully!');
this.creating.set(false);
this.newName.set('');
this.newEmail.set('');
this.newPassword.set('');
this.loadHRUsers();
},
error: (err) => {
this.error.set(err.error?.message || 'Failed to create HR user');
this.creating.set(false);
}
});
}
startEdit(user: any): void {
this.editingId.set(user._id);
this.editName.set(user.name);
this.editEmail.set(user.email);
this.editPassword.set(''); // Blank password to keep it unchanged if not specified
}
cancelEdit(): void {
this.editingId.set(null);
}
saveEdit(userId: string): void {
const data: any = {};
if (this.editName().trim()) data.name = this.editName().trim();
if (this.editEmail().trim()) data.email = this.editEmail().trim();
if (this.editPassword().trim()) data.password = this.editPassword().trim();
this.saving.set(true);
this.error.set('');
this.success.set('');
this.quizService.editUser(userId, data).subscribe({
next: () => {
this.success.set('HR User updated successfully!');
this.saving.set(false);
this.cancelEdit();
this.loadHRUsers();
},
error: (err) => {
this.error.set(err.error?.message || 'Failed to update HR user');
this.saving.set(false);
}
});
}
deleteHRUser(userId: string): void {
if (confirm('Are you sure you want to delete this HR server? This revokes access entirely.')) {
this.error.set('');
this.success.set('');
this.quizService.deleteUser(userId).subscribe({
next: () => {
this.success.set('HR User deleted successfully!');
this.loadHRUsers();
},
error: (err) => {
this.error.set(err.error?.message || 'Failed to delete HR user');
}
});
}
}
}
.page-container { padding: 32px 40px; max-width: 900px; } .page-container { max-width: 1400px; padding: 32px 40px; margin: 0 auto; height: calc(100vh - 64px); display: flex; box-sizing: border-box; }
.split-view { gap: 32px; align-items: stretch; }
.main-workspace { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow-y: auto; padding-right: 16px; }
.page-header { margin-bottom: 28px; } .page-header { margin-bottom: 28px; }
.page-header h1 { font-size: 26px; font-weight: 700; color: var(--text-primary); margin: 0 0 8px; } .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; } .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; } .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 { background: var(--bg-card); border-radius: var(--radius-lg); border: 1px solid var(--border-color); }
.card-padding { padding: 24px; } .card-padding { padding: 24px; }
.group-form { display: flex; flex-direction: column; } .group-form { display: flex; flex-direction: column; }
.row-align { display: flex; gap: 16px; align-items: center; } .row-align { display: flex; gap: 16px; align-items: stretch; flex-wrap: wrap; }
.input-container { flex: 1; position: relative; display: flex; align-items: center; } .input-container { flex: 1; position: relative; display: flex; align-items: center; min-width: 220px; }
.input-container .block-icon { position: absolute; left: 16px; color: var(--text-muted); font-size: 20px; } .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 { 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); } .form-input:focus { border-color: var(--accent-primary); box-shadow: 0 0 0 3px rgba(102,126,234,0.15); }
.form-input-small { padding: 10px 14px; font-size: 14px; border-radius: 8px; }
.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 { 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-size: 15px; font-family: inherit; }
.btn-primary { background: var(--accent-gradient); color: #fff; box-shadow: 0 4px 15px rgba(102,126,234,0.3); } .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: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(260px, 1fr)); gap: 16px; } .action-btn { background: none; border: none; color: var(--text-muted); cursor: pointer; border-radius: 6px; padding: 6px; display: inline-flex; align-items: center; justify-content: center; transition: all 0.2s; }
.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; } .action-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.group-card:hover { border-color: var(--accent-primary); transform: translateY(-3px); box-shadow: var(--shadow-md); } .text-danger:hover { color: #ef4444; background: rgba(239, 68, 68, 0.1); }
.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; } .text-success:hover { color: #22c55e; background: rgba(34, 197, 94, 0.1); }
.group-icon-wrapper .material-symbols-rounded { font-size: 26px; }
.group-details h4 { margin: 0; font-size: 17px; font-weight: 600; color: var(--text-primary); } /* DRAG AND DROP GROUPS */
.groups-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 24px; }
.group-lane { display: flex; flex-direction: column; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 16px; overflow: hidden; min-height: 80px; transition: all 0.25s ease-in-out; cursor: pointer; }
.group-lane:hover { border-color: rgba(102,126,234,0.4); transform: translateY(-2px); box-shadow: var(--shadow-md); }
.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: 16px 20px; background: rgba(102,126,234,0.05); border-bottom: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; transition: border-bottom 0.2s; pointer-events: none; }
.group-lane:has(.group-lane-body) .group-lane-header { border-bottom: 1px solid var(--border-color); }
.group-actions { pointer-events: auto; }
.group-lane-body { cursor: default; }
.group-header-info { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.lane-title { display: flex; align-items: center; gap: 8px; font-size: 16px; margin: 0; color: var(--text-primary); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lane-title .material-symbols-rounded { font-size: 18px; color: var(--accent-primary); }
.member-count { font-size: 12px; color: var(--text-muted); font-weight: 500; }
.group-actions { display: flex; gap: 4px; }
.group-lane-body { flex: 1; padding: 16px; display: flex; flex-direction: column; gap: 10px; background: rgba(0,0,0,0.01); min-height: 100px; }
.empty-body { justify-content: center; align-items: center; }
.drop-placeholder { text-align: center; font-size: 13px; color: var(--text-muted); font-style: italic; opacity: 0.6; padding: 30px; border: 1px dashed var(--border-color); border-radius: 12px; width: 100%; box-sizing: border-box; }
.student-pill { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border-color); border-radius: 10px; cursor: grab; box-shadow: 0 2px 5px rgba(0,0,0,0.02); transition: transform 0.2s, box-shadow 0.2s; }
.student-pill:active { cursor: grabbing; }
.pill-avatar { width: 32px; height: 32px; border-radius: 50%; 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; font-size: 14px; font-weight: 600; flex-shrink: 0; }
.pill-info { flex: 1; min-width: 0; }
.pill-name { display: block; font-size: 14px; font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* UNASSIGNED SIDEBAR */
.unassigned-sidebar { width: 340px; display: flex; flex-direction: column; flex-shrink: 0; height: 100%; overflow: hidden; background: var(--surface); }
.sidebar-header { padding: 20px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; background: rgba(102,126,234,0.05); }
.sidebar-title { display: flex; align-items: center; gap: 10px; }
.sidebar-title h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--text-primary); }
.sidebar-title .material-symbols-rounded { color: var(--text-muted); font-size: 20px; }
.sidebar-badge { background: var(--accent-primary); color: white; padding: 2px 8px; border-radius: 20px; font-size: 12px; font-weight: 600; }
.unassigned-list { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.empty-unassigned { text-align: center; padding: 40px 20px; color: var(--text-muted); display: flex; flex-direction: column; align-items: center; gap: 12px; opacity: 0.7; }
.empty-unassigned .material-symbols-rounded { font-size: 40px; color: #22c55e; }
.unassigned-card { display: flex; align-items: center; gap: 16px; padding: 14px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; cursor: grab; transition: border-color 0.2s, box-shadow 0.2s; position: relative; }
.unassigned-card:hover { border-color: var(--accent-primary); box-shadow: 0 4px 12px rgba(102,126,234,0.08); transform: translateY(-2px); }
.unassigned-card:active { cursor: grabbing; }
.unassigned-avatar { width: 44px; height: 44px; border-radius: 50%; background: var(--bg-input); color: var(--text-primary); display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 600; flex-shrink: 0; }
.unassigned-details { flex: 1; min-width: 0; }
.unassigned-details h4 { margin: 0 0 4px 0; font-size: 15px; font-weight: 600; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.unassigned-details p { margin: 0; font-size: 13px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.drag-handle { color: var(--text-muted); cursor: grab; opacity: 0.5; font-size: 20px; }
/* CDK DRAG AND DROP PREVIEWS */
.cdk-drag-preview { box-shadow: 0 10px 30px rgba(0,0,0,0.15); border-radius: 12px; opacity: 0.95; z-index: 1000 !important; cursor: grabbing !important; }
.cdk-drag-animating { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); }
.custom-drag-placeholder { opacity: 0.3; background: rgba(102,126,234,0.1); border: 2px dashed var(--accent-primary); border-radius: 12px; min-height: 60px; transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); }
.unassigned-list.cdk-drop-list-dragging .unassigned-card:not(.cdk-drag-placeholder) { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); }
.group-lane-body.cdk-drop-list-dragging .student-pill:not(.cdk-drag-placeholder) { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); }
.alert { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-radius: 12px; margin-bottom: 24px; font-size: 14px; font-weight: 500; } .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-success { background: rgba(74,222,128,0.1); border: 1px solid rgba(74,222,128,0.2); color: #22c55e; }
...@@ -38,13 +97,8 @@ ...@@ -38,13 +97,8 @@
.loading-state { display: flex; justify-content: center; padding: 40px; } .loading-state { display: flex; justify-content: center; padding: 40px; }
@keyframes spin { 100% { transform: rotate(360deg); } } @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); } @media (max-width: 900px) {
.empty-icon { font-size: 48px; color: var(--text-muted); display: block; margin-bottom: 12px; } .split-view { flex-direction: column; }
.empty-state h3 { color: var(--text-primary); margin: 0 0 8px; font-size: 18px; } .unassigned-sidebar { width: 100%; height: 400px; }
.empty-state p { color: var(--text-muted); margin: 0; font-size: 14px; } .page-container { height: auto; padding: 20px 16px; }
@media (max-width: 600px) {
.page-container { padding: 20px 16px; }
.row-align { flex-direction: column; align-items: stretch; }
.groups-grid { grid-template-columns: 1fr; }
} }
<div class="page-container animate-fade-in"> <div class="page-container animate-fade-in split-view" cdkDropListGroup>
<div class="page-header">
<h1>Manage Groups</h1> <div class="main-workspace">
<p class="page-subtitle">Create and oversee organizational groups for candidates</p> <div class="page-header">
</div> <h1>Manage Groups</h1>
<p class="page-subtitle">Create organizational groups and effortlessly drag-and-drop candidates into them.</p>
</div>
@if (error()) { @if (error()) {
<div class="alert alert-error"><span class="material-symbols-rounded">error</span> {{ error() }}</div> <div class="alert alert-error"><span class="material-symbols-rounded">error</span> {{ error() }}</div>
} }
@if (success()) { @if (success()) {
<div class="alert alert-success"><span class="material-symbols-rounded">check_circle</span> {{ success() }}</div> <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;"> <!-- Create Group Area -->
<h2 class="section-title">Create New Group</h2> <div class="card card-padding form-card" style="margin-bottom: 30px;">
<form (ngSubmit)="createGroup()" class="group-form"> <h2 class="section-title" style="margin-top: 0; margin-bottom: 16px;">Create New Group</h2>
<div class="form-group row-align"> <form (ngSubmit)="createGroup()" class="group-form">
<div class="input-container"> <div class="form-group row-align">
<span class="material-symbols-rounded block-icon">group_add</span> <div class="input-container">
<input class="form-input" [ngModel]="newGroupName()" (ngModelChange)="newGroupName.set($event)" name="groupName" placeholder="e.g., Spring Cohort 2026"> <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., Summer Interns 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> </div>
</form>
<button type="submit" class="btn btn-primary" [disabled]="loading() || !newGroupName().trim()"> </div>
@if (loading()) {
<span class="spinner"></span> <h2 class="section-title">Your Groups</h2>
} @else { @if (loadingGroups()) {
<span class="material-symbols-rounded">add</span> Add Group <div class="loading-state"><div class="loader"></div></div>
} } @else if (groups().length === 0) {
</button> <div class="empty-state">
<span class="material-symbols-rounded empty-icon">group_off</span>
<h3>No groups available</h3>
<p>Create your first grouping tier up above</p>
</div>
} @else {
<!-- Outer wrapper handles the drag contexts collectively -->
<div class="groups-grid">
@for (group of groups(); track group) {
<div class="group-lane card" cdkDropList [cdkDropListData]="group" (cdkDropListDropped)="onDrop($event)" (click)="toggleGroupExpansion(group)">
<div class="group-lane-header">
<div class="group-header-info">
@if (editingGroup() === group) {
<input type="text" class="form-input form-input-small" [ngModel]="editName()" (ngModelChange)="editName.set($event)" autofocus>
} @else {
<h4 class="lane-title"><span class="material-symbols-rounded">workspaces</span> {{ group }}</h4>
<span class="member-count">{{ getStudentsInGroup(group).length }} Members</span>
}
</div>
<div class="group-actions" (click)="$event.stopPropagation()">
@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>
<!-- Drop Zone Body (Displayed on Expansion) -->
@if (expandedGroup() === group) {
<div class="group-lane-body" [class.empty-body]="getStudentsInGroup(group).length === 0" (click)="$event.stopPropagation()">
@if (getStudentsInGroup(group).length === 0) {
<div class="drop-placeholder">No candidates found in {{group}}</div>
}
@for (student of getStudentsInGroup(group); track student._id) {
<div class="student-pill card" cdkDrag [cdkDragData]="student">
<div class="pill-avatar">{{ student.name.charAt(0).toUpperCase() }}</div>
<div class="pill-info">
<span class="pill-name">{{ student.name }}</span>
</div>
<button class="action-btn text-danger" title="Remove" (click)="removeFromGroup(student)">
<span class="material-symbols-rounded" style="font-size: 16px;">close</span>
</button>
<div class="custom-drag-placeholder" *cdkDragPlaceholder></div>
</div>
}
</div>
}
</div>
}
</div> </div>
</form> }
</div> </div>
<h2 class="section-title">Existing Groups</h2> <!-- Sidebar: Unassigned Candidates -->
@if (loadingGroups()) { <aside class="unassigned-sidebar card">
<div class="loading-state"> <div class="sidebar-header">
<div class="loader"></div> <div class="sidebar-title">
</div> <span class="material-symbols-rounded">person_search</span>
} @else if (groups().length === 0) { <h2>Unassigned Candidates</h2>
<div class="empty-state"> </div>
<span class="material-symbols-rounded empty-icon">group_off</span> <div class="sidebar-badge">{{ unassignedStudents().length }}</div>
<h3>No groups available</h3>
<p>Create your first group above</p>
</div> </div>
} @else {
<div class="groups-grid"> <div class="unassigned-list" cdkDropList [cdkDropListData]="'General'" (cdkDropListDropped)="onDrop($event)">
@for (group of groups(); track group) { @if (unassignedStudents().length === 0) {
<div class="group-card"> <div class="empty-unassigned">
<div class="group-icon-wrapper"> <span class="material-symbols-rounded">done_all</span>
<span class="material-symbols-rounded">group</span> <p>All candidates are matched to groups!</p>
</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>
<div class="group-actions"> }
@if (editingGroup() === group) { @for (student of unassignedStudents(); track student._id) {
<button class="action-btn text-success" (click)="saveEdit(group)" title="Save"><span class="material-symbols-rounded">check</span></button> <div class="unassigned-card" cdkDrag [cdkDragData]="student">
<button class="action-btn text-danger" (click)="cancelEdit()" title="Cancel"><span class="material-symbols-rounded">close</span></button> <div class="unassigned-avatar">{{ student.name.charAt(0).toUpperCase() }}</div>
} @else { <div class="unassigned-details">
<button class="action-btn" (click)="startEdit(group)" title="Edit"><span class="material-symbols-rounded">edit</span></button> <h4>{{ student.name }}</h4>
<button class="action-btn text-danger" (click)="deleteGroup(group)" title="Delete"><span class="material-symbols-rounded">delete</span></button> <p>{{ student.email }}</p>
} </div>
<span class="material-symbols-rounded drag-handle">drag_indicator</span>
<div class="custom-drag-placeholder" *cdkDragPlaceholder></div>
</div> </div>
</div> }
}
</div> </div>
} </aside>
</div> </div>
import { Component, signal, OnInit } from '@angular/core'; import { Component, signal, OnInit, computed } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { QuizService } from '../../../services/quiz.service'; import { QuizService } from '../../../services/quiz.service';
import { DragDropModule, CdkDragDrop } from '@angular/cdk/drag-drop';
@Component({ @Component({
selector: 'app-manage-groups', selector: 'app-manage-groups',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule, RouterLink], imports: [CommonModule, FormsModule, RouterLink, DragDropModule],
templateUrl: './manage-groups.html', templateUrl: './manage-groups.html',
styleUrl: './manage-groups.css' styleUrl: './manage-groups.css'
}) })
export class ManageGroupsComponent implements OnInit { export class ManageGroupsComponent implements OnInit {
groups = signal<string[]>([]); groups = signal<string[]>([]);
allStudents = signal<any[]>([]);
newGroupName = signal<string>(''); newGroupName = signal<string>('');
loading = signal<boolean>(false); loading = signal<boolean>(false);
error = signal<string>(''); error = signal<string>('');
...@@ -22,10 +25,17 @@ export class ManageGroupsComponent implements OnInit { ...@@ -22,10 +25,17 @@ export class ManageGroupsComponent implements OnInit {
editingGroup = signal<string | null>(null); editingGroup = signal<string | null>(null);
editName = signal<string>(''); editName = signal<string>('');
expandedGroup = signal<string | null>(null);
unassignedStudents = computed(() => {
return this.allStudents().filter(s => !s.group || s.group === 'General');
});
constructor(private quizService: QuizService) {} constructor(private quizService: QuizService) {}
ngOnInit(): void { ngOnInit(): void {
this.loadGroups(); this.loadGroups();
this.loadStudents();
} }
loadGroups(): void { loadGroups(): void {
...@@ -42,6 +52,32 @@ export class ManageGroupsComponent implements OnInit { ...@@ -42,6 +52,32 @@ export class ManageGroupsComponent implements OnInit {
}); });
} }
loadStudents(): void {
this.quizService.getUsers('candidate').subscribe({
next: (res) => {
// Since getUsers('candidate') might not exist perfectly on HR route if we don't change HR route, wait, HR route has getHRCandidates
// Let's rely on standard getUsers which points to getBaseUrl()/users depending on role if properly set. Actually quiz.service.ts uses adminUrl directly for getUsers. Let's see. I will dynamically fetch.
this.allStudents.set(res.users ? res.users.filter((u:any) => u.role === 'candidate') : (res.candidates || []));
},
error: () => {}
});
}
getStudentsInGroup(groupName: string): any[] {
return this.allStudents().filter(s => s.group === groupName);
}
toggleGroupExpansion(groupName: string, event?: Event): void {
if (event) {
event.stopPropagation();
}
if (this.expandedGroup() === groupName) {
this.expandedGroup.set(null);
} else {
this.expandedGroup.set(groupName);
}
}
createGroup(): void { createGroup(): void {
if (!this.newGroupName().trim()) { if (!this.newGroupName().trim()) {
this.error.set('Group name cannot be blank'); this.error.set('Group name cannot be blank');
...@@ -89,6 +125,10 @@ export class ManageGroupsComponent implements OnInit { ...@@ -89,6 +125,10 @@ export class ManageGroupsComponent implements OnInit {
this.success.set('Group updated successfully!'); this.success.set('Group updated successfully!');
this.cancelEdit(); this.cancelEdit();
this.loadGroups(); this.loadGroups();
// Update local students cache
this.allStudents.update(students =>
students.map(s => s.group === oldName ? { ...s, group: trimmed } : s)
);
}, },
error: (err) => { error: (err) => {
this.error.set(err.error?.message || 'Failed to update group'); this.error.set(err.error?.message || 'Failed to update group');
...@@ -97,13 +137,17 @@ export class ManageGroupsComponent implements OnInit { ...@@ -97,13 +137,17 @@ export class ManageGroupsComponent implements OnInit {
} }
deleteGroup(name: string): void { 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.`)) { if (confirm(`Are you sure you want to delete the group "${name}"?\nUsers mapped to this group will moved to Unassigned.`)) {
this.error.set(''); this.error.set('');
this.success.set(''); this.success.set('');
this.quizService.deleteGroup(name).subscribe({ this.quizService.deleteGroup(name).subscribe({
next: () => { next: () => {
this.success.set('Group deleted successfully!'); this.success.set('Group deleted successfully!');
this.loadGroups(); this.loadGroups();
// Move students to General
this.allStudents.update(students =>
students.map(s => s.group === name ? { ...s, group: 'General' } : s)
);
}, },
error: (err) => { error: (err) => {
this.error.set(err.error?.message || 'Failed to delete group'); this.error.set(err.error?.message || 'Failed to delete group');
...@@ -111,4 +155,39 @@ export class ManageGroupsComponent implements OnInit { ...@@ -111,4 +155,39 @@ export class ManageGroupsComponent implements OnInit {
}); });
} }
} }
onDrop(event: CdkDragDrop<string>) {
if (event.previousContainer === event.container) {
return; // No change
}
const student = event.item.data;
const previousGroup = event.previousContainer.data;
const newGroup = event.container.data;
this.allStudents.update(students =>
students.map(s => s._id === student._id ? { ...s, group: newGroup } : s)
);
this.quizService.assignUserGroup(student._id, newGroup).subscribe({
next: () => {
// Success
this.success.set(``);
},
error: () => {
this.error.set('Failed to assign user to group');
this.loadStudents();
}
});
}
removeFromGroup(student: any) {
this.allStudents.update(students =>
students.map(s => s._id === student._id ? { ...s, group: 'General' } : s)
);
this.quizService.assignUserGroup(student._id, 'General').subscribe({
next: () => {},
error: () => this.loadStudents()
});
}
} }
.dashboard-layout { display: flex; min-height: 100vh; background: #0f1117; } .page-container {
.sidebar { width: 260px; background: rgba(255,255,255,0.03); border-right: 1px solid rgba(255,255,255,0.06); display: flex; flex-direction: column; padding: 24px 16px; position: fixed; top: 0; left: 0; bottom: 0; z-index: 10; } padding: 32px 40px;
.sidebar-header { display: flex; align-items: center; gap: 10px; padding: 0 8px; margin-bottom: 32px; flex-wrap: wrap; } max-width: 1400px;
.sidebar-header .logo-icon { font-size: 28px; } }
.sidebar-header h2 { font-size: 20px; font-weight: 700; color: #fff; margin: 0; }
.role-badge { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; font-size: 11px; font-weight: 600; padding: 3px 10px; border-radius: 20px; text-transform: uppercase; }
.sidebar-nav { display: flex; flex-direction: column; gap: 4px; flex: 1; }
.nav-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-radius: 12px; color: rgba(255,255,255,0.6); text-decoration: none; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.nav-item:hover { background: rgba(255,255,255,0.06); color: #fff; }
.nav-item.active { background: rgba(102,126,234,0.15); color: #667eea; }
.nav-icon { font-size: 18px; }
.sidebar-footer { border-top: 1px solid rgba(255,255,255,0.06); padding-top: 16px; }
.user-info { display: flex; align-items: center; gap: 12px; padding: 8px; margin-bottom: 12px; }
.user-avatar { width: 38px; height: 38px; border-radius: 10px; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 16px; }
.user-details { display: flex; flex-direction: column; }
.user-name { color: #fff; font-size: 13px; font-weight: 600; }
.user-email { color: rgba(255,255,255,0.4); font-size: 11px; }
.logout-btn { width: 100%; padding: 10px; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); border-radius: 10px; color: #f87171; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 8px; font-family: inherit; }
.logout-btn:hover { background: rgba(239,68,68,0.2); }
.main-content { flex: 1; margin-left: 260px; padding: 40px; }
.loading-state { display: flex; flex-direction: column; align-items: center; padding: 80px 0; gap: 16px; } .loading-state { display: flex; flex-direction: column; align-items: center; padding: 80px 0; gap: 16px; }
.loader { width: 40px; height: 40px; border: 3px solid rgba(255,255,255,0.1); border-top-color: #667eea; border-radius: 50%; animation: spin 0.8s linear infinite; } .loader { width: 40px; height: 40px; border: 3px solid var(--border-color); border-top-color: #667eea; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } } @keyframes spin { to { transform: rotate(360deg); } }
.loading-state p { color: rgba(255,255,255,0.5); } .loading-state p { color: var(--text-secondary); }
.breadcrumb { display: flex; align-items: center; gap: 8px; margin-bottom: 28px; flex-wrap: wrap; } .breadcrumb { display: flex; align-items: center; gap: 8px; margin-bottom: 28px; flex-wrap: wrap; }
.breadcrumb a { color: #667eea; text-decoration: none; font-size: 14px; font-weight: 500; } .breadcrumb a { color: #667eea; text-decoration: none; font-size: 14px; font-weight: 500; }
.breadcrumb a:hover { color: #8b9cf7; } .breadcrumb a:hover { color: #8b9cf7; }
.breadcrumb span { color: rgba(255,255,255,0.3); font-size: 13px; } .breadcrumb span { color: var(--text-muted); font-size: 13px; }
.breadcrumb .current { color: rgba(255,255,255,0.7); } .breadcrumb .current { color: var(--text-primary); font-weight: 600; }
.score-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 36px; } .score-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 36px; }
.score-card { .score-card {
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 16px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 16px;
padding: 20px; display: flex; flex-direction: column; gap: 8px; padding: 20px; display: flex; flex-direction: column; gap: 8px;
} }
.score-label { color: rgba(255,255,255,0.4); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } .score-label { color: var(--text-muted); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.score-value { color: #fff; font-size: 24px; font-weight: 700; } .score-value { font-size: 24px; font-weight: 700; color: var(--text-primary); }
.score-value.small { font-size: 14px; } .score-value.small { font-size: 14px; }
.score-value.good { color: #4ade80; } .score-value.good { color: var(--success); }
.score-value.avg { color: #fbbf24; } .score-value.avg { color: var(--warning); }
.score-value.poor { color: #f87171; } .score-value.poor { color: var(--danger); }
.section-title { color: #fff; font-size: 18px; font-weight: 600; margin: 0 0 20px; } .section-title { font-size: 18px; font-weight: 600; margin: 0 0 20px; color: var(--text-primary); }
.questions-list { display: flex; flex-direction: column; gap: 20px; } .questions-list { display: flex; flex-direction: column; gap: 20px; }
.question-card { .question-card {
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 16px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 16px;
padding: 24px; transition: all 0.2s; padding: 24px; transition: all 0.2s;
} }
.question-card.correct { border-left: 4px solid #4ade80; } .question-card.correct { border-left: 4px solid var(--success); }
.question-card.wrong { border-left: 4px solid #f87171; } .question-card.wrong { border-left: 4px solid var(--danger); }
.q-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; } .q-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.q-number { background: rgba(102,126,234,0.15); color: #667eea; padding: 4px 12px; border-radius: 8px; font-weight: 700; font-size: 13px; } .q-number { background: rgba(102,126,234,0.15); color: var(--accent-primary); padding: 4px 12px; border-radius: 8px; font-weight: 700; font-size: 13px; }
.q-type-badge { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.6); padding: 4px 10px; border-radius: 8px; font-size: 11px; font-weight: 600; text-transform: uppercase; } .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; }
.q-status { margin-left: auto; font-weight: 600; font-size: 13px; } .q-status { margin-left: auto; font-weight: 600; font-size: 13px; }
.q-status.correct { color: #4ade80; } .q-status.correct { color: var(--success); }
.q-status:not(.correct) { color: #f87171; } .q-status:not(.correct) { color: var(--danger); }
.q-text { color: #fff; font-size: 15px; line-height: 1.6; margin: 0 0 16px; } .q-text { font-size: 15px; line-height: 1.6; margin: 0 0 16px; color: var(--text-primary); }
.options-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .options-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.option { .option {
padding: 12px 16px; border-radius: 10px; font-size: 14px; color: rgba(255,255,255,0.7); padding: 12px 16px; border-radius: 10px; font-size: 14px; color: var(--text-primary);
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); background: var(--bg-input); border: 1px solid var(--border-color);
display: flex; align-items: center; gap: 8px; display: flex; align-items: center; gap: 8px;
} }
.option.correct-answer { background: rgba(74,222,128,0.1); border-color: rgba(74,222,128,0.3); color: #4ade80; } .option.correct-answer { background: rgba(74,222,128,0.1); border-color: rgba(74,222,128,0.3); color: var(--success); }
.option.wrong-answer { background: rgba(248,113,113,0.1); border-color: rgba(248,113,113,0.3); color: #f87171; } .option.wrong-answer { background: rgba(248,113,113,0.1); border-color: rgba(248,113,113,0.3); color: var(--danger); }
.option.student-answer:not(.wrong-answer) { background: rgba(74,222,128,0.1); border-color: rgba(74,222,128,0.3); color: #4ade80; } .option.student-answer:not(.wrong-answer) { background: rgba(74,222,128,0.1); border-color: rgba(74,222,128,0.3); color: var(--success); }
.option-marker { font-weight: 700; font-size: 16px; } .option-marker { font-weight: 700; font-size: 16px; }
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebar { width: 100%; position: relative; border-right: none; border-bottom: 1px solid rgba(255,255,255,0.06); } .page-container { padding: 24px 16px; }
.main-content { margin-left: 0; padding: 24px; }
.dashboard-layout { flex-direction: column; }
.options-grid { grid-template-columns: 1fr; } .options-grid { grid-template-columns: 1fr; }
.score-summary { grid-template-columns: 1fr 1fr; } .score-summary { grid-template-columns: 1fr 1fr; }
} }
<div class="dashboard-layout"> <div class="page-container animate-fade-in">
<aside class="sidebar">
<div class="sidebar-header">
<span class="logo-icon">📝</span><h2>QuizMaster</h2><span class="role-badge">Admin</span>
</div>
<nav class="sidebar-nav">
<a routerLink="/admin/dashboard" class="nav-item"><span class="nav-icon">🏠</span><span>Dashboard</span></a>
<a routerLink="/admin/users" class="nav-item active"><span class="nav-icon">👥</span><span>Users</span></a>
<a routerLink="/admin/generate-quiz" class="nav-item"><span class="nav-icon"></span><span>Generate Quiz</span></a>
</nav>
<div class="sidebar-footer">
<div class="user-info">
<div class="user-avatar">{{ authService.currentUser()?.name?.charAt(0) || 'A' }}</div>
<div class="user-details"><span class="user-name">{{ authService.currentUser()?.name }}</span><span class="user-email">{{ authService.currentUser()?.email }}</span></div>
</div>
<button class="logout-btn" (click)="logout()"><span>🚪</span> Logout</button>
</div>
</aside>
<main class="main-content">
@if (loading()) { @if (loading()) {
<div class="loading-state"><div class="loader"></div><p>Loading details...</p></div> <div class="loading-state"><div class="loader"></div><p>Loading details...</p></div>
} @else if (submission()) { } @else if (submission()) {
...@@ -76,5 +57,4 @@ ...@@ -76,5 +57,4 @@
} }
</div> </div>
} }
</main>
</div> </div>
.dashboard-layout { .page-container {
display: flex; padding: 32px 40px;
min-height: 100vh; max-width: 1400px;
background: var(--bg-secondary);
}
.sidebar {
width: 260px;
background: var(--bg-card);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 24px 16px;
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 10;
}
.sidebar-header {
display: flex;
align-items: center;
gap: 10px;
padding: 0 8px;
margin-bottom: 32px;
flex-wrap: wrap;
}
.sidebar-header .logo-icon {
font-size: 28px;
}
.sidebar-header h2 {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.role-badge {
background: linear-gradient(135deg, #667eea, #764ba2);
color: var(--text-primary);
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
text-transform: uppercase;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 12px;
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--text-primary);
}
.nav-item.active {
background: rgba(102, 126, 234, 0.15);
color: #667eea;
}
.nav-icon {
font-size: 18px;
}
.sidebar-footer {
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding-top: 16px;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
margin-bottom: 12px;
}
.user-avatar {
width: 38px;
height: 38px;
border-radius: 10px;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
font-weight: 700;
font-size: 16px;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
color: var(--text-primary);
font-size: 13px;
font-weight: 600;
}
.user-email {
color: rgba(255, 255, 255, 0.4);
font-size: 11px;
}
.logout-btn {
width: 100%;
padding: 10px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 10px;
color: #f87171;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: inherit;
}
.logout-btn:hover {
background: rgba(239, 68, 68, 0.2);
}
.main-content {
flex: 1;
margin-left: 0px;
padding: 24px 32px;
} }
.breadcrumb { .breadcrumb {
...@@ -262,7 +113,7 @@ ...@@ -262,7 +113,7 @@
.history-table-wrap { .history-table-wrap {
overflow-x: auto; overflow-x: auto;
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid var(--border-color);
} }
.history-table { .history-table {
...@@ -286,7 +137,7 @@ ...@@ -286,7 +137,7 @@
.history-table td { .history-table td {
padding: 16px 20px; padding: 16px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.06); border-top: 1px solid var(--border-color);
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;
} }
...@@ -296,12 +147,11 @@ ...@@ -296,12 +147,11 @@
} }
.history-table tbody tr:hover { .history-table tbody tr:hover {
background: rgba(255, 255, 255, 0.03); background: var(--bg-hover);
} }
.quiz-name { .quiz-name {
font-weight: 600; font-weight: 600;
color: #fff !important;
} }
.score-badge { .score-badge {
...@@ -316,7 +166,7 @@ ...@@ -316,7 +166,7 @@
.percent-bar { .percent-bar {
width: 80px; width: 80px;
height: 6px; height: 6px;
background: rgba(255, 255, 255, 0.1); background: var(--bg-input);
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
display: inline-block; display: inline-block;
...@@ -360,20 +210,8 @@ ...@@ -360,20 +210,8 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebar { .page-container {
width: 100%; padding: 24px 16px;
position: relative;
border-right: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.main-content {
margin-left: 0;
padding: 24px;
}
.dashboard-layout {
flex-direction: column;
} }
} }
......
<div class="page-container animate-fade-in">
<div class="content-wrapper">
<main class="main-content">
<div class="content-wrapper">
<div class="breadcrumb"> <div class="breadcrumb">
<a routerLink="/admin/users">← Back to Users</a> <a routerLink="/admin/users">← Back to Users</a>
</div> </div>
...@@ -70,6 +67,5 @@ ...@@ -70,6 +67,5 @@
</div> </div>
} }
} }
</div>
</div> </div>
</main> \ No newline at end of file
\ No newline at end of file
.dashboard-layout { .page-container {
display: flex; padding: 32px 40px;
min-height: 100vh; max-width: 1400px;
background: var(--bg-secondary);
}
.sidebar {
width: 260px;
background: var(--bg-card);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 24px 16px;
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 10;
}
.sidebar-header {
display: flex;
align-items: center;
gap: 10px;
padding: 0 8px;
margin-bottom: 32px;
flex-wrap: wrap;
}
.sidebar-header .logo-icon {
font-size: 28px;
}
.sidebar-header h2 {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.role-badge {
background: linear-gradient(135deg, #667eea, #764ba2);
color: var(--text-primary);
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
text-transform: uppercase;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 12px;
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--text-primary);
}
.nav-item.active {
background: rgba(102, 126, 234, 0.15);
color: var(--text-secondary);
}
.nav-icon {
font-size: 18px;
}
.sidebar-footer {
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding-top: 16px;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
margin-bottom: 12px;
}
.user-avatar {
width: 38px;
height: 38px;
border-radius: 10px;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
font-weight: 700;
font-size: 16px;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
color: var(--text-primary);
font-size: 13px;
font-weight: 600;
}
.user-email {
color: rgba(255, 255, 255, 0.4);
font-size: 11px;
}
.logout-btn {
width: 100%;
padding: 10px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 10px;
color: #f87171;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: inherit;
}
.logout-btn:hover {
background: rgba(239, 68, 68, 0.2);
}
.main-content {
flex: 1;
margin-left: 0px;
padding: 40px;
} }
.page-header { .page-header {
...@@ -337,21 +188,33 @@ color: var(--text-secondary); ...@@ -337,21 +188,33 @@ color: var(--text-secondary);
transform: translateX(4px); transform: translateX(4px);
} }
@media (max-width: 768px) { .action-btn {
.sidebar { background: transparent;
width: 100%; border: none;
position: relative; border-radius: 8px;
border-right: 1px solid var(--border-color); width: 32px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06); height: 32px;
} display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
cursor: pointer;
transition: all 0.2s;
margin-left: 8px;
}
.main-content { .action-btn .material-symbols-rounded {
margin-left: 0; font-size: 20px;
padding: 24px; }
}
.text-danger:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.dashboard-layout { @media (max-width: 768px) {
flex-direction: column; .page-container {
padding: 24px 16px;
} }
.users-grid { .users-grid {
......
<div class="dashboard-layout"> <div class="page-container animate-fade-in">
<aside class="sidebar">
<div class="sidebar-header">
<span class="logo-icon">📝</span>
<h2>QuizMaster</h2>
<span class="role-badge">Admin</span>
</div>
<nav class="sidebar-nav">
<a routerLink="/admin/dashboard" class="nav-item">
<span class="nav-icon">🏠</span><span>Dashboard</span>
</a>
<a routerLink="/admin/users" class="nav-item active">
<span class="nav-icon">👥</span><span>Users</span>
</a>
<a routerLink="/admin/generate-quiz" class="nav-item">
<span class="nav-icon"></span><span>Generate Quiz</span>
</a>
</nav>
<div class="sidebar-footer">
<div class="user-info">
<div class="user-avatar">{{ authService.currentUser()?.name?.charAt(0) || 'A' }}</div>
<div class="user-details">
<span class="user-name">{{ authService.currentUser()?.name }}</span>
<span class="user-email">{{ authService.currentUser()?.email }}</span>
</div>
</div>
<button class="logout-btn" (click)="logout()"><span>🚪</span> Logout</button>
</div>
</aside>
<main class="main-content">
<div class="page-header"> <div class="page-header">
<h1>Student Users</h1> <h1>Student Users</h1>
<p>View and manage registered students</p> <p>View and manage registered students</p>
...@@ -61,10 +31,14 @@ ...@@ -61,10 +31,14 @@
<span class="user-joined">Joined {{ user.createdAt | date:'mediumDate' }}</span> <span class="user-joined">Joined {{ user.createdAt | date:'mediumDate' }}</span>
</div> </div>
<div class="status-dot" [class.online]="user.isLoggedIn"></div> <div class="status-dot" [class.online]="user.isLoggedIn"></div>
<button class="action-btn text-danger ml-auto" (click)="$event.preventDefault(); $event.stopPropagation(); deleteUser(user._id)" title="Delete Student">
<span class="material-symbols-rounded">delete</span>
</button>
<span class="view-arrow"></span> <span class="view-arrow"></span>
</a> </a>
} }
</div> </div>
} }
</main>
</div> </div>
...@@ -24,10 +24,11 @@ export class AdminUsersComponent implements OnInit { ...@@ -24,10 +24,11 @@ export class AdminUsersComponent implements OnInit {
loadUsers(): void { loadUsers(): void {
this.loading.set(true); this.loading.set(true);
const request = this.showAll() ? this.quizService.getUsers() : this.quizService.getLoggedInUsers(); const request = this.showAll() ? this.quizService.getUsers('candidate') : this.quizService.getLoggedInUsers();
request.subscribe({ request.subscribe({
next: (res) => { next: (res) => {
this.users.set(res.users); const filtered = res.users.filter((u: any) => u.role === 'candidate');
this.users.set(filtered);
this.loading.set(false); this.loading.set(false);
}, },
error: () => this.loading.set(false) error: () => this.loading.set(false)
...@@ -39,6 +40,20 @@ export class AdminUsersComponent implements OnInit { ...@@ -39,6 +40,20 @@ export class AdminUsersComponent implements OnInit {
this.loadUsers(); this.loadUsers();
} }
deleteUser(userId: string): void {
if (confirm('Are you sure you want to permanently delete this student user?')) {
this.quizService.deleteUser(userId).subscribe({
next: () => {
this.loadUsers();
},
error: (err) => {
console.error('Failed to delete user', err);
alert('Failed to delete student user.');
}
});
}
}
logout(): void { logout(): void {
this.authService.logout(); this.authService.logout();
} }
......
...@@ -76,24 +76,7 @@ ...@@ -76,24 +76,7 @@
</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">
......
import { Component, signal, OnInit } from '@angular/core'; import { Component, signal } 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,53 +11,39 @@ import { AuthService } from '../../services/auth.service'; ...@@ -11,53 +11,39 @@ import { AuthService } from '../../services/auth.service';
templateUrl: './register.html', templateUrl: './register.html',
styleUrl: './register.css' styleUrl: './register.css'
}) })
export class RegisterComponent implements OnInit { export class RegisterComponent {
name = ''; name = '';
email = ''; email = '';
password = ''; password = '';
confirmPassword = ''; confirmPassword = '';
group = '';
availableGroups = signal<string[]>([]); loading = signal(false);
error = signal<string>(''); error = signal('');
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');
return; return;
} }
if (this.password !== this.confirmPassword) { if (this.password !== this.confirmPassword) {
this.error.set('Passwords do not match'); this.error.set('Passwords do not match');
return; return;
} }
if (this.password.length < 6) {
this.error.set('Password must be at least 6 characters');
return;
}
this.loading.set(true); this.loading.set(true);
this.error.set(''); this.error.set('');
this.authService.register(this.name, this.email, this.password, this.group).subscribe({ this.authService.register(this.name, this.email, this.password, 'General').subscribe({
next: () => { next: () => {
this.loading.set(false); this.loading.set(false);
alert('Registration successful! Please sign in.');
this.router.navigate(['/login']); this.router.navigate(['/login']);
}, },
error: (err) => { error: (err) => {
this.error.set(err.error?.message || 'Registration failed');
this.loading.set(false); this.loading.set(false);
this.error.set(err.error?.message || 'Registration failed. Please try again.');
} }
}); });
} }
......
...@@ -35,6 +35,10 @@ ...@@ -35,6 +35,10 @@
return this.http.delete(`${this.adminUrl}/users/${userId}`); return this.http.delete(`${this.adminUrl}/users/${userId}`);
} }
editUser(userId: string, data: { name?: string; email?: string; password?: string }): Observable<any> {
return this.http.put(`${this.adminUrl}/users/${userId}`, data);
}
getUserHistory(userId: string): Observable<any> { getUserHistory(userId: string): Observable<any> {
return this.http.get(`${this.adminUrl}/users/${userId}/history`); return this.http.get(`${this.adminUrl}/users/${userId}/history`);
} }
...@@ -179,4 +183,8 @@ ...@@ -179,4 +183,8 @@
deleteGroup(name: string): Observable<any> { deleteGroup(name: string): Observable<any> {
return this.http.delete(`${this.getBaseUrl()}/groups/${encodeURIComponent(name)}`); return this.http.delete(`${this.getBaseUrl()}/groups/${encodeURIComponent(name)}`);
} }
assignUserGroup(userId: string, groupName: string): Observable<any> {
return this.http.put(`${this.getBaseUrl()}/users/${userId}/group`, { group: groupName });
}
} }
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