Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b4eced9
feat: add notification component to display in-system notifications f…
samindiii May 18, 2025
d3efbd1
feat: update header component to include the notification panel
samindiii May 18, 2025
3ef9141
feat: add Grant Extension form component
JoeMacl Mar 29, 2025
9f6e603
feat: updated grant extension form with validation and styling
JoeMacl Apr 4, 2025
ce815da
feat: update progress on grant extension form
JoeMacl Apr 12, 2025
dfec36e
feat: implement Grant Extension form as Material dialog with validati…
JoeMacl Apr 13, 2025
ccad39a
feat: grant extension integration with backend
JoeMacl May 2, 2025
2f3c8cc
feat: grant extension integration with backend
JoeMacl May 5, 2025
929af96
feat: grant extension integration with backend
JoeMacl May 5, 2025
d240e00
chore: remove unnecessary changes
JoeMacl May 6, 2025
dd8bc80
chore: remove unnecessary changes
JoeMacl May 6, 2025
29d2295
fix: prevent Cancel button from submitting form
JoeMacl May 17, 2025
83a2a91
feat(forms): add student search and multi-select support for staff gr…
SahiruWithanage May 19, 2025
a51f596
chore(comments): make appropriate changes to the comments
SahiruWithanage May 19, 2025
a09e431
fix(ui): resolve checkbox blur error in extension form
SahiruWithanage May 20, 2025
56f8b22
Delete app/api/staff_grant_extension_api.rb
SahiruWithanage May 31, 2025
d11fdee
Delete app/services/extension_service.rb
SahiruWithanage May 31, 2025
281c33c
feat: add staff grant extension page with embedded form
SahiruWithanage Aug 15, 2025
2d1102e
refactor(forms): add character limits and clean up grant extension forms
SahiruWithanage Aug 31, 2025
3f66d19
fix: correct weeks vs days mismatch
samindiii Sep 7, 2025
ce07b0e
feat: remove hardcoded form data and add input properties
samindiii Sep 7, 2025
01668b2
feat: Staff Grant Extension routing (WIP)
returnMarcco Aug 26, 2025
8bef623
feat: add UIRouter declaration + conditional rendering
returnMarcco Sep 1, 2025
6962e18
chore: remove test code
returnMarcco Sep 2, 2025
2ed390c
fix: replace placeholder with Staff Grant Extension component
samindiii Sep 7, 2025
7753a23
feat: load students, handle task selection, and pass data to extensio…
rashi-agrawal29 Sep 10, 2025
72785f9
refactor(forms): remove unused notes field from staff grant extension
SahiruWithanage Sep 11, 2025
4724aab
fix: remove unused code and update tasks for selected unit
rashi-agrawal29 Sep 11, 2025
c9ff4d7
feat: add summary component and models for grant extension response
samindiii Sep 14, 2025
f7221ec
feat: integrate summary component into staff grant extension UI
samindiii Sep 14, 2025
66c148a
refactor: update form to emit structured summary payload
samindiii Sep 14, 2025
b87097d
chore: declare new summary component in module
samindiii Sep 14, 2025
9ab3cd6
fix: unlock form on errors and refresh extension summary
samindiii Sep 14, 2025
c4b9c42
fix: click outside to exit notification and make card bigger
samindiii Sep 14, 2025
345e26e
fix: improve error message extraction for grant extension failures
SahiruWithanage Sep 14, 2025
b35b3f1
refactor: remove debugging console logs from error handling
SahiruWithanage Sep 14, 2025
14cfc83
feat(SGE): wired up the feature to v10
SteveDala Jan 21, 2026
43bb4f2
docs: remove console debugging
SteveDala Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/app/api/services/extension.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { UserService } from 'src/app/api/models/doubtfire-model';

interface GrantExtensionPayload {
student_ids: number[];
task_definition_id: number;
weeks_requested: number;
comment: string;
}

@Injectable({
providedIn: 'root'
})
export class ExtensionService {
constructor(
private http: HttpClient,
private userService: UserService
) {}

grantExtension(unitId: number, payload: GrantExtensionPayload): Observable<any> {
const authToken = this.userService.currentUser?.authenticationToken ?? '';
const username = this.userService.currentUser?.username ?? '';

const headers = new HttpHeaders({
'Auth-Token': authToken,
'Username': username
});

return this.http.post(
`/api/units/${unitId}/staff-grant-extension`,
payload,
{ headers }
);
}
}
5 changes: 5 additions & 0 deletions src/app/common/header/header.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
<mat-icon>qr_code_scanner</mat-icon>
</button>
}

@if (currentUser.role === 'Student') {
<f-notifications-button></f-notifications-button>
}

@if (currentUser.role === 'Admin' || currentUser.role === 'Convenor') {
<button
#menuState="matMenuTrigger"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
.notification-panel {
position: absolute;
top: 60px;
right: 20px;
width: 450px;
max-height: 75vh;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
z-index: 1000;
overflow-y: auto;
padding: 16px;
scrollbar-width: thin;
scrollbar-color: #ccc transparent;
}

.notification-panel::-webkit-scrollbar {
width: 6px;
}
.notification-panel::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 10px;
}

.notification-panel mat-list-item {
border: 1px solid #eee;
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
background: #fdfdfd;
font-size: 15px;
line-height: 1.5;
}

.notification-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 10px;
}

.notification-content span {
flex: 1;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
line-height: 1.5;
font-size: 11px;
}

.notification-content button {
flex-shrink: 0;
width: 36px;
height: 36px;
}

.notification-badge {
position: absolute;
top: 4px;
right: 4px;
background: red;
color: white;
font-size: 12px;
font-weight: bold;
border-radius: 50%;
padding: 4px 7px;
line-height: 1;
min-width: 18px;
text-align: center;
}

.delete-all-container {
display: flex;
justify-content: center;
margin-top: 12px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<button mat-icon-button (click)="toggleNotifications()" aria-label="Notifications" class="notification-button">
<mat-icon>notifications</mat-icon>
<span *ngIf="notifications.length > 0" class="notification-badge">{{ notifications.length }}</span>
</button>

<!-- Notification List Panel -->
<div *ngIf="showNotifications" class="notification-panel" #panel>
<mat-list>
<mat-list-item *ngFor="let note of notifications">
<div class="notification-content">
<span class="mat-body-2">{{ note.message }}</span>
<button mat-icon-button aria-label="Dismiss notification" (click)="dismissNotification(note.id)">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-list-item>

<!-- If no notifications -->
<mat-list-item *ngIf="notifications.length === 0">
<span class="mat-body-2">No notifications</span>
</mat-list-item>
</mat-list>

<div *ngIf="notifications.length > 0" class="delete-all-container">
<button mat-button color="warn" (click)="deleteAllNotifications()">Delete All</button>
</div>

<div class="close-container">
<button mat-button (click)="toggleNotifications()">Close</button>
</div>
</div>




Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { NotificationsButtonComponent } from './notifications-button.component';

describe('NotificationsButtonComponent', () => {
let component: NotificationsButtonComponent;
let fixture: ComponentFixture<NotificationsButtonComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NotificationsButtonComponent]
})
.compileComponents();

fixture = TestBed.createComponent(NotificationsButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Component, OnInit, ElementRef, HostListener } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AppInjector } from 'src/app/app-injector';
import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants';

interface Notification {
id: number;
message: string;
}

@Component({
selector: 'f-notifications-button',
templateUrl: './notifications-button.component.html',
styleUrls: ['./notifications-button.component.css']
})
export class NotificationsButtonComponent implements OnInit {
showNotifications = false;
notifications: Notification[] = [];
private readonly API_URL = AppInjector.get(DoubtfireConstants).API_URL;

constructor(private http: HttpClient, private eRef: ElementRef) {}

ngOnInit() {
this.loadNotifications();
}

toggleNotifications() {
this.showNotifications = !this.showNotifications;
}

loadNotifications() {
this.http.get<Notification[]>(`${this.API_URL}/notifications`).subscribe({
next: data => this.notifications = data,
error: err => console.error('Error loading notifications', err)
});
}

dismissNotification(notificationId: number) {
this.http.delete(`${this.API_URL}/notifications/${notificationId}`).subscribe({
next: () => {
this.notifications = this.notifications.filter(note => note.id !== notificationId);
},
error: err => console.error('Error deleting notification', err)
});
}

deleteAllNotifications() {
this.http.delete(`${this.API_URL}/notifications`).subscribe({
next: () => {
this.notifications = [];
},
error: err => console.error('Error deleting all notifications', err)
});
}

@HostListener('document:click', ['$event'])
clickOutside(event: Event) {
if (this.showNotifications && !this.eRef.nativeElement.contains(event.target)) {
this.showNotifications = false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@
@if (
unitRole.role === 'Convenor' || unitRole.role === 'Admin' || unitRole.role === 'Auditor'
) {
<button uiSref="units/staff_grant_extension" [uiParams]="{unitId: unitRole.unit.id}" mat-menu-item>
<mat-icon aria-label="Staff Grant Extension" fontIcon="schedule"></mat-icon>Staff Grant Extension
</button>
}
@if (unitRole.role === 'Convenor' || unitRole.role === 'Admin' || unitRole.role === 'Auditor') {
<button uiSref="units/admin" [uiParams]="{unitId: unitRole.unit.id}" mat-menu-item>
<mat-icon aria-label="Unit Administration" fontIcon="admin_panel_settings"></mat-icon>
Administration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class TaskDropdownComponent {
'Tutorial List': 'Tutorials',
'Unit Administration': 'Admin',
'Unit Analytics': 'Analytics',
'Staff Grant Extension': 'Extension',
};

taskDropdownData: { title: string; target: string; visible: any }[];
Expand Down
12 changes: 12 additions & 0 deletions src/app/doubtfire-angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/direc
import {OverseerScriptEditorModalComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component';
import {CodeEditorModule} from '@ngstack/code-editor';
import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component';
import {NotificationsButtonComponent} from './common/header/notifications-button/notifications-button.component';

// See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036
const MY_DATE_FORMAT = {
Expand All @@ -310,6 +311,11 @@ const MY_DATE_FORMAT = {
monthYearA11yLabel: 'MMMM yyyy',
},
};
// import {UnitStudentEnrolmentModalComponent} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component';
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
import {StaffGrantExtensionComponent} from './units/states/tasks/staff-grant-extension/staff-grant-extension.component';
import {StaffGrantExtensionFormComponent} from './units/states/tasks/staff-grant-extension/grant-extension-form/grant-extension-form.component';
import {StaffGrantExtensionSummaryComponent} from './units/states/tasks/staff-grant-extension/staff-grant-extension-summary/staff-grant-extension-summary.component';

@NgModule({
// Components we declare
Expand Down Expand Up @@ -452,6 +458,10 @@ const MY_DATE_FORMAT = {
AnalyticsTutorTimesComponent,
PortfolioIncludedTasksComponent,
OverseerScriptEditorModalComponent,
NotificationsButtonComponent,
StaffGrantExtensionComponent,
StaffGrantExtensionSummaryComponent,
StaffTaskListComponent,
],
providers: [
// Services we provide
Expand Down Expand Up @@ -541,6 +551,7 @@ const MY_DATE_FORMAT = {
LtiService,
TaskPrerequisiteService,
MarkingSessionService,
provideAnimationsAsync(),
],
imports: [
FlexLayoutModule,
Expand Down Expand Up @@ -594,6 +605,7 @@ const MY_DATE_FORMAT = {
EmojiModule,
PdfViewerModule,
LottieComponent,
StaffGrantExtensionFormComponent,
UIRouterUpgradeModule.forRoot({states: doubtfireStates}),
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
Expand Down
43 changes: 43 additions & 0 deletions src/app/doubtfire.states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,23 @@ import {TeachingPeriodListComponent} from './admin/states/teaching-periods/teach
import {AcceptEulaComponent} from './eula/accept-eula/accept-eula.component';
import {FUsersComponent} from './admin/states/f-users/f-users.component';
import {FUnitsComponent} from './admin/states/f-units/f-units.component';
// import {UnauthorisedComponent} from './errors/states/unauthorised/unauthorised.component';
import {StaffGrantExtensionComponent} from './units/states/tasks/staff-grant-extension/staff-grant-extension.component';
// import {ProjectDashboardComponent} from './projects/states/dashboard/project-dashboard/project-dashboard.component';
// import {UnitRootState} from './units/unit-root-state.component';
// import {ProjectRootState} from './projects/states/project-root-state.component';
// import {TaskViewerState} from './units/task-viewer/task-viewer-state.component';
import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component';
import {TutorDiscussionComponent} from './projects/states/tutor-discussion/tutor-discussion.component';
import {SuccessCloseComponent} from './common/success-close/success-close.component';
import {ProjectPlanComponent} from './projects/states/plan/project-plan.component';
import {JplagReportViewerComponent} from './projects/states/jplag/jplag-report-viewer.component';
import {LtiDashboardComponent} from './home/states/lti-dashboard/lti-dashboard.component';
import {LtiUnitLinkComponent} from './home/states/lti-unit-link/lti-unit-link.component';
import {Ng2ViewDeclaration} from '@uirouter/angular';
// import {GrantExtensionFormComponent} from './admin/modals/grant-extension-form/grant-extension-form.component';
import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; // Todo @SGE team: Replace with SGE component

/*
* Use this file to store any states that are sourced by angular components.
*/
Expand Down Expand Up @@ -571,6 +581,32 @@ const LtiUnitLinkState: NgHybridStateDeclaration = {
},
};

/**
* Define the Staff Grant Extension state.
*/
const StaffGrantExtensionState: NgHybridStateDeclaration = { // Todo @Jason: Navigating to this route makes the `TaskDropdown` disappear - fix
name: 'units/staff_grant_extension',
url: '/units/:unitId/staff_grant_extension',
resolve: {
unitId: [
'$stateParams',
function ($stateParams) {
return $stateParams.unitId;
},
],
},
views: {
main: {
component: StaffGrantExtensionComponent,
},
},
data: {
task: 'Staff Grant Extension',
pageTitle: 'Staff Grant Extension',
roleWhitelist: ['Tutor', 'Convenor', 'Admin'],
},
};

/**
* Export the list of states we have created in angular
*/
Expand All @@ -596,4 +632,11 @@ export const doubtfireStates = [
LtiDashboardState,
LtiUnitLinkState,
TutorAttendance,
// GrantExtensionState,
// UnauthoriedState,
// ProjectRootState,
// ProjectDashboardState,
// UnitRootState,
// TaskViewerState,
StaffGrantExtensionState,
];
Loading