Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions src/app/api/services/peer-progress.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Injectable } from '@angular/core';
import { Project } from 'src/app/api/models/doubtfire-model';

export interface PeerProgressSummary {
cohortAverage: number;
yourProgress: number;
peersAhead: number;
strongestProgress: number;
totalPeers: number;
yourAlias: string;
}

export interface AnonymizedPeerProgress {
alias: string;
progress: number;
bandLabel: string;
isCurrentStudent: boolean;
}

@Injectable({
providedIn: 'root',
})
export class PeerProgressService {
private readonly sampleProgress = [92, 88, 84, 81, 78, 75, 72, 69];

getSummary(project?: Project): PeerProgressSummary {
const peers = this.getAnonymizedPeers(project);
const currentPeer = peers.find((peer) => peer.isCurrentStudent) ?? peers[0];
const strongestProgress = Math.max(...peers.map((peer) => peer.progress));
const cohortAverage = Math.round(
peers.reduce((total, peer) => total + peer.progress, 0) / peers.length,
);

return {
cohortAverage,
yourProgress: currentPeer.progress,
peersAhead: peers.filter((peer) => peer.progress > currentPeer.progress).length,
strongestProgress,
totalPeers: peers.length,
yourAlias: currentPeer.alias,
};
}

getAnonymizedPeers(project?: Project): AnonymizedPeerProgress[] {
const offset = (project?.id ?? 0) % this.sampleProgress.length;
const currentIndex = offset % this.sampleProgress.length;

return this.sampleProgress.map((_, index) => {
const progress = this.sampleProgress[(index + offset) % this.sampleProgress.length];

return {
alias: `Peer ${String(index + 1).padStart(2, '0')}`,
progress,
bandLabel: this.toBandLabel(progress),
isCurrentStudent: index === currentIndex,
};
});
}

private toBandLabel(progress: number): string {
if (progress >= 85) return 'Leading';
if (progress >= 75) return 'On Track';
if (progress >= 65) return 'Building';
return 'Needs Support';
}
}
24 changes: 14 additions & 10 deletions src/app/common/header/task-dropdown/task-dropdown.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@
<mat-icon aria-label="Tutorial List icon" fontIcon="meeting_room"></mat-icon> Tutorial
List
</button>
<mat-divider></mat-divider>
<button
uiSref="projects/peer-progress"
[uiParams]="{projectId: currentProject.id}"
mat-menu-item
>
<mat-icon aria-label="Peer Progress icon" fontIcon="insights"></mat-icon>
Peer Progress
</button>
}
@if (unitRole && currentView === 'UNIT') {
<!-- <p class="task-dropdown-heading">Tasks</p> -->
Expand All @@ -114,11 +123,7 @@
</button>
<mat-divider></mat-divider>
<!-- <p class="task-dropdown-heading">Students</p> -->
<button
uiSref="units/students/list"
[uiParams]="{unitId: unitRole.unit.id}"
mat-menu-item
>
<button uiSref="units/students/list" [uiParams]="{unitId: unitRole.unit.id}" mat-menu-item>
<mat-icon aria-label="Students list" fontIcon="group"></mat-icon> Students
</button>
<button
Expand All @@ -133,18 +138,17 @@
[uiParams]="{unitId: unitRole.unit.id}"
mat-menu-item
>
<mat-icon
aria-label="Student portfolios icon"
fontIcon="collections_bookmark"
></mat-icon>
<mat-icon aria-label="Student portfolios icon" fontIcon="collections_bookmark"></mat-icon>
Portfolios
</button>
<mat-divider></mat-divider>
<!-- <p class="task-dropdown-heading">Unit</p> -->
<button uiSref="units/analytics" [uiParams]="{unitId: unitRole.unit.id}" mat-menu-item>
<mat-icon aria-label="Unit Analytics icon" fontIcon="insights"></mat-icon>Analytics
</button>
@if (unitRole.role === 'Convenor' || unitRole.role === 'Admin' || unitRole.role === 'Auditor') {
@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
4 changes: 4 additions & 0 deletions src/app/doubtfire-angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ import {ProjectProgressBarComponent} from './common/project-progress-bar/project
import {TeachingPeriodListComponent} from './admin/states/teaching-periods/teaching-period-list/teaching-period-list.component';
import {FChipComponent} from './common/f-chip/f-chip.component';
import {TaskSimilarityViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component';
import {PeerProgressComponent} from './projects/states/peer-progress/peer-progress.component';
import {PeersAnonymizedComponent} from './projects/states/peer-progress/peers-anonymized/peers-anonymized.component';
import {FileViewerComponent} from './common/file-viewer/file-viewer.component';
import {TaskDefinitionEditorComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component';
import {TaskDefinitionGeneralComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component';
Expand Down Expand Up @@ -281,6 +283,8 @@ import {GradeService} from './common/services/grade.service';
PdfViewerPanelComponent,
StaffTaskListComponent,
TaskSimilarityViewComponent,
PeerProgressComponent,
PeersAnonymizedComponent,
FiltersPipe,
TasksOfTaskDefinitionPipe,
TasksInTutorialsPipe,
Expand Down
12 changes: 12 additions & 0 deletions src/app/doubtfire-angularjs.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ import 'build/src/app/projects/states/dashboard/directives/directives.js';
import 'build/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.js';
import 'build/src/app/projects/states/dashboard/dashboard.js';
import 'build/src/app/projects/states/outcomes/outcomes.js';
import 'build/src/app/projects/states/peer-progress/peer-progress.js';
import 'build/src/app/projects/states/peer-progress/peers-anonymized/peers-anonymized.js';
import 'build/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.js';
import 'build/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.js';
import 'build/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.js';
Expand Down Expand Up @@ -208,6 +210,8 @@ import {TaskStatusCardComponent} from './projects/states/dashboard/directives/ta
import {TaskDueCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component';
import {FooterComponent} from './common/footer/footer.component';
import {TaskAssessmentCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component';
import {PeerProgressComponent} from './projects/states/peer-progress/peer-progress.component';
import {PeersAnonymizedComponent} from './projects/states/peer-progress/peers-anonymized/peers-anonymized.component';
import {TaskSubmissionCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component';
import {InboxComponent} from './units/states/tasks/inbox/inbox.component';
import {TaskDefinitionEditorComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component';
Expand Down Expand Up @@ -381,6 +385,14 @@ DoubtfireAngularJSModule.directive(
'fTaskAssessmentCard',
downgradeComponent({component: TaskAssessmentCardComponent}),
);
DoubtfireAngularJSModule.directive(
'fPeerProgress',
downgradeComponent({component: PeerProgressComponent}),
);
DoubtfireAngularJSModule.directive(
'fPeersAnonymized',
downgradeComponent({component: PeersAnonymizedComponent}),
);
DoubtfireAngularJSModule.directive(
'institutionSettings',
downgradeComponent({component: InstitutionSettingsComponent}),
Expand Down
16 changes: 16 additions & 0 deletions src/app/projects/states/peer-progress/peer-progress.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
angular.module('doubtfire.projects.states.peer-progress', [])

#
# Peer progress state for projects
#
.config(($stateProvider) ->
$stateProvider.state 'projects/peer-progress', {
parent: 'projects/index'
url: '/peer-progress'
templateUrl: 'projects/states/peer-progress/peer-progress.tpl.html'
data:
task: "Peer Progress"
pageTitle: "_Home_"
roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Student', 'Auditor']
}
)
46 changes: 46 additions & 0 deletions src/app/projects/states/peer-progress/peer-progress.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<div class="peer-progress-page">
<div class="peer-progress-card">
<p class="peer-progress-eyebrow">Peer Progress</p>
<h1>{{ project?.unit?.name || 'Current Unit' }}</h1>
<p class="peer-progress-copy">
This sample view compares your project progress with the wider cohort using anonymized peer
data.
</p>

<div class="peer-progress-summary" *ngIf="summary">
<div class="peer-progress-metric">
<span class="metric-label">Your anonymized profile</span>
<strong>{{ summary.yourAlias }} (You)</strong>
</div>
<div class="peer-progress-metric">
<span class="metric-label">Cohort average</span>
<strong>{{ summary.cohortAverage }}%</strong>
</div>
<div class="peer-progress-metric">
<span class="metric-label">Peers ahead</span>
<strong>{{ summary.peersAhead }} of {{ summary.totalPeers }}</strong>
</div>
<div class="peer-progress-metric">
<span class="metric-label">Highest sample progress</span>
<strong>{{ summary.strongestProgress }}%</strong>
</div>
</div>

<div class="progress-panel" *ngIf="summary">
<div class="progress-panel-heading">
<div>
<h2>Sample Peer Progress Bar</h2>
<p>Current progress for your anonymized peer profile.</p>
</div>
<strong>{{ summary.yourProgress }}%</strong>
</div>
<mat-progress-bar mode="determinate" [value]="summary.yourProgress"></mat-progress-bar>
</div>

<div class="peer-progress-actions">
<button mat-flat-button color="primary" (click)="goToAnonymizedPeers()">
View Anonymized Peer Progress
</button>
</div>
</div>
</div>
101 changes: 101 additions & 0 deletions src/app/projects/states/peer-progress/peer-progress.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
.peer-progress-page {
padding: 16px;
}

.peer-progress-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
margin: 0 auto;
max-width: 960px;
padding: 24px;
}

.peer-progress-eyebrow {
color: #2563eb;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
margin: 0 0 8px;
text-transform: uppercase;
}

.peer-progress-card h1 {
color: #111827;
font-size: 28px;
margin: 0 0 16px;
}

.peer-progress-copy {
color: #4b5563;
font-size: 16px;
line-height: 1.5;
margin: 0 0 12px;
}

.peer-progress-summary {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
margin: 24px 0;
}

.peer-progress-metric {
background: #f8fafc;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 16px;
}

.metric-label {
color: #64748b;
display: block;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.05em;
margin-bottom: 8px;
text-transform: uppercase;
}

.peer-progress-metric strong {
color: #0f172a;
font-size: 18px;
}

.progress-panel {
background: linear-gradient(135deg, #eff6ff, #f8fafc);
border-radius: 12px;
margin-top: 24px;
padding: 20px;
}

.progress-panel-heading {
align-items: flex-start;
display: flex;
gap: 16px;
justify-content: space-between;
margin-bottom: 16px;
}

.progress-panel-heading h2 {
color: #0f172a;
font-size: 20px;
margin: 0 0 4px;
}

.progress-panel-heading p {
color: #475569;
margin: 0;
}

.progress-panel-heading strong {
color: #1d4ed8;
font-size: 24px;
}

.peer-progress-actions {
display: flex;
justify-content: flex-end;
margin-top: 24px;
}
33 changes: 33 additions & 0 deletions src/app/projects/states/peer-progress/peer-progress.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Component, Inject, Input, OnInit } from '@angular/core';
import { UIRouter } from '@uirouter/angular';
import { Project } from 'src/app/api/models/doubtfire-model';
import {
PeerProgressService,
PeerProgressSummary,
} from 'src/app/api/services/peer-progress.service';

@Component({
selector: 'f-peer-progress',
templateUrl: './peer-progress.component.html',
styleUrls: ['./peer-progress.component.scss'],
})
export class PeerProgressComponent implements OnInit {
@Input() project: Project;

summary: PeerProgressSummary;

constructor(
@Inject(UIRouter) private router: UIRouter,
private peerProgressService: PeerProgressService,
) {}

ngOnInit(): void {
this.summary = this.peerProgressService.getSummary(this.project);
}

goToAnonymizedPeers(): void {
this.router.stateService.go('projects/peers-anonymized', {
projectId: this.project?.id,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<f-peer-progress [project]="project"></f-peer-progress>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
angular.module('doubtfire.projects.states.peers-anonymized', [])

#
# Anonymized peer progress state for projects
#
.config(($stateProvider) ->
$stateProvider.state 'projects/peers-anonymized', {
parent: 'projects/index'
url: '/peer-progress/anonymized'
templateUrl: 'projects/states/peer-progress/peers-anonymized/peers-anonymized.tpl.html'
data:
task: "Peer Progress"
pageTitle: "_Home_"
roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Student', 'Auditor']
}
)
Loading