Skip to content

Commit f048a8b

Browse files
feat(forms): add student search and multi-select support for staff grant extension form
This commit adds the Student Search and Select feature to the Staff Grant Extension form, enhancing the form's usability by allowing staff to search for individual students and select multiple students for bulk extension grants. This update integrates directly with the existing extension form backend, improving the overall user experience for staff handling large classes. Changes include: - Added student search input for filtering student lists. - Implemented multi-student selection support for bulk operations. - Improved UI components for efficient student selection. - Integrated with the existing form backend for seamless data flow. References: - Related to JoeMacl's Grant Extension Form PR thoth-tech#330 - Addresses requirements for bulk extension management.
1 parent 42b2f2f commit f048a8b

File tree

2 files changed

+159
-33
lines changed

2 files changed

+159
-33
lines changed
Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,71 @@
11
<h2 mat-dialog-title>Grant Extension</h2>
22
<form [formGroup]="grantExtensionForm" (ngSubmit)="submitForm()">
33

4-
54
<mat-dialog-content [formGroup]="grantExtensionForm">
6-
<!-- Student Selection -->
7-
<mat-form-field appearance="fill" class="w-full mb-4">
8-
<mat-label>Student</mat-label>
9-
<mat-select formControlName="student" required>
10-
<mat-option value="" disabled>Select a student</mat-option>
11-
<mat-option *ngFor="let student of students" [value]="student.id">
12-
{{ student.name }}
13-
</mat-option>
14-
</mat-select>
15-
<mat-error *ngIf="grantExtensionForm.get('student')?.touched && grantExtensionForm.get('student')?.invalid">
16-
Please select a student.
5+
<!-- Student Selection with Search -->
6+
<div class="w-full mb-4">
7+
<mat-form-field appearance="fill" class="w-full">
8+
<mat-label>Search Students</mat-label>
9+
<input matInput
10+
[(ngModel)]="searchQuery"
11+
(ngModelChange)="filterStudents()"
12+
(focus)="showStudentList = true"
13+
[ngModelOptions]="{standalone: true}"
14+
placeholder="Type to search students...">
15+
</mat-form-field>
16+
17+
<!-- Select All Option -->
18+
<div class="flex items-center px-2 mt-0" *ngIf="showStudentList">
19+
<mat-checkbox
20+
#selectAllBox
21+
[checked]="selectedStudents.length === filteredStudents.length"
22+
(change)="toggleSelectAll()"
23+
(mouseup)="selectAllBox.blur()"
24+
color="primary"
25+
disableRipple>
26+
Select All
27+
</mat-checkbox>
28+
</div>
29+
30+
<!-- Student List -->
31+
<div *ngIf="showStudentList"
32+
class="student-list-container mt-4 mb-8"
33+
style="max-height: 200px; min-height: 100px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; scrollbar-width: thin; scrollbar-color: #888 #f1f1f1;">
34+
<div *ngFor="let student of filteredStudents"
35+
class="flex items-center px-4 py-2 cursor-pointer transition-colors duration-200"
36+
[class.hover:bg-gray-100]="!isStudentSelected(student.id)"
37+
[class.bg-blue-100]="isStudentSelected(student.id)"
38+
(click)="toggleStudent(student.id)"
39+
(keydown)="handleStudentKeydown($event, student.id)"
40+
tabindex="0"
41+
role="option"
42+
[attr.aria-selected]="isStudentSelected(student.id)">
43+
{{ student.name }} ({{ student.id }})
44+
</div>
45+
</div>
46+
47+
<mat-error *ngIf="grantExtensionForm.get('students')?.touched && grantExtensionForm.get('students')?.invalid">
48+
Please select at least one student.
1749
</mat-error>
18-
</mat-form-field>
50+
</div>
1951

2052
<!-- Extension Duration -->
21-
<div class="mb-4">
22-
<label for="extension" class="block text-xl font-semibold text-gray-900">
23-
Extension Duration: <strong>{{ grantExtensionForm.get('extension')?.value }}</strong> day(s)
24-
</label>
53+
<div class="mb-8">
54+
<label for="extension" class="block text-xl font-semibold text-gray-900 mb-4">
55+
Extension Duration: <strong>{{ grantExtensionForm.get('extension')?.value }}</strong> day(s)
56+
</label>
2557

26-
<mat-slider
27-
min="1"
28-
max="30"
29-
step="1"
30-
tickInterval="5"
31-
thumbLabel
32-
class="w-full"
33-
style="width: 100%; max-width: 600px; min-width: 300px;"
34-
>
35-
<input matSliderThumb formControlName="extension" />
36-
</mat-slider>
58+
<mat-slider
59+
min="1"
60+
max="30"
61+
step="1"
62+
tickInterval="5"
63+
thumbLabel
64+
class="w-full"
65+
style="width: 100%; max-width: 600px; min-width: 300px;"
66+
>
67+
<input matSliderThumb formControlName="extension" />
68+
</mat-slider>
3769
</div>
3870

3971
<!-- Reason Field -->
@@ -64,4 +96,5 @@ <h2 mat-dialog-title>Grant Extension</h2>
6496
Grant Extension
6597
</button>
6698
</mat-dialog-actions>
99+
</form>
67100

src/app/admin/modals/grant-extension-form/grant-extension-form.component.ts

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { MatButtonModule } from '@angular/material/button';
99
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
1010
import { MatSnackBar } from '@angular/material/snack-bar';
1111
import { ExtensionService } from 'src/app/api/services/extension.service';
12+
import { FormsModule } from '@angular/forms';
13+
import { MatCheckboxModule } from '@angular/material/checkbox';
1214

1315
@Component({
1416
selector: 'f-grant-extension-form',
@@ -21,13 +23,18 @@ import { ExtensionService } from 'src/app/api/services/extension.service';
2123
MatInputModule,
2224
MatSliderModule,
2325
MatButtonModule,
24-
MatDialogModule
26+
MatDialogModule,
27+
FormsModule,
28+
MatCheckboxModule
2529
],
2630
templateUrl: './grant-extension-form.component.html'
2731
})
2832
export class GrantExtensionFormComponent implements OnInit {
2933
grantExtensionForm!: FormGroup;
3034
isSubmitting = false;
35+
searchQuery = '';
36+
selectedStudents: number[] = [];
37+
showStudentList = false;
3138

3239
// Temporary values will be replaced with dynamic context
3340
unitId = 1;
@@ -37,8 +44,26 @@ export class GrantExtensionFormComponent implements OnInit {
3744
students = [
3845
{ id: 1, name: 'Joe M' },
3946
{ id: 2, name: 'Sahiru W' },
40-
{ id: 3, name: 'Samindi M' }
47+
{ id: 3, name: 'Samindi M' },
48+
{ id: 4, name: 'Student 4' },
49+
{ id: 5, name: 'Student 5' },
50+
{ id: 6, name: 'Student 6' },
51+
{ id: 7, name: 'Student 7' },
52+
{ id: 8, name: 'Student 8' },
53+
{ id: 9, name: 'Student 9' },
54+
{ id: 10, name: 'Student 10' },
55+
{ id: 11, name: 'Student 11' },
56+
{ id: 12, name: 'Student 12' },
57+
{ id: 13, name: 'Student 13' },
58+
{ id: 14, name: 'Student 14' },
59+
{ id: 15, name: 'Student 15' },
60+
{ id: 16, name: 'Student 16' },
61+
{ id: 17, name: 'Student 17' },
62+
{ id: 18, name: 'Student 18' },
63+
{ id: 19, name: 'Student 19' },
64+
{ id: 20, name: 'Student 20' }
4165
];
66+
filteredStudents = this.students;
4267

4368
constructor(
4469
private fb: FormBuilder,
@@ -50,13 +75,81 @@ export class GrantExtensionFormComponent implements OnInit {
5075
// Initialize the reactive form with validators for each field
5176
ngOnInit(): void {
5277
this.grantExtensionForm = this.fb.group({
53-
student: ['', Validators.required],
78+
students: [[], Validators.required],
5479
extension: [1, [Validators.required, Validators.min(1)]],
5580
reason: ['', Validators.required],
5681
notes: ['']
5782
});
5883
}
5984

85+
/**
86+
* Filters the student list based on the search query.
87+
* Matches against both student name and ID.
88+
* If no query is provided, shows all students.
89+
*/
90+
filterStudents(): void {
91+
if (!this.searchQuery) {
92+
this.filteredStudents = this.students;
93+
} else {
94+
const query = this.searchQuery.toLowerCase();
95+
this.filteredStudents = this.students.filter(student =>
96+
student.name.toLowerCase().includes(query) ||
97+
student.id.toString().includes(query)
98+
);
99+
}
100+
}
101+
102+
/**
103+
* Toggles the selection state of a student.
104+
* Adds the student to selectedStudents if not already selected,
105+
* removes them if already selected.
106+
* Updates the form control value accordingly.
107+
*/
108+
toggleStudent(studentId: number): void {
109+
const index = this.selectedStudents.indexOf(studentId);
110+
if (index === -1) {
111+
this.selectedStudents.push(studentId);
112+
} else {
113+
this.selectedStudents.splice(index, 1);
114+
}
115+
this.grantExtensionForm.patchValue({ students: this.selectedStudents });
116+
}
117+
118+
/**
119+
* Handles keyboard navigation for student selection.
120+
* Allows selection/deselection using Enter or Space keys.
121+
* Prevents default browser behavior for these keys.
122+
*/
123+
handleStudentKeydown(event: KeyboardEvent, studentId: number): void {
124+
if (event.key === 'Enter' || event.key === ' ') {
125+
event.preventDefault();
126+
this.toggleStudent(studentId);
127+
}
128+
}
129+
130+
/**
131+
* Toggles selection of all currently filtered students.
132+
* If all filtered students are selected, deselects all.
133+
* If not all are selected, selects all filtered students.
134+
* Updates the form control value accordingly.
135+
*/
136+
toggleSelectAll(): void {
137+
if (this.selectedStudents.length === this.filteredStudents.length) {
138+
this.selectedStudents = [];
139+
} else {
140+
this.selectedStudents = this.filteredStudents.map(student => student.id);
141+
}
142+
this.grantExtensionForm.patchValue({ students: this.selectedStudents });
143+
}
144+
145+
/**
146+
* Checks if a student is currently selected.
147+
* Used for UI state management and visual feedback.
148+
*/
149+
isStudentSelected(studentId: number): boolean {
150+
return this.selectedStudents.includes(studentId);
151+
}
152+
60153
// Handles form submission.
61154
// Builds the payload and sends it to the backend via the ExtensionService.
62155
// Displays a success or error message and closes the dialog on success.
@@ -68,10 +161,10 @@ export class GrantExtensionFormComponent implements OnInit {
68161

69162
this.isSubmitting = true;
70163

71-
const { student, extension, reason, notes } = this.grantExtensionForm.value;
164+
const { students, extension, reason, notes } = this.grantExtensionForm.value;
72165
const unitId = 1; // temporary value
73166
const payload = {
74-
student_ids: [student],
167+
student_ids: students,
75168
task_definition_id: 25,
76169
weeks_requested: extension,
77170
comment: reason,

0 commit comments

Comments
 (0)