diff --git a/README.md b/README.md index cf49e28efd..7885b47a9b 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,34 @@ A modern, lightweight learning management system. ## Migration Progress +Important: When completing a frontend migration, please update the below list regarding the component you have migrated. + SUMMARY: -73 / 132 components migrated +- `89 / 183` components migrated +- `19` components no longer in the doubtfire-lms/9.x branch + +NO LONGER IN doubtfire-lms/9.x + +- [x] ./src/app/projects/states/all/directives/all-projects-list/all-projects-list.coffee +- [x] ./src/app/projects/states/all/all.coffee +- [x] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee +- [x] ./src/app/tasks/task-definition-selector/task-definition-selector.coffee +- [x] ./src/app/tasks/task-status-selector/task-status-selector.coffee +- [x] ./src/app/config/debug/debug.coffee +- [x] ./src/app/projects/states/all/directives/directives.coffee +- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee +- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-outcomes-card/task-outcomes-card.coffee +- [x] ./src/app/admin/states/states.coffee +- [x] ./src/app/admin/admin.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/directives.coffee +- [x] ./src/app/units/states/tasks/viewer/viewer.coffee +- [x] ./src/app/units/states/all/directives/all-units-list/all-units-list.coffee +- [x] ./src/app/units/states/all/directives/directives.coffee +- [x] ./src/app/units/states/all/all.coffee +- [x] ./src/app/common/alert-list/alert-list.coffee +- [x] ./src/app/common/modals/progress-modal/progress-modal.coffee +- [x] ./src/app/errors/states/not-found/not-found.coffee MIGRATED: @@ -25,6 +50,7 @@ MIGRATED: - [x] ./src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.ts - [x] ./src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.ts - [x] ./src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.ts +- [x] ./src/app/tasks/project-tasks-list/project-tasks-list.coffee - [x] ./src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.ts - [x] ./src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts - [x] ./src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts @@ -46,10 +72,10 @@ MIGRATED: - [x] ./src/app/admin/tii-action-log/tii-action-log.component.ts - [x] ./src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts - [x] ./src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts +- [x] ./src/app/admin/modals/create-unit-modal/create-new-unit-modal.component.ts - [x] ./src/app/eula/accept-eula/accept-eula.component.ts - [x] ./src/app/welcome/welcome.component.ts - [x] ./src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts -- [x] ./src/app/units/states/tasks/inbox/inbox.component.ts - [x] ./src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.ts - [x] ./src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.ts - [x] ./src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.ts @@ -64,6 +90,7 @@ MIGRATED: - [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.ts - [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.ts - [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.ts +- [x] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts - [x] ./src/app/units/states/analytics/unit-analytics-route.component.ts - [x] ./src/app/common/footer/footer.component.ts - [x] ./src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.ts @@ -91,7 +118,31 @@ MIGRATED: - [x] ./src/app/common/services/alert.service.ts - [x] ./src/app/sessions/states/sign-in/sign-in.component.ts - [x] ./src/app/account/edit-profile/edit-profile.component.ts +- [x] ./src/app/tasks/modals/grade-task-modal/grade-task-modal.component.ts +- [x] ./src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.ts +- [x] ./src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts +- [x] ./src/app/config/privacy-policy/privacy-policy.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/task-sheet-view/task-sheet-view.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee +- [x] ./src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee +- [x] ./src/app/units/states/tasks/inbox/inbox.coffee +- [x] ./src/app/admin/states/units/units.component.ts +- [x] ./src/app/admin/states/users/users.component.ts +- [x] ./src/app/common/grade-icon/grade-icon.component.ts +- [x] ./src/app/common/services/grade.service.ts +- [x] ./src/app/common/services/alert.service.ts +- [x] ./src/app/errors/states/unauthorised/unauthorised.component.ts +- [x] ./src/app/groups/group-set-selector/group-set-selector.component.ts - [x] ./src/app/admin/modals/create-unit-modal/create-unit-modal.coffee +- [x] ./src/app/common/services/date.service.ts +- [x] ./src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee (IN 10.0.x) +- [x] ./src/app/groups/group-member-list/group-member-list.coffee +- [x] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee +- [x] ./src/app/common/modals/confirmation-modal/confirmation-modal.coffee +- [x] ./src/app/common/modals/comments-modal/comments-modal.coffee (IN 10.0.x) +- [x] ./src/app/groups/group-selector/group-selector.coffee +- [x] ./src/app/groups/group-set-manager/group-set-manager.coffee TODO: @@ -101,46 +152,33 @@ TODO: - [ ] ./src/app/visualisations/achievement-custom-bar-chart.coffee - [ ] ./src/app/visualisations/student-task-status-pie-chart.coffee - [ ] ./src/app/visualisations/alignment-bullet-chart.coffee -- [ ] ./src/app/visualisations/progress-burndown-chart.coffee - [ ] ./src/app/visualisations/task-status-pie-chart.coffee - [ ] ./src/app/visualisations/achievement-box-plot.coffee - [ ] ./src/app/visualisations/task-completion-box-plot.coffee - [ ] ./src/app/visualisations/visualisations.coffee -- [ ] ./src/app/tasks/task-status-selector/task-status-selector.coffee - [ ] ./src/app/tasks/tasks.coffee -- [ ] ./src/app/tasks/modals/modals.coffee - [ ] ./src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee -- [ ] ./src/app/tasks/modals/grade-task-modal/grade-task-modal.coffee -- [ ] ./src/app/tasks/task-definition-selector/task-definition-selector.coffee -- [ ] ./src/app/tasks/project-tasks-list/project-tasks-list.coffee -- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee - [ ] ./src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.coffee - [ ] ./src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment.coffee - [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.coffee - [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment.coffee - [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.coffee -- [ ] ./src/app/config/privacy-policy/privacy-policy.coffee +- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee +- [ ] ./src/app/tasks/modals/modals.coffee - [ ] ./src/app/config/config.coffee - [ ] ./src/app/config/runtime/runtime.coffee - [ ] ./src/app/config/root-controller/root-controller.coffee - [ ] ./src/app/config/local-storage/local-storage.coffee -- [ ] ./src/app/config/routing/routing.coffee - [ ] ./src/app/config/vendor-dependencies/vendor-dependencies.coffee +- [ ] ./src/app/config/routing/routing.coffee - [ ] ./src/app/config/analytics/analytics.coffee -- [ ] ./src/app/config/debug/debug.coffee - [ ] ./src/app/projects/projects.coffee - [ ] ./src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee - [ ] ./src/app/projects/states/states.coffee -- [ ] ./src/app/projects/states/all/directives/directives.coffee -- [ ] ./src/app/projects/states/all/directives/all-projects-list/all-projects-list.coffee -- [ ] ./src/app/projects/states/all/all.coffee - [ ] ./src/app/projects/states/groups/groups.coffee - [ ] ./src/app/projects/states/feedback/feedback.coffee - [ ] ./src/app/projects/states/dashboard/directives/directives.coffee - [ ] ./src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee -- [ ] ./src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee -- [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee -- [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-outcomes-card/task-outcomes-card.coffee - [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee - [ ] ./src/app/projects/states/dashboard/dashboard.coffee - [ ] ./src/app/projects/states/outcomes/outcomes.coffee @@ -148,47 +186,28 @@ TODO: - [ ] ./src/app/projects/states/portfolio/directives/directives.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee +- [ ] ./src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.coffee - [ ] ./src/app/projects/states/portfolio/portfolio.coffee - [ ] ./src/app/projects/states/index/index.coffee -- [ ] ./src/app/projects/states/tutorials/tutorials.coffee - [ ] ./src/app/projects/project-outcome-alignment/project-outcome-alignment.coffee +- [ ] ./src/app/projects/states/tutorials/tutorials.coffee - [ ] ./src/app/admin/modals/modals.coffee -- [ ] ./src/app/admin/states/states.coffee -- [ ] ./src/app/admin/states/units/units.coffee -- [ ] ./src/app/admin/states/users/users.coffee -- [ ] ./src/app/admin/admin.coffee -- [ ] ./src/app/groups/group-selector/group-selector.coffee -- [ ] ./src/app/groups/group-set-manager/group-set-manager.coffee - [ ] ./src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee -- [ ] ./src/app/groups/group-member-list/group-member-list.coffee -- [ ] ./src/app/groups/group-set-selector/group-set-selector.coffee - [ ] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee - [ ] ./src/app/groups/groups.coffee -- [ ] ./src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee +- [ ] ./src/app/units/states/groups/groups.coffee +- [ ] ./src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee - [ ] ./src/app/units/modals/modals.coffee - [ ] ./src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.coffee - [ ] ./src/app/units/units.coffee - [ ] ./src/app/units/states/states.coffee -- [ ] ./src/app/units/states/tasks/inbox/inbox.coffee - [ ] ./src/app/units/states/tasks/tasks.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/directives.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/task-sheet-view/task-sheet-view.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee -- [ ] ./src/app/units/states/tasks/viewer/viewer.coffee - [ ] ./src/app/units/states/tasks/definition/definition.coffee - [ ] ./src/app/units/states/portfolios/portfolios.coffee -- [ ] ./src/app/units/states/all/directives/all-units-list/all-units-list.coffee -- [ ] ./src/app/units/states/all/directives/directives.coffee -- [ ] ./src/app/units/states/all/all.coffee -- [ ] ./src/app/units/states/groups/groups.coffee +- [ ] ./src/app/units/states/analytics/analytics.coffee - [ ] ./src/app/units/states/edit/directives/directives.coffee -- [ ] ./src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee -- [ ] ./src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee -- [ ] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee - [ ] ./src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.coffee - [ ] ./src/app/units/states/edit/edit.coffee - [ ] ./src/app/units/states/rollover/directives/directives.coffee @@ -196,34 +215,24 @@ TODO: - [ ] ./src/app/units/states/rollover/rollover.coffee - [ ] ./src/app/units/states/index/index.coffee - [ ] ./src/app/units/states/students-list/students-list.coffee -- [ ] ./src/app/units/states/analytics/analytics.coffee -- [ ] ./src/app/common/filters/filters.coffee -- [ ] ./src/app/common/content-editable/content-editable.coffee -- [ ] ./src/app/common/alert-list/alert-list.coffee -- [ ] ./src/app/common/modals/confirmation-modal/confirmation-modal.coffee -- [ ] ./src/app/common/modals/comments-modal/comments-modal.coffee - [ ] ./src/app/common/modals/modals.coffee - [ ] ./src/app/common/modals/csv-result-modal/csv-result-modal.coffee -- [ ] ./src/app/common/modals/progress-modal/progress-modal.coffee -- [ ] ./src/app/common/grade-icon/grade-icon.coffee - [ ] ./src/app/common/file-uploader/file-uploader.coffee - [ ] ./src/app/common/common.coffee -- [ ] ./src/app/common/services/grade-service.coffee -- [ ] ./src/app/common/services/date-service.coffee -- [ ] ./src/app/common/services/alert-service.coffee +- [ ] ./src/app/common/content-editable/content-editable.coffee - [ ] ./src/app/common/services/media-service.coffee - [ ] ./src/app/common/services/recorder-service.coffee - [ ] ./src/app/common/services/outcome-service.coffee - [ ] ./src/app/common/services/listener-service.coffee -- [ ] ./src/app/common/services/analytics-service.coffee - [ ] ./src/app/common/services/services.coffee +- [ ] ./src/app/common/services/date-service.coffee +- [ ] ./src/app/common/services/analytics-service.coffee - [ ] ./src/app/sessions/auth/http-auth-injector.coffee - [ ] ./src/app/sessions/sessions.coffee - [ ] ./src/app/errors/errors.coffee - [ ] ./src/app/errors/states/states.coffee -- [ ] ./src/app/errors/states/unauthorised/unauthorised.coffee -- [ ] ./src/app/errors/states/not-found/not-found.coffee - [ ] ./src/app/errors/states/timeout/timeout.coffee +- [ ] ./src/app/common/filters/filters.coffee ## Table of Contents diff --git a/src/app/api/models/project.ts b/src/app/api/models/project.ts index 380c62d199..3c154842e2 100644 --- a/src/app/api/models/project.ts +++ b/src/app/api/models/project.ts @@ -347,7 +347,7 @@ export class Project extends Entity { } public isEnrolledIn(tutorial: Tutorial): boolean { - return this.tutorials.includes(tutorial); + return this.tutorials.some((t) => t.id === tutorial.id); } public updateUnitEnrolment(): void { diff --git a/src/app/api/services/unit.service.ts b/src/app/api/services/unit.service.ts index e04d3bec82..42c8211ab8 100644 --- a/src/app/api/services/unit.service.ts +++ b/src/app/api/services/unit.service.ts @@ -53,7 +53,8 @@ export class UnitService extends CachedEntityService { }, }, { - keys: 'unitRoles', + // keys: 'unitRoles', + keys: 'staff', toEntityOp: (data, key, entity) => { const unitRoleService = AppInjector.get(UnitRoleService); // Add staff diff --git a/src/app/common/header/header.component.ts b/src/app/common/header/header.component.ts index b5e1960c8a..842287b92e 100644 --- a/src/app/common/header/header.component.ts +++ b/src/app/common/header/header.component.ts @@ -104,8 +104,7 @@ export class HeaderComponent implements OnInit, OnDestroy { } isUniqueRole = (unit) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const units = this.unitRoles.filter((role: any) => role.unit.id === unit.unit.id); + const units = this.unitRoles.filter((role: UnitRole) => role.unit?.id === unit.unit?.id); return units.length == 1 || unit.role == 'Tutor'; }; diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.coffee b/src/app/common/modals/confirmation-modal/confirmation-modal.coffee deleted file mode 100644 index c7999098cf..0000000000 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.coffee +++ /dev/null @@ -1,35 +0,0 @@ -angular.module("doubtfire.common.modals.confirmation-modal", []) - -.factory("ConfirmationModal", ($modal) -> - ConfirmationModal = {} - - # - # Show a modal asking the user to confirm their indicated action. - # - ConfirmationModal.show = (title, message, action) -> - modalInstance = $modal.open - templateUrl: 'common/modals/confirmation-modal/confirmation-modal.tpl.html' - controller: 'ConfirmationModalCtrl' - resolve: - title: -> title - message: -> message - action: -> action - - ConfirmationModal -) - -# -# Controller for confirmation modal -# -.controller('ConfirmationModalCtrl', ($scope, $modalInstance, title, message, action, alertService) -> - $scope.title = title - $scope.message = message - - $scope.confirmAction = -> - action() - $modalInstance.dismiss() - - $scope.cancelAction = -> - alertService.message "#{title} action cancelled", 3000 - $modalInstance.dismiss() -) diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.html b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html new file mode 100644 index 0000000000..3ec07fc729 --- /dev/null +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html @@ -0,0 +1,20 @@ +
+

+
+ +
+
{{ title }}
+ Please confirm that you want to perform this action. +
+
+

+ + {{ message }} + + + + + +
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.scss b/src/app/common/modals/confirmation-modal/confirmation-modal.component.scss similarity index 100% rename from src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.scss rename to src/app/common/modals/confirmation-modal/confirmation-modal.component.scss diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts new file mode 100644 index 0000000000..f9506e5b8b --- /dev/null +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts @@ -0,0 +1,47 @@ +import {Component, OnInit, Input, Inject} from '@angular/core'; +import {AlertService} from '../../services/alert.service'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; + +export interface ConfirmationModalData { + title: string; + message: string; + action?: any; +} + +@Component({ + selector: 'confirmation-modal', + templateUrl: './confirmation-modal.component.html', + styleUrls: ['./confirmation-modal.component.scss'], +}) +export class ConfirmationModalComponent implements OnInit { + @Input() title: string; + @Input() message: string; + @Input() action: () => void; + + constructor( + @Inject(AlertService) private alertService: AlertService, + @Inject(MAT_DIALOG_DATA) public data: ConfirmationModalData, + + public dialogRef: MatDialogRef, + ) {} + + ngOnInit(): void { + this.title = this.data.title; + this.message = this.data.message; + this.action = this.data.action; + } + + public confirmAction() { + if (typeof this.action === 'function') { + this.action(); + } else { + this.alertService.error(`${this.title} action failed.`); + } + this.dialogRef.close(); + } + + public cancelAction() { + this.alertService.success(`${this.title} action cancelled.`); + this.dialogRef.close(); + } +} diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.scss b/src/app/common/modals/confirmation-modal/confirmation-modal.scss deleted file mode 100644 index f30b0e345c..0000000000 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.scss +++ /dev/null @@ -1,3 +0,0 @@ -.confirmation-modal .modal-body { - font-size: 1.5em; -} diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts new file mode 100644 index 0000000000..6257277eff --- /dev/null +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts @@ -0,0 +1,26 @@ +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {ConfirmationModalComponent, ConfirmationModalData} from './confirmation-modal.component'; + +@Injectable({ + providedIn: 'root', +}) +export class ConfirmationModalService { + constructor(public dialog: MatDialog) {} + + public show(title: string, message: string, action?: any) { + this.dialog.open( + ConfirmationModalComponent, + { + data: { + title, + message, + action, + }, + position: {top: '2.5%'}, + width: '100%', + maxWidth: '650px', + }, + ); + } +} diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html b/src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html deleted file mode 100644 index 4bd87f6868..0000000000 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ -
- - - -
diff --git a/src/app/common/modals/modals.coffee b/src/app/common/modals/modals.coffee index 16d2be1ec8..73aae8685b 100644 --- a/src/app/common/modals/modals.coffee +++ b/src/app/common/modals/modals.coffee @@ -1,5 +1,4 @@ angular.module("doubtfire.common.modals", [ 'doubtfire.common.modals.csv-result-modal' - 'doubtfire.common.modals.confirmation-modal' 'doubtfire.common.modals.comments-modal' ]) diff --git a/src/app/common/services/date-service.coffee b/src/app/common/services/date-service.coffee deleted file mode 100644 index 10cffe1980..0000000000 --- a/src/app/common/services/date-service.coffee +++ /dev/null @@ -1,31 +0,0 @@ -angular.module("doubtfire.common.services.dates", []) -# -# Services for making alerts -# -.factory("dateService", -> - - dateService = {} - - monthNames = [ - "Jan", "Feb", "Mar", - "Apr", "May", "Jun", "Jul", - "Aug", "Sep", "Oct", - "Nov", "Dec" - ] - - dateService.showDate = (dateValue) -> - if (dateValue?) - date = new Date(dateValue) - "#{monthNames[date.getMonth()]} #{date.getFullYear()}" - else - "-" - - dateService.showFullDate = (dateValue) -> - if (dateValue?) - date = new Date(dateValue) - "#{date.getDate()} #{monthNames[date.getMonth()]} #{date.getFullYear()}" - else - "-" - - dateService -) diff --git a/src/app/common/services/date.service.spec.ts b/src/app/common/services/date.service.spec.ts new file mode 100644 index 0000000000..7421b9d308 --- /dev/null +++ b/src/app/common/services/date.service.spec.ts @@ -0,0 +1,15 @@ +import {TestBed} from '@angular/core/testing'; +import {DateService} from './date.service'; + +describe('DateService', () => { + let service: DateService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DateService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/src/app/common/services/date.service.ts b/src/app/common/services/date.service.ts new file mode 100644 index 0000000000..915fd96fcc --- /dev/null +++ b/src/app/common/services/date.service.ts @@ -0,0 +1,69 @@ +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root', // Available throughout the app. +}) +export class DateService { + private monthNames: string[] = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + + constructor() { + // Bind the methods to ensure `this` context is correct + this.showDate = this.showDate.bind(this); + this.showFullDate = this.showFullDate.bind(this); + } + + /** + * Returns a dateString for the passed-in `date`, + * in the format of `MMM-YYYY`. + * + * Note: If you are going to pass in a `dateString`, please ensure it is in + * `ISO 8601` format. + * + * @param {string | Date} dateValue + * + * @returns {string} + */ + showDate(dateValue?: string | Date): string { + if (dateValue) { + const date = new Date(dateValue); + + return `${this.monthNames[date.getMonth()]} ${date.getFullYear()}`; + } else { + return '-'; + } + } + + /** + * Returns a dateString for the passed-in `date`, + * in the format of `D-MM-YYYY`. + * + * Note: If you are going to pass in a `dateString`, please ensure it is in + * `ISO 8601` format. + * + * @param {string | Date} dateValue + * + * @returns {string} + */ + showFullDate(dateValue?: string | Date): string { + if (dateValue) { + const date = new Date(dateValue); + + return `${date.getDate()} ${this.monthNames[date.getMonth()]} ${date.getFullYear()}`; + } else { + return '-'; + } + } +} diff --git a/src/app/common/services/media-service.coffee b/src/app/common/services/media-service.coffee deleted file mode 100644 index 21c6cefa08..0000000000 --- a/src/app/common/services/media-service.coffee +++ /dev/null @@ -1,20 +0,0 @@ -angular.module("doubtfire.common.services.media-service", []) -# -# Services for working with media APIs -# -.factory("mediaService", ($rootScope, $timeout, $sce) -> - mediaService = {} - - mediaService.audioCtx = mediaService.audioCtx? || (new (window.AudioContext || webkitAudioContext)()) - - mediaService.getMimeType = () -> - mimeType = 'audio/webm' - if !MediaRecorder.isTypeSupported(mimeType) - if navigator.userAgent.toLowerCase().indexOf('firefox') > -1 - mimeType = 'audio/ogg' - else - mimeType = '' - mimeType - - mediaService -) diff --git a/src/app/common/services/services.coffee b/src/app/common/services/services.coffee index e88e7a7bdc..086ea3536f 100644 --- a/src/app/common/services/services.coffee +++ b/src/app/common/services/services.coffee @@ -1,7 +1,6 @@ angular.module("doubtfire.common.services", [ 'doubtfire.common.services.outcome-service' 'doubtfire.common.services.analytics' - 'doubtfire.common.services.dates' 'doubtfire.common.services.listener' 'doubtfire.common.services.recorder-service' ]) diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index c714ff0e5a..4bf79b47d1 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -1,16 +1,17 @@ import {interval} from 'rxjs'; import {take} from 'rxjs/operators'; -import { NgModule, Injector, DoBootstrap } from '@angular/core'; -import { BrowserModule, DomSanitizer, Title } from '@angular/platform-browser'; -import { UpgradeModule } from '@angular/upgrade/static'; -import { AppInjector, setAppInjector } from './app-injector'; -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgxChartsModule } from '@swimlane/ngx-charts'; +import {NgModule, Injector, DoBootstrap} from '@angular/core'; +import {BrowserModule, DomSanitizer, Title} from '@angular/platform-browser'; +import {UpgradeModule} from '@angular/upgrade/static'; +import {AppInjector, setAppInjector} from './app-injector'; +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {NgxChartsModule} from '@swimlane/ngx-charts'; // Lottie animation module // import {LottieModule, LottieCacheModule} from 'ngx-lottie'; +import {ProgressDashboardComponent} from './projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component'; import {provideLottieOptions, LottieComponent} from 'ngx-lottie'; import player from 'lottie-web'; import {ClipboardModule} from '@angular/cdk/clipboard'; @@ -96,14 +97,19 @@ import {ExtensionCommentComponent} from './tasks/task-comments-viewer/extension- import {CampusListComponent} from './admin/institution-settings/campuses/campus-list/campus-list.component'; import {ExtensionModalComponent} from './common/modals/extension-modal/extension-modal.component'; import {CalendarModalComponent} from './common/modals/calendar-modal/calendar-modal.component'; +import {ConfirmationModalComponent} from './common/modals/confirmation-modal/confirmation-modal.component'; import {MatRadioModule} from '@angular/material/radio'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; -import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatOptionModule} from '@angular/material/core'; +import { + DateAdapter, + MAT_DATE_FORMATS, + MAT_DATE_LOCALE, + MatOptionModule, +} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import { DateFnsAdapter } from '@angular/material-date-fns-adapter'; -import { enAU } from 'date-fns/locale'; - +import {DateFnsAdapter} from '@angular/material-date-fns-adapter'; +import {enAU} from 'date-fns/locale'; import {doubtfireStates} from './doubtfire.states'; import {MatTableModule} from '@angular/material/table'; @@ -226,23 +232,23 @@ import { TeachingPeriodUnitImportDialogComponent, TeachingPeriodUnitImportService, } from './admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog'; -import { UnauthorisedComponent } from './errors/states/unauthorised/unauthorised.component'; -import { AcceptEulaComponent } from './eula/accept-eula/accept-eula.component'; -import { TiiActionLogComponent } from './admin/tii-action-log/tii-action-log.component'; -import { TiiActionService } from './api/services/tii-action.service'; -import { FUnitsComponent } from './admin/states/units/units.component'; -import { FUnitTaskListComponent } from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; -import { FTaskDetailsViewComponent } from './units/task-viewer/directives/task-details-view/task-details-view.component'; -import { FTaskSheetViewComponent } from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; -import { UnitCodeComponent } from './common/unit-code/unit-code.component'; -import { GradeService } from './common/services/grade.service'; -import { UnitRootStateComponent } from './units/unit-root-state.component'; -import { TaskViewerStateComponent } from './units/task-viewer/task-viewer-state.component'; -import { ProjectRootStateComponent } from './projects/states/project-root-state.component'; -import { ProjectProgressDashboardComponent } from './projects/project-progress-dashboard/project-progress-dashboard.component'; -import { ProgressBurndownChartComponent } from './visualisations/progress-burndown-chart/progressburndownchart.component'; -import { TaskVisualisationComponent } from './visualisations/task-visualisation/taskvisualisation.component'; -import { ChartBaseComponent } from './common/chart-base/chart-base-component/chart-base-component.component'; +import {UnauthorisedComponent} from './errors/states/unauthorised/unauthorised.component'; +import {AcceptEulaComponent} from './eula/accept-eula/accept-eula.component'; +import {TiiActionLogComponent} from './admin/tii-action-log/tii-action-log.component'; +import {TiiActionService} from './api/services/tii-action.service'; +import {FUnitsComponent} from './admin/states/units/units.component'; +import {FUnitTaskListComponent} from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; +import {FTaskDetailsViewComponent} from './units/task-viewer/directives/task-details-view/task-details-view.component'; +import {FTaskSheetViewComponent} from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; +import {UnitCodeComponent} from './common/unit-code/unit-code.component'; +import {GradeService} from './common/services/grade.service'; +import {UnitRootStateComponent} from './units/unit-root-state.component'; +import {TaskViewerStateComponent} from './units/task-viewer/task-viewer-state.component'; +import {ProjectRootStateComponent} from './projects/states/project-root-state.component'; +import {ProjectProgressDashboardComponent} from './projects/project-progress-dashboard/project-progress-dashboard.component'; +import {ProgressBurndownChartComponent} from './visualisations/progress-burndown-chart/progressburndownchart.component'; +import {TaskVisualisationComponent} from './visualisations/task-visualisation/taskvisualisation.component'; +import {ChartBaseComponent} from './common/chart-base/chart-base-component/chart-base-component.component'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import {ScormAdapterService} from './api/services/scorm-adapter.service'; import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component'; @@ -250,9 +256,13 @@ import {TaskScormCardComponent} from './projects/states/dashboard/directives/tas import {TestAttemptService} from './api/services/test-attempt.service'; import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component'; import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component'; -import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; -import { GradeTaskModalComponent } from './tasks/modals/grade-task-modal/grade-task-modal.component'; -import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; +import {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; +import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +// import {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; +// import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; +import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.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 = { @@ -266,12 +276,19 @@ const MY_DATE_FORMAT = { monthYearA11yLabel: 'MMMM yyyy', }, }; -import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; +import {TutorialsComponent} from './projects/states/tutorials/tutorials.component'; +import {UnitStudentEnrolmentModalComponent} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; +import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-chart/taskstatuspiechart.component'; +import {GroupMemberListComponent} from './groups/group-member-list/group-member-list.component'; +import {GroupSelectorComponent} from './groups/group-selector/group-selector.component'; +import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-manager.component'; @NgModule({ // Components we declare declarations: [ + TaskStatusPieChartComponent, AlertComponent, + ProgressDashboardComponent, UnitStudentEnrolmentModalComponent, AboutDoubtfireModalContent, TeachingPeriodUnitImportDialogComponent, @@ -291,6 +308,7 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student- OverseerImageListComponent, ExtensionModalComponent, CalendarModalComponent, + ConfirmationModalComponent, InstitutionSettingsComponent, HomeComponent, CommentBubbleActionComponent, @@ -389,6 +407,12 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student- TaskScormCardComponent, ScormExtensionCommentComponent, ScormExtensionModalComponent, + TutorialsComponent, + UnitStaffEditorComponent, + PortfolioGradeSelectStepComponent, + GroupMemberListComponent, + GroupSelectorComponent, + GroupSetManagerComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index d9245086aa..ba04e6285a 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -63,7 +63,6 @@ import 'build/src/app/projects/project-progress-dashboard/project-progress-dashb import 'build/src/app/projects/states/groups/groups.js'; import 'build/src/app/projects/states/feedback/feedback.js'; import 'build/src/app/projects/states/states.js'; -import 'build/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.js'; import 'build/src/app/projects/states/dashboard/directives/student-task-list/student-task-list.js'; import 'build/src/app/projects/states/dashboard/directives/directives.js'; import 'build/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.js'; @@ -72,21 +71,15 @@ import 'build/src/app/projects/states/outcomes/outcomes.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'; -import 'build/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.js'; import 'build/src/app/projects/states/portfolio/directives/directives.js'; import 'build/src/app/projects/states/portfolio/portfolio.js'; import 'build/src/app/projects/states/index/index.js'; -import 'build/src/app/projects/states/tutorials/tutorials.js'; import 'build/src/app/projects/project-outcome-alignment/project-outcome-alignment.js'; import 'build/src/app/admin/modals/modals.js'; -import 'build/src/app/groups/group-selector/group-selector.js'; -import 'build/src/app/groups/group-set-manager/group-set-manager.js'; import 'build/src/app/groups/groups.js'; import 'build/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.js'; -import 'build/src/app/groups/group-member-list/group-member-list.js'; -import 'build/src/app/groups/group-set-selector/group-set-selector.js'; import 'build/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.js'; import 'build/src/app/units/modals/modals.js'; import 'build/src/app/units/units.js'; @@ -98,7 +91,6 @@ import 'build/src/app/units/states/groups/groups.js'; import 'build/src/app/units/states/states.js'; import 'build/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.js'; import 'build/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.js'; -import 'build/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.js'; import 'build/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.js'; import 'build/src/app/units/states/edit/directives/directives.js'; import 'build/src/app/units/states/edit/edit.js'; @@ -110,7 +102,6 @@ import 'build/src/app/units/states/students-list/students-list.js'; import 'build/src/app/units/states/analytics/analytics.js'; import 'build/src/app/common/filters/filters.js'; import 'build/src/app/common/content-editable/content-editable.js'; -import 'build/src/app/common/modals/confirmation-modal/confirmation-modal.js'; import 'build/src/app/common/modals/comments-modal/comments-modal.js'; import 'build/src/app/common/modals/csv-result-modal/csv-result-modal.js'; import 'build/src/app/common/modals/modals.js'; @@ -120,9 +111,7 @@ import 'build/src/app/common/services/listener-service.js'; import 'build/src/app/common/services/outcome-service.js'; import 'build/src/app/common/services/services.js'; import 'build/src/app/common/services/recorder-service.js'; -import 'build/src/app/common/services/media-service.js'; import 'build/src/app/common/services/analytics-service.js'; -import 'build/src/app/common/services/date-service.js'; import 'build/src/app/sessions/auth/http-auth-injector.js'; import 'build/src/app/sessions/sessions.js'; import 'build/src/app/errors/errors.js'; @@ -145,6 +134,7 @@ import {ExtensionCommentComponent} from './tasks/task-comments-viewer/extension- import {TaskAssessmentCommentComponent} from './tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component'; import {ExtensionModalService} from './common/modals/extension-modal/extension-modal.service'; import {CalendarModalService} from './common/modals/calendar-modal/calendar-modal.service'; +import {ConfirmationModalService} from './common/modals/confirmation-modal/confirmation-modal.service'; import {CampusListComponent} from './admin/institution-settings/campuses/campus-list/campus-list.component'; import {ActivityTypeListComponent} from './admin/institution-settings/activity-type-list/activity-type-list.component'; import {InstitutionSettingsComponent} from './admin/institution-settings/institution-settings.component'; @@ -156,6 +146,7 @@ import {TutorialStreamService} from './api/services/tutorial-stream.service'; import {UnitStudentsEditorComponent} from './units/states/edit/directives/unit-students-editor/unit-students-editor.component'; import {CampusService} from './api/services/campus.service'; import {WebcalService} from './api/services/webcal.service'; +import {DateService} from './common/services/date.service'; import {StudentTutorialSelectComponent} from './units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component'; import {StudentCampusSelectComponent} from './units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component'; import {EmojiService} from './common/services/emoji.service'; @@ -168,6 +159,7 @@ import {fPdfViewerComponent} from './common/pdf-viewer/pdf-viewer.component'; import {PdfViewerPanelComponent} from './common/pdf-viewer-panel/pdf-viewer-panel.component'; import {StaffTaskListComponent} from './units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component'; import {StatusIconComponent} from './common/status-icon/status-icon.component'; +import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-chart/taskstatuspiechart.component'; import { GroupSetService, LearningOutcomeService, @@ -179,49 +171,57 @@ import { UnitService, UserService, } from './api/models/doubtfire-model'; -import { UnauthorisedComponent } from './errors/states/unauthorised/unauthorised.component'; -import { FileDownloaderService } from './common/file-downloader/file-downloader.service'; -import { CheckForUpdateService } from './sessions/service-worker-updater/check-for-update.service'; -import { TaskSubmissionService } from './common/services/task-submission.service'; -import { TaskAssessmentModalService } from './common/modals/task-assessment-modal/task-assessment-modal.service'; -import { TaskSubmissionHistoryComponent } from './tasks/task-submission-history/task-submission-history.component'; -import { HeaderComponent } from './common/header/header.component'; -import { SplashScreenComponent } from './home/splash-screen/splash-screen.component'; -import { GlobalStateService } from './projects/states/index/global-state.service'; -import { TransitionHooksService } from './sessions/transition-hooks.service'; -import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; -import { GradeTaskModalService } from './tasks/modals/grade-task-modal/grade-task-modal.service'; -import { AuthenticationService } from './api/services/authentication.service'; -import { ProjectService } from './api/services/project.service'; -import { ObjectSelectComponent } from './common/obect-select/object-select.component'; -import { TaskDefinitionService } from './api/services/task-definition.service'; -import { EditProfileDialogService } from './common/modals/edit-profile-dialog/edit-profile-dialog.service'; -import { GroupService } from './api/services/group.service'; -import { UserBadgeComponent } from './common/user-badge/user-badge.component'; -import { TaskStatusCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component'; -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 { 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'; -import { UnitAnalyticsComponent } from './units/states/analytics/unit-analytics-route.component'; -import { UnitTaskEditorComponent } from './units/states/edit/directives/unit-tasks-editor/unit-task-editor.component'; -import { CreateNewUnitModal } from './admin/modals/create-new-unit-modal/create-new-unit-modal.component'; -import { FUsersComponent } from './admin/states/users/users.component'; -import { FUnitTaskListComponent } from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; -import { FTaskDetailsViewComponent } from './units/task-viewer/directives/task-details-view/task-details-view.component'; -import { FTaskSheetViewComponent } from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; -import { ProgressBurndownChartComponent } from './visualisations/progress-burndown-chart/progressburndownchart.component'; -import { TaskVisualisationComponent } from './visualisations/task-visualisation/taskvisualisation.component'; - +import {UnauthorisedComponent} from './errors/states/unauthorised/unauthorised.component'; +import {FileDownloaderService} from './common/file-downloader/file-downloader.service'; +import {CheckForUpdateService} from './sessions/service-worker-updater/check-for-update.service'; +import {TaskSubmissionService} from './common/services/task-submission.service'; +import {TaskAssessmentModalService} from './common/modals/task-assessment-modal/task-assessment-modal.service'; +import {TaskSubmissionHistoryComponent} from './tasks/task-submission-history/task-submission-history.component'; +import {HeaderComponent} from './common/header/header.component'; +import {SplashScreenComponent} from './home/splash-screen/splash-screen.component'; +import {GlobalStateService} from './projects/states/index/global-state.service'; +import {TransitionHooksService} from './sessions/transition-hooks.service'; +import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; +import {GradeTaskModalService} from './tasks/modals/grade-task-modal/grade-task-modal.service'; +import {AuthenticationService} from './api/services/authentication.service'; +import {ProjectService} from './api/services/project.service'; +import {ObjectSelectComponent} from './common/obect-select/object-select.component'; +import {TaskDefinitionService} from './api/services/task-definition.service'; +import {EditProfileDialogService} from './common/modals/edit-profile-dialog/edit-profile-dialog.service'; +import {GroupService} from './api/services/group.service'; +import {UserBadgeComponent} from './common/user-badge/user-badge.component'; +import {TaskStatusCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component'; +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 {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'; +import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; +import {UnitTaskEditorComponent} from './units/states/edit/directives/unit-tasks-editor/unit-task-editor.component'; +import {CreateNewUnitModal} from './admin/modals/create-new-unit-modal/create-new-unit-modal.component'; +import {FUsersComponent} from './admin/states/users/users.component'; +import {FUnitTaskListComponent} from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; +import {FTaskDetailsViewComponent} from './units/task-viewer/directives/task-details-view/task-details-view.component'; +import {FTaskSheetViewComponent} from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; +import {ProgressBurndownChartComponent} from './visualisations/progress-burndown-chart/progressburndownchart.component'; +import {TaskVisualisationComponent} from './visualisations/task-visualisation/taskvisualisation.component'; +import {TutorialsComponent} from './projects/states/tutorials/tutorials.component'; +import {ProgressDashboardComponent} from './projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component'; import {FUnitsComponent} from './admin/states/units/units.component'; import {AlertService} from './common/services/alert.service'; - import {GradeService} from './common/services/grade.service'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; -import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; -import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import {UnitStudentEnrolmentModalService} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; +import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; + +// import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; +// import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; +import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; +import {GroupMemberListComponent} from './groups/group-member-list/group-member-list.component'; +import {GroupSelectorComponent} from './groups/group-selector/group-selector.component'; +import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-manager.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -241,6 +241,10 @@ DoubtfireAngularJSModule.factory('AboutDoubtfireModal', downgradeInjectable(Abou DoubtfireAngularJSModule.factory('DoubtfireConstants', downgradeInjectable(DoubtfireConstants)); DoubtfireAngularJSModule.factory('ExtensionModal', downgradeInjectable(ExtensionModalService)); DoubtfireAngularJSModule.factory('CalendarModal', downgradeInjectable(CalendarModalService)); +DoubtfireAngularJSModule.factory( + 'ConfirmationModal', + downgradeInjectable(ConfirmationModalService), +); DoubtfireAngularJSModule.factory('TaskCommentService', downgradeInjectable(TaskCommentService)); DoubtfireAngularJSModule.factory('alertService', downgradeInjectable(AlertService)); DoubtfireAngularJSModule.factory('tutorialService', downgradeInjectable(TutorialService)); @@ -298,12 +302,24 @@ DoubtfireAngularJSModule.factory( 'EditProfileService', downgradeInjectable(EditProfileDialogService), ); +DoubtfireAngularJSModule.factory('dateService', downgradeInjectable(DateService)); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); DoubtfireAngularJSModule.factory('GradeTaskModal', downgradeInjectable(GradeTaskModalService)); -DoubtfireAngularJSModule.factory('UnitStudentEnrolmentModal', downgradeInjectable(UnitStudentEnrolmentModalService)); +DoubtfireAngularJSModule.factory( + 'UnitStudentEnrolmentModal', + downgradeInjectable(UnitStudentEnrolmentModalService), +); DoubtfireAngularJSModule.factory('PrivacyPolicy', downgradeInjectable(PrivacyPolicy)); // directive -> component +DoubtfireAngularJSModule.directive( + 'fTaskStatusPieChart', + downgradeComponent({component: TaskStatusPieChartComponent}), +); +DoubtfireAngularJSModule.directive( + 'fProgressDashboard', + downgradeComponent({component: ProgressDashboardComponent}), +); DoubtfireAngularJSModule.directive( 'fProjectTasksList', downgradeComponent({component: ProjectTasksListComponent}), @@ -468,7 +484,23 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('newFUnits', downgradeComponent({component: FUnitsComponent})); -DoubtfireAngularJSModule.directive('unauthorised', downgradeComponent({ component: UnauthorisedComponent })); +DoubtfireAngularJSModule.directive( + 'fTutorials', + downgradeComponent({component: TutorialsComponent}), +); +DoubtfireAngularJSModule.directive( + 'unitStaffEditor', + downgradeComponent({component: UnitStaffEditorComponent}), +); +DoubtfireAngularJSModule.directive( + 'unauthorised', + downgradeComponent({component: UnauthorisedComponent}), +); + +DoubtfireAngularJSModule.directive( + 'fPortfolioGradeSelectStep', + downgradeComponent({component: PortfolioGradeSelectStepComponent}), +); // Global configuration @@ -483,13 +515,27 @@ const otherwiseConfigBlock = [ ]; DoubtfireAngularJSModule.config(otherwiseConfigBlock); - DoubtfireAngularJSModule.directive( 'fProgressBurndownChart', - downgradeComponent({ component: ProgressBurndownChartComponent }) + downgradeComponent({component: ProgressBurndownChartComponent}), ); DoubtfireAngularJSModule.directive( 'fTaskVisualisation', - downgradeComponent({ component: TaskVisualisationComponent }) + downgradeComponent({component: TaskVisualisationComponent}), +); + +DoubtfireAngularJSModule.directive( + 'fGroupMemberList', + downgradeComponent({component: GroupMemberListComponent}), +); + +DoubtfireAngularJSModule.directive( + 'fGroupSelector', + downgradeComponent({component: GroupSelectorComponent}), +); + +DoubtfireAngularJSModule.directive( + 'fGroupSetManager', + downgradeComponent({component: GroupSetManagerComponent}), ); diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index 7baa910f57..d2a7226cb2 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -14,6 +14,7 @@ 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 { Ng2ViewDeclaration } from '@uirouter/angular'; +import { TutorialsComponent } from './projects/states/tutorials/tutorials.component'; /* * Use this file to store any states that are sourced by angular components. @@ -411,6 +412,24 @@ const ScormPlayerReviewState: NgHybridStateDeclaration = { }, }; +const TutorialState: NgHybridStateDeclaration = { + name: 'projects/tutorials', + url: '/tutorials/project/:projectId', + views: { + main: { + component: TutorialsComponent, // Link to the Angular component + }, + }, + resolve: { + projectId: ['$stateParams', ($stateParams) => $stateParams.projectId], // Resolve the project object + }, + data: { + task: 'Tutorial List', + pageTitle: '_Home_', + roleWhiteList: ['Tutor', 'Convenor', 'Admin', 'Student', 'Auditor'], // Roles allowed to access this state + }, +}; + /** * Export the list of states we have created in angular */ @@ -433,4 +452,5 @@ export const doubtfireStates = [ ScormPlayerNormalState, ScormPlayerReviewState, ScormPlayerStudentReviewState, + TutorialState, ]; diff --git a/src/app/groups/group-member-list/group-member-list.coffee b/src/app/groups/group-member-list/group-member-list.coffee deleted file mode 100644 index ddb80ece67..0000000000 --- a/src/app/groups/group-member-list/group-member-list.coffee +++ /dev/null @@ -1,58 +0,0 @@ -angular.module('doubtfire.groups.group-member-list', []) - -# -# Lists members in a group -# -.directive('groupMemberList', -> - restrict: 'E' - templateUrl: 'groups/group-member-list/group-member-list.tpl.html' - scope: - unit: '=' - project: '=' - unitRole: '=' - selectedGroup: '=' - onMembersLoaded: '=?' - controller: ($scope, $timeout, gradeService, alertService, listenerService) -> - # Cleanup - listeners = listenerService.listenTo($scope) - - # Initial sort orders - $scope.tableSort = - order: 'student_name' - reverse: false - - # Table sorting - $scope.sortTableBy = (column) -> - $scope.tableSort.order = column - $scope.tableSort.reverse = !$scope.tableSort.reverse - - # Loading - startLoading = -> $scope.loaded = false - finishLoading = -> $timeout(-> - $scope.loaded = true - $scope.onMembersLoaded?() - , 500) - - # Initially not loaded - $scope.loaded = false - - # Remove group members - $scope.removeMember = (member) -> - $scope.selectedGroup.removeMember(member) - - # Listen for changes to group - listeners.push $scope.$watch "selectedGroup.id", (newGroupId) -> - return unless newGroupId? - startLoading() - $scope.canRemoveMembers = $scope.unitRole || ($scope.selectedGroup.groupSet.allowStudentsToManageGroups && !$scope.selectedGroup.locked) - - $scope.selectedGroup.getMembers().subscribe({ - next: (members) -> - finishLoading() - error: (failure) -> - $timeout((-> - alertService.error( "Unauthorised to view members in this group", 3000) - $scope.selectedGroup = null - ), 1000) - }) -) diff --git a/src/app/groups/group-member-list/group-member-list.component.html b/src/app/groups/group-member-list/group-member-list.component.html new file mode 100644 index 0000000000..aa9cc8ae8a --- /dev/null +++ b/src/app/groups/group-member-list/group-member-list.component.html @@ -0,0 +1,55 @@ +@if (loading) { +
+ Loading members... +
+} @else if (!selectedGroup || selectedGroup.members.length === 0) { +
+ group_off +

There are no members in this group

+
+} @else { + + + + + + + + + + + + + + + + + + + + + + + +
{{ unitRole ? 'Student ID' : '' }} + @if (unitRole) { + {{ member.student.username || 'N/A' }} + } + Name + {{ member.student.name }} + {{ unitRole ? 'Target Grade' : '' }} + @if (unitRole) { + + } + {{ canRemoveMembers ? 'Actions' : '' }} + @if (canRemoveMembers) { + @if (!project && unitRole) { + + } @else if (project && project.id === member.id) { + + } + } +
+} diff --git a/src/app/groups/group-member-list/group-member-list.component.scss b/src/app/groups/group-member-list/group-member-list.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/groups/group-member-list/group-member-list.component.ts b/src/app/groups/group-member-list/group-member-list.component.ts new file mode 100644 index 0000000000..3e71170de3 --- /dev/null +++ b/src/app/groups/group-member-list/group-member-list.component.ts @@ -0,0 +1,72 @@ +import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; +import {MatTableDataSource} from '@angular/material/table'; +import {Subscription} from 'rxjs'; +import {Group, UnitRole} from 'src/app/api/models/doubtfire-model'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-group-member-list', + templateUrl: './group-member-list.component.html', + styleUrls: ['./group-member-list.component.scss'], +}) +export class GroupMemberListComponent implements OnInit, OnChanges { + @Input() unit: Unit; + @Input() unitRole: UnitRole; + @Input() project: Project; + @Input() selectedGroup: Group; + @Input() onMembersLoaded: () => void; + + loading = false; + + canRemoveMembers = false; + + displayedColumns: string[] = ['student_id', 'name', 'target_grade', 'actions']; + groupMembers: Project[] = []; + dataSource = new MatTableDataSource(); + + private groupMembersSub?: Subscription; + + constructor(private alertService: AlertService) {} + + ngOnInit() { + if (!this.selectedGroup) { + return; + } + + this.groupMembersSub = this.selectedGroup.projectsCache.values.subscribe((values) => { + this.dataSource.data = values; + }); + } + + public removeMember(member: Project) { + this.selectedGroup.removeMember(member); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['selectedGroup'] && this.selectedGroup) { + this.loading = true; + this.selectedGroup.getMembers().subscribe({ + next: (members) => { + this.loading = false; + this.onMembersLoaded(); + this.canRemoveMembers = + !!this.unitRole || + (this.selectedGroup.groupSet.allowStudentsToManageGroups && !this.selectedGroup.locked); + + this.dataSource.data = members; + + this.groupMembersSub?.unsubscribe(); + this.groupMembersSub = this.selectedGroup.projectsCache.values.subscribe((values) => { + this.dataSource.data = values; + }); + }, + error: (error) => { + this.alertService.error(`Failed to fetch group members: ${error}`, 6000); + this.selectedGroup = null; + }, + }); + } + } +} diff --git a/src/app/groups/group-member-list/group-member-list.scss b/src/app/groups/group-member-list/group-member-list.scss deleted file mode 100644 index 75fb259794..0000000000 --- a/src/app/groups/group-member-list/group-member-list.scss +++ /dev/null @@ -1,6 +0,0 @@ -group-member-list table { - th.student-id { width: 25%; } - th.student-name { width: 50%; } - th.actions { width: 25%; } - th.student-grade { width: 50%; } -} diff --git a/src/app/groups/group-member-list/group-member-list.tpl.html b/src/app/groups/group-member-list/group-member-list.tpl.html deleted file mode 100644 index 8c3b3c2dc5..0000000000 --- a/src/app/groups/group-member-list/group-member-list.tpl.html +++ /dev/null @@ -1,55 +0,0 @@ -
- Loading Members... -
-
-
-

No members in group

-

There are no members in this group

-
-
- - - - - - - - - - - - - - - - - -
- - Student ID - - - - - Name - - - - - Target Grade - - - - Actions -
{{member.student.username || "N/A"}}{{member.student.name}} - - - - -
diff --git a/src/app/groups/group-selector/group-selector.coffee b/src/app/groups/group-selector/group-selector.coffee deleted file mode 100644 index f6e0a56745..0000000000 --- a/src/app/groups/group-selector/group-selector.coffee +++ /dev/null @@ -1,210 +0,0 @@ -angular.module('doubtfire.groups.group-selector', []) - -# -# Allows tutors and students to select (and create if applicable) -# new groups for teamwork -# -.directive('groupSelector', -> - restrict: 'E' - templateUrl: 'groups/group-selector/group-selector.tpl.html' - scope: - unit: "=" - # Use project for student context - project: "=?" - # Use unit role for tutor context - unitRole: "=?" - # Pass in a groupset to set the groupset context - selectedGroupSet: '=' - # Bind the selected group for switching - selectedGroup: '=?' - # Shows the groupset selector - showGroupSetSelector: '=?' - # On change of a group - onSelect: '=?' - controller: ($scope, $filter, $timeout, alertService, listenerService, newUserService, newGroupService) -> - # Cleanup - listeners = listenerService.listenTo($scope) - - # Unit role or project should be included in $scope - if !$scope.unitRole? && !$scope.project? || $scope.unitRole? && $scope.project? - throw Error "Group selector must have exactly one unit role or one project" - - # Filtering - applyFilters = -> - if $scope.unitRole? # apply staff filter - filteredGroups = $filter('groupsInTutorials')($scope.selectedGroupSet.groups, $scope.unitRole, $scope.staffFilter) - else # apply project filter - filteredGroups = $scope.selectedGroupSet.groups - # Apply remaining filters - $scope.filteredGroups = $filter('paginateAndSort')(filteredGroups, $scope.pagination, $scope.tableSort) - - $scope.setStaffFilter = (scope) -> - $scope.staffFilter = scope - applyFilters() - - # Pagination values - $scope.pagination = - currentPage: 1 - maxSize: 10 - pageSize: 10 - totalSize: null - show: false - onChange: applyFilters - - # Initial sort orders - $scope.tableSort = - order: 'name' - reverse: false - - # Table sorting - $scope.sortTableBy = (column) -> - $scope.tableSort.order = column - $scope.tableSort.reverse = !$scope.tableSort.reverse - applyFilters() - - # Loading - startLoading = -> $scope.loaded = false - finishLoading = -> $timeout((-> - $scope.loaded = true - if $scope.project? - $scope.selectGroup($scope.project.groupForGroupSet($scope.selectedGroupSet)) - ), 500) - - # Select group function - $scope.selectGroup = (group) -> - return if $scope.project? && ! $scope.project.inGroup(group) # its the student view - - $scope.selectedGroup = group - $scope.onSelect?(group) - - # Sets the placeholder text (useful to know named - # groups are technically optional) - resetNewGroupForm = () -> - $scope.newGroupName = "" - - # Group set selector - $scope.selectedGroupSet ?= _.first($scope.unit.groupSets) - $scope.showGroupSetSelector ?= $scope.unit.groupSets.length > 1 - $scope.selectGroupSet = (groupSet) -> - return unless groupSet? - startLoading() - $scope.selectGroup(null) - # Can only create groups if unitRole provided and selectedGroupSet - $scope.canCreateGroups = $scope.unitRole? || groupSet?.allowStudentsToCreateGroups - $scope.unit.getGroups(groupSet).subscribe({ - next: (groups) -> - $scope.selectedGroupSet = groupSet - finishLoading() - resetNewGroupForm() - applyFilters() - error: (message) -> - finishLoading() - alertService.error( "Unable to get groups #{message}", 6000) - }) - - $scope.selectGroupSet($scope.selectedGroupSet) - - # Load groups if not loaded - # $scope.unit.getGroups($scope.selectedGroupSet.id) if $scope.selectedGroupSet?.groups? - - # Staff filter options (convenor should see all) - $scope.staffFilter = { - Convenor: 'all', - Tutor: 'mine' - }[$scope.unitRole.role] if $scope.unitRole? - - # Changing staff filter reapplies filter - $scope.onChangeStaffFilter = applyFilters - - # Search text reapplies filter - $scope.searchTextChanged = applyFilters - - # Adds a group to the unit - $scope.addGroup = (name) -> - if $scope.unit.tutorials.length == 0 - alertService.error( "Please ensure there is at least one tutorial before groups are created", 6000) - # Student context - if $scope.project - #TODO: Need to add stream to group set - tutorialId = $scope.project.tutorials[0].id || $scope.unit.tutorials[0].id - else - # Convenor or Tutor - tutorName = $scope.unitRole?.name || newUserService.currentUser.name - tutorialId = _.find($scope.unit.tutorials, (tute) -> tute.tutor?.name == tutorName)?.id - # Default to first tutorial if can't find - tutorialId ?= _.first($scope.unit.tutorials).id - - newGroupService.create({ - unitId: $scope.unit.id, - groupSetId: $scope.selectedGroupSet.id, - }, { - cache: $scope.selectedGroupSet.groupsCache, - constructorParams: $scope.unit - body: { - group: { - name: name, - tutorial_id: tutorialId - } - } - }).subscribe({ - next: (group) -> - resetNewGroupForm() - applyFilters() - $scope.selectedGroup = group - error: (message) -> alertService.error( message, 6000) - }) - - # Join or leave group as project - $scope.projectInGroup = (group) -> - $scope.project?.inGroup(group) - - $scope.joinGroup = (group) -> - return unless $scope.project? - partOfGroup = $scope.projectInGroup(group) - return alertService.error( "You are already member of this group") if partOfGroup - group.addMember($scope.project, - () -> - $scope.selectedGroup = group - () -> - ) - - # Update group function - $scope.updateGroup = (data, group) -> - group.capacityAdjustment = data.capacityAdjustment - group.tutorial = data.tutorial - group.name = data.name - - newGroupService.update(group).subscribe({ - next: () -> - alertService.success( "Updated group", 2000) - applyFilters() - error: (message) -> alertService.error( "Failed to update group. #{message}", 6000) - }) - - # Remove group function - $scope.deleteGroup = (group) -> - newGroupService.delete(group, { cache: $scope.selectedGroupSet.groupsCache }).subscribe({ - next: () -> - alertService.success( "Deleted group", 2000) - $scope.selectedGroup = null if group.id == $scope.selectedGroup?.id - resetNewGroupForm() - applyFilters() - error: () -> alertService.error( "Failed to delete group. #{message}", 6000) - }) - - # Toggle lockable group - $scope.toggleLocked = (group) -> - group.locked = !group.locked - newGroupService.update(group).subscribe({ - next: (success) -> - group.locked = success.locked - alertService.success( "Group updated", 2000) - error: () -> alertService.error( "Failed to lock group. #{message}", 6000) - }) - - # Watch selected group set changes - listeners.push $scope.$on 'UnitGroupSetEditor/SelectedGroupSetChanged', (evt, args) -> - newGroupSet = $scope.unit.findGroupSet(args.id) - # return if newGroupSet == $scope.selectedGroupSet - $scope.selectGroupSet(newGroupSet) -) diff --git a/src/app/groups/group-selector/group-selector.component.html b/src/app/groups/group-selector/group-selector.component.html new file mode 100644 index 0000000000..b0984e69f3 --- /dev/null +++ b/src/app/groups/group-selector/group-selector.component.html @@ -0,0 +1,198 @@ + + +
+
+
+ + Groups for + @if (!showGroupSetSelector && selectedGroup) { + "{{ selectedGroupSet?.name }}" + } + + + @if (showGroupSetSelector) { + + + @for (gs of unit.groupSets; track gs.id) { + {{ gs.name }} + } + + + } +
+ @if (unitRole || selectedGroupSet?.allowStudentsToCreateGroups) { +
+ + + + +
+ } +
+ @if (unitRole) { +
+ + All Tutorials + My Tutorials + +
+ } +
+
+ + @if (selectedGroupSet && selectedGroupSet.groups.length === 0) { +
+ group_off +

There are no groups in this set

+
+ } @else { + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + @if (editing(group)) { + + + + } @else { + {{ group.name || 'Not set' }} + } + Tutorial + @if (editing(group)) { + + + @for (tutorial of unit.tutorials; track tutorial) { + {{ tutorial.abbreviation }} + } + + + } @else { + {{ group.tutorial.abbreviation }} + } + + @if (unitRole) { + Capacity Adjustment + } + + @if (unitRole) { + @if (editing(group)) { + + + + } @else { + {{ group.capacityAdjustment }} + } + } + Capacity + @if (group.hasSpace()) { + Available + } @else { + Full + } + + @if (unitRole || (project && selectedGroupSet.allowStudentsToManageGroups)) { + Actions + } + + @if (isPartOfGroup(project, group)) { +
Joined
+ } @else if (project && group.hasSpace() && selectedGroupSet.allowStudentsToManageGroups) { +
+ @if (!group.locked && !selectedGroupSet.locked) { + + } @else { + lock + } +
+ } + @if (unitRole) { +
+ @if (editing(group)) { +
+ + +
+ } @else { + + + + } +
+ } +
+ + } + + @if (selectedGroupSet.keepGroupsInSameClass && selectedGroupSet.groups.length > 0 && !unitRole) { +

+ Can't see the group you need to join? Groups shown are limited to those in your allocated + tutorials. Use the + Tutorial List to check and update + your tutorial enrolment if needed. +

+ } +
diff --git a/src/app/groups/group-selector/group-selector.component.scss b/src/app/groups/group-selector/group-selector.component.scss new file mode 100644 index 0000000000..200fdfce50 --- /dev/null +++ b/src/app/groups/group-selector/group-selector.component.scss @@ -0,0 +1,9 @@ +.mat-mdc-row .mat-mdc-cell { + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; + cursor: pointer; +} + +.mat-mdc-row:hover { + background-color: #eee; +} diff --git a/src/app/groups/group-selector/group-selector.component.ts b/src/app/groups/group-selector/group-selector.component.ts new file mode 100644 index 0000000000..f4847ba263 --- /dev/null +++ b/src/app/groups/group-selector/group-selector.component.ts @@ -0,0 +1,264 @@ +import { + AfterViewInit, + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild, +} from '@angular/core'; +import {UntypedFormControl, Validators} from '@angular/forms'; +import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {MatPaginator} from '@angular/material/paginator'; +import {MatTableDataSource} from '@angular/material/table'; +import {Subscription} from 'rxjs'; +import {Group, GroupSet, UnitRole, UserService} from 'src/app/api/models/doubtfire-model'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {GroupService} from 'src/app/api/services/group.service'; +import {EntityFormComponent} from 'src/app/common/entity-form/entity-form.component'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-group-selector', + templateUrl: './group-selector.component.html', + styleUrls: ['./group-selector.component.scss'], +}) +export class GroupSelectorComponent + extends EntityFormComponent + implements OnInit, OnChanges, AfterViewInit +{ + @Input() unit: Unit; + @Input() unitRole: UnitRole; + @Input() project: Project; + @Input() selectedGroup: Group; + @Input() selectedGroupSet: GroupSet; + @Input() onSelect: (group: Group) => void; + + @ViewChild(MatPaginator) paginator!: MatPaginator; + displayedColumns: string[] = ['name', 'tutorial', 'capacity_adjustment', 'capacity', 'actions']; + public groups: Group[] = []; + + public newGroupName: string; + public staffTutorialFilter: 'all' | 'mine' = 'all'; + + private groupsSub?: Subscription; + + constructor( + private userService: UserService, + private groupService: GroupService, + private alertService: AlertService, + ) { + super( + { + name: new UntypedFormControl('', [Validators.required]), + tutorial: new UntypedFormControl(null, [Validators.required]), + capacityAdjustment: new UntypedFormControl('', [Validators.required]), + }, + 'Group', + ); + } + + public get showGroupSetSelector() { + return this.unit.groupSets.length > 1; + } + + ngOnInit(): void { + if (this.unit.groupSets.length > 0) { + this.selectedGroupSet = this.unit.groupSets[0]; + } + } + + selectGroupSet(groupSet: GroupSet) { + this.selectedGroupSet = groupSet; + this.refreshGroups(); + } + + ngAfterViewInit() { + this.dataSource = new MatTableDataSource(); + this.dataSource.paginator = this.paginator; + + if (this.unit.groupSets.length > 0) { + this.selectedGroupSet = this.unit.groupSets[0]; + } + + this.refreshGroups(); + } + + refreshGroups() { + this.groupsSub?.unsubscribe(); + this.groupsSub = this.selectedGroupSet.groupsCache.values.subscribe((values) => { + this.groups = [...values]; + }); + this.applyFilters(); + } + + onGroupNameChange() { + this.applyFilters(); + } + + applyFilters() { + const filteredGroups = this.groups + .filter( + (g) => + this.staffTutorialFilter === 'all' || + (this.unitRole && g.tutorial.tutor.id === this.unitRole.user.id), + ) + .filter( + (g) => !this.newGroupName || g.name.toLowerCase().includes(this.newGroupName.toLowerCase()), + ); + + this.dataSource.data = filteredGroups.sort((a, b) => a.name.localeCompare(b.name)); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['selectedGroupSet'] && this.selectedGroupSet) { + if (!this.dataSource) { + this.dataSource = new MatTableDataSource(); + } + this.refreshGroups(); + } + } + + onTutorialFilterChange(event: MatButtonToggleChange) { + this.staffTutorialFilter = event.value; + this.applyFilters(); + } + + addGroup(name: string) { + if (this.unit.tutorials.length == 0) { + this.alertService.error( + `Please ensure there is at least one tutorial before groups are created`, + 6000, + ); + return; + } + let tutorialId = -1; + if (this.project) { + tutorialId = this.project.tutorials[0].id || this.unit.tutorials[0].id; + } else { + const tutorName = this.unitRole?.user.name || this.userService.currentUser.name; + tutorialId = + this.unit.tutorials.find((t) => t.tutor?.name === tutorName)?.id ?? + this.unit.tutorials[0].id; + } + + this.groupService + .create( + { + unitId: this.unit.id, + groupSetId: this.selectedGroupSet.id, + }, + { + cache: this.selectedGroupSet.groupsCache, + constructorParams: this.unit, + body: { + group: { + name, + tutorial_id: tutorialId, + }, + }, + }, + ) + .subscribe({ + next: (group) => { + this.alertService.success('Successfully created group', 3000); + this.selectedGroup = group; + this.newGroupName = ''; + this.applyFilters(); + }, + error: (error) => { + this.alertService.error(`Failed to create group: ${error}`); + }, + }); + } + + isPartOfGroup(project: Project, group: Group) { + return group && project?.inGroup(group); + } + + joinGroup(group: Group) { + if (!this.project) { + return; + } + + if (this.isPartOfGroup(this.project, group)) { + this.alertService.error('You are already member of this group'); + return; + } + + group.addMember(this.project, () => { + this.selectedGroup = group; + this.selectGroup(group); + }); + } + + selectGroup(group: Group) { + if (this.project && !this.project.inGroup(group)) { + // Return because we're in the student view + return; + } + + if (this.editing(group)) { + return; + } + + this.selectedGroup = group; + this.onSelect(group); + } + + deleteGroup(event: Event, group: Group) { + event.stopPropagation(); + + this.groupService.delete(group, {cache: this.selectedGroupSet.groupsCache}).subscribe({ + next: () => { + this.alertService.success('Deleted group', 3000); + if (group.id === this.selectedGroup?.id) { + this.selectedGroup = null; + this.selectGroup(null); + } + }, + error: (error) => { + this.alertService.error(`Failed to delete group: ${error}`, 6000); + }, + }); + } + + toggleLocked(event: Event, group: Group) { + event.stopPropagation(); + + const originalLockedState = group.locked; + group.locked = !group.locked; + + this.groupService.update(group).subscribe({ + next: (success) => { + group.locked = success.locked; + this.alertService.success(`Group has been ${!group.locked ? 'un' : ''}locked`, 3000); + }, + error: (error) => { + this.alertService.error(`Failed to ${!group.locked ? 'un' : ''}lock group: ${error}`, 6000); + group.locked = originalLockedState; + }, + }); + } + + startEditGroup(event: Event, group: Group) { + event.stopPropagation(); + this.flagEdit(group); + } + + cancelEditGroup(event: Event) { + event.stopPropagation(); + this.cancelEdit(); + } + + saveEdit(event: Event) { + event.stopPropagation(); + super.submit(this.groupService, this.alertService, this.onSuccess.bind(this)); + this.cancelEdit(); + } + + onSuccess(): void { + this.refreshGroups(); + } +} diff --git a/src/app/groups/group-selector/group-selector.scss b/src/app/groups/group-selector/group-selector.scss deleted file mode 100644 index c176bf951a..0000000000 --- a/src/app/groups/group-selector/group-selector.scss +++ /dev/null @@ -1,45 +0,0 @@ -group-selector { - display: block; -} -group-selector table { - th.name { - width: 25%; - } - th.tutorial { - width: 15%; - } - th.capacity_adjustment { - width: 15%; - } - th.capacity { - width: 15%; - } - th.actions { - width: 25%; - } -} -group-selector .panel-title > group-set-selector { - display: inline-block; - max-width: 50%; - padding-left: 1ex; -} -@media (max-width: $screen-md) { - group-selector .input-group.staff-filter { - margin-bottom: 1em; - &, - .btn-group { - width: 100%; - } - .btn { - width: 50%; - } - } -} - -.lockButton { - width: 70px; -} - -.joinButton { - width: 70px; -} diff --git a/src/app/groups/group-selector/group-selector.tpl.html b/src/app/groups/group-selector/group-selector.tpl.html deleted file mode 100644 index 3e52cc4402..0000000000 --- a/src/app/groups/group-selector/group-selector.tpl.html +++ /dev/null @@ -1,220 +0,0 @@ -
-

- Groups for - {{selectedGroupSet.name}} - - -

-
- -
-
-
-
- - -
-
- -
- - - - -
- -
-
- -
Loading Groups...
- -
-
-

No Groups To Show

-

- There are no groups available for {{selectedGroupSet.name}}{{staffFilter == 'mine' || - selectedGroupSet.keepGroupsInSameClass ? " in your tutorials." : ""}}{{newGroupName.length > 0 ? " with name " + newGroupName + "." : "."}} -

-

- Please make sure that you are enrolled in the correct tutorial. You can only join a group that is running in your - allocated tutorial. Use the Tutorial List to - check and update your tutorial enrolment. -

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - Name - - - - Tutorial - - - - - Capacity Adjustment - - - - - Capacity - - - - Actions -
- - {{ group.name || 'Not Set' }} - - - - - {{group.tutorial.abbreviation}} - - - - - {{group.capacityAdjustment}} - - - Available - Full - -
- - - - -
-
-
- - -
- - -
-
- diff --git a/src/app/groups/group-set-manager/group-set-manager.coffee b/src/app/groups/group-set-manager/group-set-manager.coffee deleted file mode 100644 index 5e8506f7cb..0000000000 --- a/src/app/groups/group-set-manager/group-set-manager.coffee +++ /dev/null @@ -1,44 +0,0 @@ -angular.module('doubtfire.groups.group-set-manager', []) - -# -# Manager directive for tutors to add and remove group -# members from a group within a group set context -# -.directive('groupSetManager', -> - restrict: 'E' - templateUrl: 'groups/group-set-manager/group-set-manager.tpl.html' - scope: - unit: '=' - unitRole: '=' - project: '=' - selectedGroupSet: '=' - showGroupSetSelector: '=?' - controller: ($scope, newGroupService, gradeService, alertService) -> - if !$scope.unitRole? && !$scope.project? - throw Error "Group set group manager must have exactly one unit role or project" - # Reset member panel toolbar visibility - $scope.newGroupSelected = -> - $scope.showMemberPanelToolbar = false if $scope.unitRole? - $scope.groupMembersLoaded = -> - $scope.showMemberPanelToolbar = true if $scope.unitRole? - - # Add new member to the group - $scope.addMember = (member) -> - $scope.selectedGroup.addMember(member) - $scope.selectedStudent = null - - # Update name of group - $scope.updateGroup = (data) -> - newGroupService.update({ - unitId: $scope.unit.id, - groupSetId: $scope.selectedGroupSet.id, - id: $scope.selectedGroup.id, - }, { - entity: data - }).subscribe({ - next: (response) -> - alertService.success( "Group changed", 2000) - error: (response) -> - alertService.error( response, 6000) - }) -) diff --git a/src/app/groups/group-set-manager/group-set-manager.component.html b/src/app/groups/group-set-manager/group-set-manager.component.html new file mode 100644 index 0000000000..1f209b3870 --- /dev/null +++ b/src/app/groups/group-set-manager/group-set-manager.component.html @@ -0,0 +1,75 @@ +
+ + + + @if (selectedGroup) { + + + Members of + @if (!editingGroupName) { + {{ selectedGroup?.name }} + @if (unitRole || selectedGroup.groupSet?.allowStudentsToManageGroups) { + + } + } @else { + @if (unitRole || selectedGroup.groupSet?.allowStudentsToManageGroups) { + + + + + + } + } + + @if (selectedGroup.locked) { + lock + } + + + + + @if (unitRole) { + + + + + @for (project of filteredProjects | async; track project) { + {{ project.student.name }} + } + + + + } + + } +
diff --git a/src/app/groups/group-set-manager/group-set-manager.component.scss b/src/app/groups/group-set-manager/group-set-manager.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/groups/group-set-manager/group-set-manager.component.ts b/src/app/groups/group-set-manager/group-set-manager.component.ts new file mode 100644 index 0000000000..d7b2430850 --- /dev/null +++ b/src/app/groups/group-set-manager/group-set-manager.component.ts @@ -0,0 +1,113 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {map, Observable, startWith} from 'rxjs'; +import {Group, GroupSet, Project, Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; +import {GroupService} from 'src/app/api/services/group.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-group-set-manager', + templateUrl: './group-set-manager.component.html', + styleUrls: ['./group-set-manager.component.scss'], +}) +export class GroupSetManagerComponent implements OnInit { + @Input() project: Project; + @Input() unit: Unit; + @Input() selectedGroupSet: GroupSet; + @Input() showGroupSetSelector: boolean; + @Input() unitRole: UnitRole; + + public selectedGroup: Group; + + editingGroupName = false; + + control = new FormControl(''); + projects: Project[] = []; + filteredProjects: Observable; + + constructor( + private groupService: GroupService, + private alertService: AlertService, + ) {} + + ngOnInit(): void { + this.filteredProjects = this.control.valueChanges.pipe( + startWith(''), + map((value) => this._filter(value)), + ); + } + + get groupSelectHandler() { + return (group: Group) => this.newGroupSelected(group); + } + + displayFn(project: Project): string { + return project && project.student.name ? project.student.name : ''; + } + + newGroupSelected(group: Group) { + if (this.selectedGroup) { + this.selectedGroup.name = this.originalGroupName; + } + this.editingGroupName = false; + this.selectedGroup = group; + + const students = this.unit.studentsForGroupTypeAhead(group) || []; + this.projects = students.filter((project) => !group.projects.find((p) => project.id === p.id)); + + this.originalGroupName = group.name; + } + + private _filter(value: string | Project): Project[] { + if (typeof value !== 'string') { + return; + } + + const filterValue = value.toLowerCase(); + return this.projects.filter( + (project) => + project.student.name.toLowerCase().includes(filterValue.toLowerCase()) && // Find by name + !this.selectedGroup.projects.find((p) => project.id === p.id), // Not already assigned to the group + ); + } + + addMember(project: Project) { + this.selectedGroup.addMember(project); + this.control.setValue(''); + } + + private originalGroupName: string; + startEditingGroupName() { + this.originalGroupName = this.selectedGroup.name; + this.editingGroupName = true; + } + + stopEditinGroupName() { + this.selectedGroup.name = this.originalGroupName; + this.editingGroupName = false; + } + + updateGroup() { + this.editingGroupName = false; + this.groupService + .update( + { + unitId: this.unit.id, + groupSetId: this.selectedGroup.groupSet.id, + id: this.selectedGroup.id, + }, + { + entity: this.selectedGroup, + }, + ) + .subscribe({ + next: () => { + this.alertService.success('Successfully updated group', 3000); + }, + error: (error) => { + this.selectedGroup.name = this.originalGroupName; + this.alertService.error(`Failed to update gorup: ${error}`, 6000); + }, + }); + } +} diff --git a/src/app/groups/group-set-manager/group-set-manager.scss b/src/app/groups/group-set-manager/group-set-manager.scss deleted file mode 100644 index b65662abca..0000000000 --- a/src/app/groups/group-set-manager/group-set-manager.scss +++ /dev/null @@ -1,6 +0,0 @@ -@media (min-width: $screen-lg) { - group-set-manager { - display: block; - @include panel-row; - } -} diff --git a/src/app/groups/group-set-manager/group-set-manager.tpl.html b/src/app/groups/group-set-manager/group-set-manager.tpl.html deleted file mode 100644 index 09386b1daa..0000000000 --- a/src/app/groups/group-set-manager/group-set-manager.tpl.html +++ /dev/null @@ -1,68 +0,0 @@ - - -
-
-
-
-

- Members of - {{selectedGroup.name}} -

-
-
- -
-
-
- -
-
- -
- -
-
-
- - - -
- diff --git a/src/app/groups/group-set-selector/group-set-selector.coffee b/src/app/groups/group-set-selector/group-set-selector.coffee deleted file mode 100644 index 62e99b74d0..0000000000 --- a/src/app/groups/group-set-selector/group-set-selector.coffee +++ /dev/null @@ -1,18 +0,0 @@ -angular.module('doubtfire.groups.group-set-selector', []) - -# -# Directive that can switch context of a specific group set -# -.directive('groupSetSelector', -> - restrict: 'E' - templateUrl: 'groups/group-set-selector/group-set-selector.tpl.html' - scope: - unit: '=' - selectedGroupSet: '=ngModel' - onSelectGroupSet: '=onChange' - controller: ($scope) -> - unless $scope.unit? - throw Error "Unit not supplied to group set selector" - $scope.selectGroupSet = -> - $scope.onSelectGroupSet?($scope.selectedGroupSet) -) diff --git a/src/app/groups/group-set-selector/group-set-selector.scss b/src/app/groups/group-set-selector/group-set-selector.scss deleted file mode 100644 index 6dee9e802c..0000000000 --- a/src/app/groups/group-set-selector/group-set-selector.scss +++ /dev/null @@ -1,7 +0,0 @@ -.groupset-selector .dropdown { - cursor: pointer; -} - -.lockButton { - width: 70px; -} diff --git a/src/app/groups/group-set-selector/group-set-selector.tpl.html b/src/app/groups/group-set-selector/group-set-selector.tpl.html deleted file mode 100644 index e735686bb4..0000000000 --- a/src/app/groups/group-set-selector/group-set-selector.tpl.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/src/app/groups/groups.coffee b/src/app/groups/groups.coffee index 031080f0b1..feae90ec11 100644 --- a/src/app/groups/groups.coffee +++ b/src/app/groups/groups.coffee @@ -1,7 +1,3 @@ angular.module('doubtfire.groups', [ 'doubtfire.groups.group-member-contribution-assigner' - 'doubtfire.groups.group-member-list' - 'doubtfire.groups.group-selector' - 'doubtfire.groups.group-set-manager' - 'doubtfire.groups.group-set-selector' ]) diff --git a/src/app/home/states/home/home.component.ts b/src/app/home/states/home/home.component.ts index b778d06460..3d6ab49bb8 100644 --- a/src/app/home/states/home/home.component.ts +++ b/src/app/home/states/home/home.component.ts @@ -1,6 +1,7 @@ import {Component, Inject, OnDestroy, OnInit, Renderer2} from '@angular/core'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -import {analyticsService, dateService} from 'src/app/ajs-upgraded-providers'; +import {analyticsService} from 'src/app/ajs-upgraded-providers'; +import {DateService} from 'src/app/common/services/date.service'; import {UIRouter} from '@uirouter/angular'; import {GlobalStateService, ViewType} from 'src/app/projects/states/index/global-state.service'; import {Project, UnitRole, User, UserService} from 'src/app/api/models/doubtfire-model'; @@ -28,7 +29,7 @@ export class HomeComponent implements OnInit, OnDestroy { private globalState: GlobalStateService, private userService: UserService, @Inject(analyticsService) private AnalyticsService: any, - @Inject(dateService) private DateService: any, + @Inject(DateService) private DateService: DateService, @Inject(UIRouter) private router: UIRouter, ) { // this.renderer.setStyle(document.body, 'background-color', '#f0f2f5'); diff --git a/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html index c1cd983626..812ba36201 100644 --- a/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html +++ b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html @@ -46,11 +46,16 @@

Targetting

- - - + [unit]="project.unit" + [grade]="project.targetGrade" + > +
diff --git a/src/app/projects/states/dashboard/dashboard.tpl.html b/src/app/projects/states/dashboard/dashboard.tpl.html index e79f1af1ca..2171877d44 100644 --- a/src/app/projects/states/dashboard/dashboard.tpl.html +++ b/src/app/projects/states/dashboard/dashboard.tpl.html @@ -7,14 +7,14 @@ style="padding: 0 8px 0 8px" > - - - restrict: 'E' - templateUrl: 'projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html' - scope: - project: '=' - onUpdateTargetGrade: '=' - controller: ($scope, $stateParams, newProjectService, gradeService, analyticsService, alertService) -> - # Is the current user a tutor? - $scope.tutor = $stateParams.tutor - # Number of tasks completed and remaining - updateTaskCompletionValues = -> - completedTasks = $scope.project.numberTasks("complete") - $scope.numberOfTasks = - completed: completedTasks - remaining: $scope.project.activeTasks().length - completedTasks - updateTaskCompletionValues() - - # Expose grade names and values - $scope.grades = - names: gradeService.grades - values: gradeService.gradeValues - - $scope.updateTargetGrade = (newGrade) -> - $scope.project.targetGrade = newGrade - newProjectService.update($scope.project).subscribe( - (project) -> - project.refreshBurndownChartData() - - # Update task completions and re-render task status graph - updateTaskCompletionValues() - $scope.renderTaskStatusPieChart?() - $scope.onUpdateTargetGrade?() - analyticsService.event("Student Project View - Progress Dashboard", "Grade Changed", $scope.grades.names[newGrade]) - alertService.success( "Updated target grade successfully", 2000) - - (failure) -> - alertService.error( "Failed to update target grade", 4000) - ) -) diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html new file mode 100644 index 0000000000..2fa31e0c7f --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html @@ -0,0 +1,74 @@ +
+
+

+ Progress Dashboard for {{ project.student.name }} +

+
+
+ +
+
+
+

Target Grade

+
+
+
+ + Select Target Grade + + + {{ grades.names[grade] }} + + + +
+
+ + +
+
+
+

Progress Burndown

+

+ The burndown chart shows how much work remains for you to achieve your target grade. +

+
+
+
+ +
+
+ Aim to keep your + Complete + line close to or ahead of the + Target + line to keep on track. +
+
+ + +
+
+
+

Task Statuses

+

+ Breakdown summary of each of your task statuses. +

+
+
+
+ +
+
+
+
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.scss b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts new file mode 100644 index 0000000000..ab9e3e6264 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts @@ -0,0 +1,67 @@ +import {Component, Input, Output, EventEmitter, OnInit, Inject} from '@angular/core'; +import {GradeService} from 'src/app/common/services/grade.service'; +import {analyticsService} from 'src/app/ajs-upgraded-providers'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {Project} from 'src/app/api/models/project'; + +@Component({ + selector: 'f-progress-dashboard', + templateUrl: './progress-dashboard.component.html', + styleUrls: ['./progress-dashboard.component.scss'], +}) +export class ProgressDashboardComponent implements OnInit { + @Input() project: Project; + @Output() doUpdateTargetGrade = new EventEmitter(); + + tutor: boolean; + grades = { + names: this.gradeService.grades, + values: this.gradeService.gradeValues, + }; + numberOfTasks = { + completed: 0, + remaining: 0, + }; + + constructor( + private gradeService: GradeService, + private projectService: ProjectService, + @Inject(analyticsService) private AnalyticsService, + private alertService: AlertService, + ) {} + + ngOnInit(): void { + this.updateTaskCompletionValues(); + this.tutor = this.project.myRole === 'Tutor' ? true : false; + } + + updateTargetGrade(newGrade: number): void { + this.project.targetGrade = newGrade; + this.projectService.update(this.project).subscribe( + (project) => { + project.refreshBurndownChartData(); + this.updateTaskCompletionValues(); + this.doUpdateTargetGrade.emit(); + this.AnalyticsService.event( + 'Student Project View - Progress Dashboard', + 'Grade Changed', + this.grades.names[newGrade], + ); + this.alertService.success('Updated target grade successfully', 2000); + }, + (error) => { + console.error('Error updating target grade:', error); + this.alertService.error('Failed to update target grade', 4000); + }, + ); + } + + private updateTaskCompletionValues(): void { + const completedTasks = this.project.numberTasks('complete'); + this.numberOfTasks = { + completed: completedTasks, + remaining: this.project.activeTasks().length - completedTasks, + }; + } +} diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html deleted file mode 100644 index 6cbfd88f44..0000000000 --- a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html +++ /dev/null @@ -1,58 +0,0 @@ -
-
-

- Progress Dashboard for {{project.student.name}} -

-
-
-
-
-
-

Target Grade

-
-
- -
-
-
-
-

Progress Burndown

-
- The burndown chart shows how much work remains for you to achieve your target grade. -
-
-
- -
- -
-
-
-
-
-

Task Statuses

-
- Breakdown summary of each of your task statuses. -
-
-
- - -
-
-
-
-
diff --git a/src/app/projects/states/groups/groups.tpl.html b/src/app/projects/states/groups/groups.tpl.html index e5086ab3c6..804d268d35 100644 --- a/src/app/projects/states/groups/groups.tpl.html +++ b/src/app/projects/states/groups/groups.tpl.html @@ -1,10 +1,8 @@
- - -
+ + + +
diff --git a/src/app/projects/states/portfolio/directives/directives.coffee b/src/app/projects/states/portfolio/directives/directives.coffee index 8ce9be0be3..661acd16e7 100644 --- a/src/app/projects/states/portfolio/directives/directives.coffee +++ b/src/app/projects/states/portfolio/directives/directives.coffee @@ -1,6 +1,5 @@ angular.module('doubtfire.projects.states.portfolio.directives', [ 'doubtfire.projects.states.portfolio.directives.portfolio-add-extra-files-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-grade-select-step' 'doubtfire.projects.states.portfolio.directives.portfolio-learning-summary-report-step' 'doubtfire.projects.states.portfolio.directives.portfolio-review-step' 'doubtfire.projects.states.portfolio.directives.portfolio-tasks-step' diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee deleted file mode 100644 index 3a7ee7f2a4..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee +++ /dev/null @@ -1,22 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-grade-select-step', []) - -# -# Allows students to select the target grade they are hoping -# to achieve with their portfolio -# -.directive('portfolioGradeSelectStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html' - controller: ($scope, newProjectService, gradeService) -> - if ! $scope.project.submittedGrade - $scope.project.submittedGrade = 0 - $scope.grades = gradeService.gradeValues - $scope.gradeName = (grade) -> gradeService.grades[grade] - $scope.agreedToAssessmentCriteria = $scope.projectHasLearningSummaryReport() - $scope.chooseGrade = (idx) -> - $scope.project.submittedGrade = idx - newProjectService.update($scope.project).subscribe((project) -> - $scope.project.refreshBurndownChartData() - ) -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html new file mode 100644 index 0000000000..ad2c4c4f0c --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html @@ -0,0 +1,96 @@ +
+ + + + +

Select Grade

+
+
+ + +

+ In preparing your portfolio, you need to undertake a self-assessment. Use the unit's + assessment criteria to determine the grade your portfolio should be awarded. +

+ + + + + + warning + Read the assessment criteria + + + + +

+ Make sure that you have reviewed the Assessment Criteria for the grade you are applying + for. Each grade will have a list of criteria that you can use to determine if you meet + the requirements to achieve that grade. +

+
+ + + + I have read the Assessment Criteria for this unit + + +
+ + + + @if (agreedToAssessmentCriteria) { + + + + Grade Application + + + + +

+ Select the grade you are applying for {{ unit.code }} + {{ unit.name }} below. +

+
+ + + + @for (grade of gradeValues; track grade) { + + + + } + + +

+ Make sure your Learning Summary Report justifies how your portfolio + demonstrates you have + met all unit learning outcomes to a {{ targetGrade }} level +

+
+
+ } +
+ + + + + + +
+
diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts new file mode 100644 index 0000000000..cce8ee1748 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts @@ -0,0 +1,57 @@ +import {Component, Injector, Input} from '@angular/core'; +import {Project, Unit} from 'src/app/api/models/doubtfire-model'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {GradeService} from 'src/app/common/services/grade.service'; + +@Component({ + selector: 'f-portfolio-grade-select-step', + templateUrl: 'portfolio-grade-select-step.component.html', + styleUrls: ['portfolio-grade-select-step.component.scss'], +}) +export class PortfolioGradeSelectStepComponent { + @Input() project: Project; + @Input() unit: Unit; + + public agreedToAssessmentCriteria: boolean = false; + + constructor( + private gradeService: GradeService, + private injector: Injector, + private projectService: ProjectService, + ) { + this.$scope = this.injector.get('$scope'); + } + + public get gradeValues() { + return this.gradeService.gradeValues; + } + + updateSubmittedGrade(newGrade: number): void { + const previousSubmittedGrade = this.project.submittedGrade; + this.project.submittedGrade = newGrade; + + this.projectService.update(this.project).subscribe( + (project) => { + project.refreshBurndownChartData?.(); + }, + (error) => { + this.project.submittedGrade = previousSubmittedGrade; + console.error('Error updating target grade:', error); + }, + ); + } + + // TODO: remove this once parent component has been migrated + private $scope: any; + goToNextStep(): void { + if (typeof this.$scope?.advanceActiveTab === 'function') { + this.$scope.advanceActiveTab(1); + } + } + + goToPreviousStep(): void { + if (typeof this.$scope?.advanceActiveTab === 'function') { + this.$scope.advanceActiveTab(-1); + } + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss deleted file mode 100644 index bfc229c4b1..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss +++ /dev/null @@ -1,10 +0,0 @@ -.project-portfolio-wizard .portfolio-grade-select-step { - .confirm-read-assessment-criteria { - font-size: 1.2em; - } - .select-the-grade { - .btn { - padding: 1em; - } - } -} diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html deleted file mode 100644 index 097b685e35..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html +++ /dev/null @@ -1,60 +0,0 @@ -
-
-

Select Grade

-
-
-

- In preparing your portfolio, you need to undertake a self assessment. Use the unit's assessment criteria to - determine the grade your portfolio should be awarded. -

-
-
-

Read the assessment criteria

- Make sure that you have reviewed the Assessment Criteria for the grade you are applying for. Each grade will - have a list of criteria that you can use to determine if you meet the requirements to achieve that grade. -
-
- - -
-
- -
-
-

Grade Application

- Select the grade you are applying for {{unit.name}} below. -
-
-
- -
-

- Make sure your Learning Summary Report justifies how your portfolio demonstrates you have - met all unit learning outcomes to a {{gradeName(project.submittedGrade)}} level -

-
-
- -
- - -
diff --git a/src/app/projects/states/portfolio/portfolio.tpl.html b/src/app/projects/states/portfolio/portfolio.tpl.html index 1c009b47ab..ae536e081f 100644 --- a/src/app/projects/states/portfolio/portfolio.tpl.html +++ b/src/app/projects/states/portfolio/portfolio.tpl.html @@ -7,7 +7,11 @@ - + + diff --git a/src/app/projects/states/states.coffee b/src/app/projects/states/states.coffee index 01b9d42dd4..a2a24c4bc4 100644 --- a/src/app/projects/states/states.coffee +++ b/src/app/projects/states/states.coffee @@ -1,7 +1,6 @@ angular.module('doubtfire.projects.states', [ 'doubtfire.projects.states.index' 'doubtfire.projects.states.dashboard' - 'doubtfire.projects.states.tutorials' 'doubtfire.projects.states.portfolio' 'doubtfire.projects.states.groups' 'doubtfire.projects.states.outcomes' diff --git a/src/app/projects/states/tutorials/tutorials.coffee b/src/app/projects/states/tutorials/tutorials.coffee deleted file mode 100644 index 5c22b609e3..0000000000 --- a/src/app/projects/states/tutorials/tutorials.coffee +++ /dev/null @@ -1,23 +0,0 @@ -angular.module('doubtfire.projects.states.tutorials', []) - -# -# Tasks state for projects -# -.config(($stateProvider) -> - $stateProvider.state 'projects/tutorials', { - parent: 'projects/index' - url: '/tutorials' - controller: 'ProjectsTutorialsStateCtrl' - templateUrl: 'projects/states/tutorials/tutorials.tpl.html' - data: - task: "Tutorial List" - pageTitle: "_Home_" - } -) - -.controller("ProjectsTutorialsStateCtrl", ($scope) -> - if $scope.unit.tutorialStreamsCache.size > 0 - $scope.sortOrder = 'tutorialStream.name' - else - $scope.sortOrder = 'abbreviation' -) diff --git a/src/app/projects/states/tutorials/tutorials.component.html b/src/app/projects/states/tutorials/tutorials.component.html new file mode 100644 index 0000000000..39ec57fbf8 --- /dev/null +++ b/src/app/projects/states/tutorials/tutorials.component.html @@ -0,0 +1,104 @@ +
+
+

Tutorials

+

+ View available tutorials and manage your enrolment. Note that availability is subject to + capacity. If you are unable to enrol in a tutorial, please contact your unit coordinator. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Stream + @if (unit.tutorialStreamsCache.size > 0) { +
{{ tutorial.tutorialStream?.name || 'All' }}
+ } @else { +
N/A
+ } +
Campus + {{ tutorial.campus?.name || 'All' }} + Code + {{ tutorial.abbreviation }} + Day + {{ tutorial.meetingDay }} + Time + {{ shortTime(tutorial.meetingTime) }} + Room + {{ tutorial.meetingLocation }} + Tutor + {{ tutorial.tutorName }} + Actions + @if (project.isEnrolledIn(tutorial)) { + @if (unit.allowStudentChangeTutorial) { + + } @else { +
+ Enrolled +
+ } + } @else if (unit.allowStudentChangeTutorial) { + + } @else { +
+ + } +
+
diff --git a/src/app/projects/states/tutorials/tutorials.component.scss b/src/app/projects/states/tutorials/tutorials.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/tutorials/tutorials.component.ts b/src/app/projects/states/tutorials/tutorials.component.ts new file mode 100644 index 0000000000..6a28239240 --- /dev/null +++ b/src/app/projects/states/tutorials/tutorials.component.ts @@ -0,0 +1,149 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {Sort} from '@angular/material/sort'; +import {MatTableDataSource} from '@angular/material/table'; +import {Tutorial, UnitService} from 'src/app/api/models/doubtfire-model'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {ProjectService} from 'src/app/api/services/project.service'; + +@Component({ + selector: 'f-tutorials', + templateUrl: './tutorials.component.html', + styleUrls: ['./tutorials.component.scss'], +}) +export class TutorialsComponent implements OnInit { + @Input() projectId: number; + + filteredTutorials: Tutorial[] = []; + + project: Project; + unit: Unit; + + displayedColumns: string[] = [ + 'stream', + 'campus', + 'code', + 'day', + 'time', + 'room', + 'tutor', + 'actions', + ]; + + dataSource = new MatTableDataSource([]); + + constructor( + private projectService: ProjectService, + private unitService: UnitService, + ) {} + + ngOnInit(): void { + this.projectService.fetch(this.projectId).subscribe({ + next: (project) => { + this.unitService.get(project.unit.id).subscribe({ + next: (unit) => { + this.unit = unit; + this.project = project; + this.filteredTutorials = this.tutorialCampusFilter([...unit.tutorials], this.project); + this.dataSource.data = this.filteredTutorials; + }, + error: (error) => { + console.error('Error fetching unit:', error); + }, + }); + }, + error: (error) => { + console.error('Error fetching project:', error); + }, + }); + } + + /** + * Switches to the passed-in tutorial. + * + * @param tutorial + * + * @returns void + */ + switchToTutorial(tutorial: Tutorial): void { + this.project.switchToTutorial(tutorial); + } + + /** + * Filters a collection of passed-in tutorials based on the campus_id of the passed-in project. + * + * @param tutorials + * @param project + * + * @returns Tutorial[] + */ + tutorialCampusFilter(tutorials: Tutorial[], project: Project): Tutorial[] { + if (!project) { + return tutorials; + } + return tutorials.filter((tutorial) => { + return ( + !project.campus?.id || + !tutorial.campus || + tutorial.campus.id === project.campus.id || + project.isEnrolledIn(tutorial) + ); + }); + } + + /** + * Formats the passed-in time string to the format of: HH:mm + * Todo: Add date validation + * @param meetingTime + * + * @returns string + */ + shortTime(meetingTime: string): string { + const [hours, minutes] = meetingTime.split(':'); + const formattedHours = hours.padStart(2, '0'); + const formattedMinutes = minutes.padStart(2, '0'); + + return `${formattedHours}:${formattedMinutes}`; + } + + private sortCompare(aValue: number | string, bValue: number | string, isAsc: boolean) { + return (aValue < bValue ? -1 : 1) * (isAsc ? 1 : -1); + } + + sortTableData(sort: Sort) { + if (!sort.active || sort.direction === '') { + return; + } + this.dataSource.data = this.dataSource.data.sort((a, b) => { + switch (sort.active) { + case 'stream': + return this.sortCompare( + a.tutorialStream?.name, + b.tutorialStream?.name, + sort.direction === 'asc', + ); + case 'campus': + return this.sortCompare(a.campus?.name, b.campus?.name, sort.direction === 'asc'); + case 'code': + return this.sortCompare(a.abbreviation, b.abbreviation, sort.direction === 'asc'); + case 'day': { + return this.sortCompare(a.meetingDay, b.meetingDay, sort.direction === 'asc'); + } + case 'time': { + return this.sortCompare( + this.shortTime(a.meetingTime), + this.shortTime(b.meetingTime), + sort.direction === 'asc', + ); + } + case 'room': { + return this.sortCompare(a.meetingLocation, b.meetingLocation, sort.direction === 'asc'); + } + case 'tutor': + return this.sortCompare(a.tutorName, b.tutorName, sort.direction === 'asc'); + default: + return 0; + } + }); + } +} diff --git a/src/app/projects/states/tutorials/tutorials.scss b/src/app/projects/states/tutorials/tutorials.scss deleted file mode 100644 index d402eae6a6..0000000000 --- a/src/app/projects/states/tutorials/tutorials.scss +++ /dev/null @@ -1,10 +0,0 @@ -#tutorials-state table { - th.stream { width: 10%; } - th.campus { width: 20%; } - th.code { width: 10%; } - th.day { width: 10%; } - th.time { width: 10%; } - th.room { width: 10%; } - th.tutor { width: 15%; } - th.actions { width: 15%; } -} diff --git a/src/app/projects/states/tutorials/tutorials.tpl.html b/src/app/projects/states/tutorials/tutorials.tpl.html deleted file mode 100644 index fae91d6ae3..0000000000 --- a/src/app/projects/states/tutorials/tutorials.tpl.html +++ /dev/null @@ -1,71 +0,0 @@ -
-
-
-

Select a Tutorial

-
-
-

- Click the plus on the specific tutorial to enrol in that tutorial, or click the minus icon to withdraw from your - current tutorial. -

-
- - - - - - - - - - - - - - - - - - - - - - - - - -
- Stream - - Campus - - Code - - Day - - Time - - Room - - Tutor - Actions
{{tutorial.tutorialStream.name || 'All'}}{{tutorial.campus ? tutorial.campus.name : 'All'}}{{tutorial.abbreviation}}{{tutorial.meetingDay}}{{tutorial.meetingTime | date: 'shortTime'}}{{tutorial.meetingLocation}}{{tutorial.tutorName}} - - -
-
-
diff --git a/src/app/units/states/edit/directives/directives.coffee b/src/app/units/states/edit/directives/directives.coffee index bfb12ed317..fe06bf4510 100644 --- a/src/app/units/states/edit/directives/directives.coffee +++ b/src/app/units/states/edit/directives/directives.coffee @@ -2,5 +2,4 @@ angular.module('doubtfire.units.states.edit.directives', [ 'doubtfire.units.states.edit.directives.unit-details-editor' 'doubtfire.units.states.edit.directives.unit-group-set-editor' 'doubtfire.units.states.edit.directives.unit-ilo-editor' - 'doubtfire.units.states.edit.directives.unit-staff-editor' ]) diff --git a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html index db550ef42d..c83b2d5fd7 100644 --- a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html +++ b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html @@ -123,18 +123,21 @@

No Group Sets Created

New Group Set -
-
- -
- +
+ + + + + + +

diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee deleted file mode 100644 index 3856daefe5..0000000000 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee +++ /dev/null @@ -1,58 +0,0 @@ -angular.module('doubtfire.units.states.edit.directives.unit-staff-editor', []) - -# -# Editor for adding new staff to a unit and assigning those staff -# members new unit roles within the unit -# -.directive('unitStaffEditor', -> - replace: true - restrict: 'E' - templateUrl: 'units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html' - controller: ($scope, $rootScope, alertService, newUnitService, newUnitRoleService) -> - temp = [] - users = [] - - $scope.unit.staffCache.values.subscribe( (staff) -> $scope.unitStaff = staff ) - - $scope.changeRole = (unitRole, role_id) -> - unitRole.roleId = role_id - newUnitRoleService.update(unitRole).subscribe({ - next: (response) -> alertService.success( "Role changed", 2000) - error: (response) -> alertService.error( response, 6000) - }) - - $scope.changeMainConvenor = (staff) -> - $scope.unit.changeMainConvenor(staff).subscribe({ - next: (response) -> - alertService.success( "Main convenor changed", 2000) - error: (response) -> - alertService.error( response, 6000) - }) - - $scope.addSelectedStaff = -> - staff = $scope.selectedStaff - $scope.selectedStaff = null - $scope.unit.staff = [] unless $scope.unit.staff - - if staff.id? - $scope.unit.addStaff(staff).subscribe({ - next: (response) -> alertService.success( "Staff member added", 2000) - error: (response) -> alertService.error( response, 6000) - }) - else - alertService.error( "Unable to add staff member. Ensure they have a tutor or convenor account in User admin first.", 6000) - - # Used in the typeahead to filter staff already in unit - $scope.filterStaff = (staff) -> - not _.find($scope.unit.staff, (listStaff) -> staff.id == listStaff.user.id) - - $scope.removeStaff = (staff) -> - newUnitRoleService.delete(staff, {cache: $scope.unit.staffCache}).subscribe({ - next: (response) -> alertService.success( "Staff member removed", 2000) - error: (response) -> alertService.error( response, 6000) - }) - - $scope.groupSetName = (id) -> - $scope.unit.groupSetsCache.get(id)?.name || "Individual Work" - -) diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html new file mode 100644 index 0000000000..9f3a65b727 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html @@ -0,0 +1,89 @@ +
+
+

Unit Staff

+

Manage unit staff by adding members and assigning them as convenors or tutors.

+
+ + + + + + + + + + + + + + + + + + + + +
Name +
+ {{ unitRole.user.name }} +
+
Role + + Tutor + Convenor + + Main Convenor + @if (unitRole?.role === 'Convenor') { + + } + Actions + +
+ + + + + {{ staff.name }} + + + +
diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts new file mode 100644 index 0000000000..7c53a04671 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts @@ -0,0 +1,166 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {UnitRoleService} from 'src/app/api/services/unit-role.service'; +import {Unit} from 'src/app/api/models/unit'; +import {User} from 'src/app/api/models/doubtfire-model'; +import {UnitRole} from 'src/app/api/models/unit-role'; +import {MatTableDataSource} from '@angular/material/table'; +import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; + +@Component({ + selector: 'unit-staff-editor', + templateUrl: 'unit-staff-editor.component.html', +}) +export class UnitStaffEditorComponent implements OnInit { + @Input() unit: Unit; + @Input() staff: User[]; + + temp = []; + users = []; + unitStaff: UnitRole[]; + filteredStaff: User[] = []; // Filtered staff members + searchTerm: string = ''; // Search term entered by the user + + displayedColumns: string[] = ['name', 'role', 'main-convenor', 'actions']; + dataSource = new MatTableDataSource(); + + // Inject services here + constructor( + private alertService: AlertService, + private unitRoleService: UnitRoleService, + private confirmationModalService: ConfirmationModalService, + ) {} + + ngOnInit(): void { + // Subscribe to staff cache + this.unit.staffCache.values.subscribe((staff: UnitRole[]) => { + this.unitStaff = staff; + this.dataSource.data = staff; + }); + } + + onRoleChange(unitRole: UnitRole, event: MatButtonToggleChange) { + const role = event.value; + if (role !== 'Tutor' && role !== 'Convenor') { + return; + } + const roleId = role === 'Tutor' ? 2 : 3; // map however you like + this.changeRole(unitRole, roleId, role); + } + /** + * Changes the role of a staff member. + * + * @param UnitRole unitRole + * @param number role_id + * + * @returns void + */ + changeRole(unitRole: UnitRole, roleId: number, role: string) { + const previousRoleId = unitRole.roleId; + const previousRole = unitRole.role; + + unitRole.roleId = roleId; + unitRole.role = role; + this.unitRoleService.update(unitRole).subscribe({ + next: () => this.alertService.success('Role changed', 2000), + error: (response) => { + // Revert changes on error + unitRole.roleId = previousRoleId; + unitRole.role = previousRole; + this.alertService.error(response, 6000); + }, + }); + } + + /** + * Changes who the `Main Convenor` of the unit is. + * + * @param UnitRole staff + * + * @returns void + */ + changeMainConvenor(staff: UnitRole) { + this.confirmationModalService.show( + 'Set Main Convenor', + `Do you want to make ${staff.user.name} the main convenor for this unit?`, + () => { + this.unit.changeMainConvenor(staff).subscribe({ + next: (_response) => this.alertService.success('Main convenor changed', 2000), + error: (response) => this.alertService.error(response, 6000), + }); + }, + ); + } + + /** + * Adds a staff member to the unit. + * + * @param User selectedStaff + * + * @returns void + */ + addSelectedStaff(selectedStaff: User) { + if (selectedStaff?.id) { + this.unit.addStaff(selectedStaff).subscribe({ + next: () => { + this.alertService.success('Staff member added', 2000); + this.searchTerm = ''; // Clear the input field + this.filterStaffList(); // Refilter the list + }, + error: (response) => this.alertService.error(response, 6000), + }); + } else { + this.alertService.error( + 'Unable to add staff member. Ensure they have a tutor or convenor account in User admin first', + ); + } + } + + /** + * Used in filtering the staff list. The `searchTerm` is bound to the auto-complete input in this class's template. + * + * @returns void + */ + filterStaffList(): void { + // `this.searchTerm` holds the selected staff member object from the dropdown OR the auto-complete input searchTerm (never at the same time). + // Thus, check the type here and exit early if string filtering is not needed. + if (typeof this.searchTerm !== 'string') { + return; + } + this.filteredStaff = this.staff.filter( + (staff) => + staff.matches(this.searchTerm.toLowerCase()) && // Find by name + !this.unit.staff.find((listStaff) => staff.id === listStaff.user.id), // Not already assigned to the unit + ); + } + + /** + * Generates a human-readable name made up of the passed-in staff member's `first` and `last` names. + * + * @param User staff + * + * @returns void + */ + displayStaffName(staff: User): string { + return staff ? staff.name : ''; + } + + /** + * Removes a staff member from the unit. + * + * @param UnitRole staff + * + * @returns void + */ + removeStaff(staff: UnitRole) { + this.unitRoleService.delete(staff, {cache: this.unit.staffCache}).subscribe({ + next: () => this.alertService.success('Staff member removed', 2000), + error: (response) => this.alertService.error(response, 6000), + }); + } + + groupSetName(id: number) { + this.unit.groupSetsCache.get(id).name || 'Individual Work'; + } +} diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html deleted file mode 100644 index f8b38cecb0..0000000000 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html +++ /dev/null @@ -1,101 +0,0 @@ -
- -
-
-

Modify Unit Staff

- Add staff members to the unit, assigning them a convenor or tutor role. -
-
-
-
This unit has no staff assigned
-
- - - - - - - - - - - - - - - - - - -
NameRoleMain ConvenorActions
- - {{staff.user.name}} -
- - -
-
- - - -
-
-
-
- -
-
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts index 72c309371d..bf4c3b626b 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts @@ -138,9 +138,7 @@ export class UnitTaskEditorComponent implements AfterViewInit { () => { this.unit.deleteTaskDefinition(taskDefinition); //TODO: reinstate ProgressModal.show "Deleting Task #{task.abbreviation}", 'Please wait while student projects are updated.', promise - - this.alerts.success('Task deleted'); - } + }, ); } diff --git a/src/app/units/states/edit/edit.tpl.html b/src/app/units/states/edit/edit.tpl.html index 3ba73129a5..a49ba3561d 100644 --- a/src/app/units/states/edit/edit.tpl.html +++ b/src/app/units/states/edit/edit.tpl.html @@ -6,7 +6,7 @@ - + diff --git a/src/app/units/states/groups/groups.tpl.html b/src/app/units/states/groups/groups.tpl.html index 319f46e08f..6d79bdb9a3 100644 --- a/src/app/units/states/groups/groups.tpl.html +++ b/src/app/units/states/groups/groups.tpl.html @@ -1,11 +1,8 @@ -
- - -
+
+ + +
+
diff --git a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html index 3f51c0d7dd..374cee4ce8 100644 --- a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html +++ b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html @@ -13,7 +13,9 @@ [yAxisLabel]="yAxisLabel" [yAxisTickFormatting]="formatPerc" [scheme]="colorScheme" + [yScaleMin]="yScaleMin" + [yScaleMax]="yScaleMax" (select)="onSelect($event)" - > + >
diff --git a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts index 4c1920154a..e49ca853dc 100644 --- a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts +++ b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts @@ -1,14 +1,13 @@ -import { Component, OnInit, Input, SimpleChanges, LOCALE_ID, ViewContainerRef } from '@angular/core'; -import { Project, Unit } from 'src/app/api/models/doubtfire-model'; -import { formatDate } from '@angular/common'; -import { MappingFunctions } from 'src/app/api/services/mapping-fn'; -import { AppInjector } from 'src/app/app-injector'; -import { ChartBaseComponent } from 'src/app/common/chart-base/chart-base-component/chart-base-component.component'; +import {Component, OnInit, Input, SimpleChanges, LOCALE_ID, ViewContainerRef} from '@angular/core'; +import {Project, Unit} from 'src/app/api/models/doubtfire-model'; +import {formatDate} from '@angular/common'; +import {AppInjector} from 'src/app/app-injector'; +import {ChartBaseComponent} from 'src/app/common/chart-base/chart-base-component/chart-base-component.component'; @Component({ selector: 'f-progress-burndown-chart', templateUrl: './progressburndownchart.component.html', - styleUrls: ['./progressburndownchart.component.scss'] + styleUrls: ['./progressburndownchart.component.scss'], }) export class ProgressBurndownChartComponent extends ChartBaseComponent implements OnInit { @Input() project: Project; @@ -28,9 +27,11 @@ export class ProgressBurndownChartComponent extends ChartBaseComponent implement showXAxisLabel: boolean = true; xAxisLabel: string = 'Time'; yAxisLabel: string = 'Tasks Remaining'; - colorScheme = { domain: ['#AAAAAA', '#777777', '#0079d8', '#E01B5D'] }; + colorScheme = {domain: ['#AAAAAA', '#777777', '#0079d8', '#E01B5D', 'transparent']}; + yScaleMin: number = 0; + yScaleMax: number = 100; - private seriesVisibility: { [key: string]: boolean } = {}; + private seriesVisibility: {[key: string]: boolean} = {}; constructor(public viewContainerRef: ViewContainerRef) { super(viewContainerRef); @@ -39,9 +40,6 @@ export class ProgressBurndownChartComponent extends ChartBaseComponent implement } ngOnInit(): void { - console.log('ProgressBurndownChartComponent: ngOnInit'); - console.log(this.project); - this.project.refreshBurndownChartData(); this.updateData(); this.data.forEach((item) => { @@ -56,49 +54,46 @@ export class ProgressBurndownChartComponent extends ChartBaseComponent implement } } - generateDates() { - const startDate: Date = this.project.unit.startDate; - const endDate: Date = this.project.unit.endDate; - const locale: string = AppInjector.get(LOCALE_ID); - const numberPoints = 10; - // Get the number of days between dates - const totalDays = MappingFunctions.daysBetween(startDate, endDate); - const interval = totalDays / (numberPoints - 1); // get gaps between points - - const dates = []; - for (let i = 0; i < numberPoints; i++) { - const date = MappingFunctions.daysAfter(startDate, interval * i); - dates.push(formatDate(date, 'd MMM', locale)); - } - - return dates; - } - updateData(): void { const chartData = this.project?.burndownChartData; - const dates = this.generateDates(); - - const formattedData = chartData.map((dataset) => { - const values = Array(10) - .fill(0) - .map((_, index) => dataset.values[index] || 0); - - const series = dates.map((date, index) => { - let value = values[index][1] ?? 0; - value = value * 100; - - if (value < 0) { - value = 0; - } + const locale: string = AppInjector.get(LOCALE_ID); + const startDate: Date = this.project.unit.startDate; + const endDate: Date = this.project.unit.endDate; - return { name: date, value }; - }); + if (!chartData) { + this.data = []; + return; + } - return { - name: dataset.key, - series, - }; - }); + const formattedData = chartData.map((series) => ({ + name: series.key, // Use the "key" as the "name" + series: series.values + .filter((value) => value[0] >= startDate.getTime() && value[0] <= endDate.getTime()) // Filter values based on the date range + .map((value) => { + if (value[1] < 0) { + value[1] = 0; // If the value is negative, set it to 0 + } + value[1] = Math.round(value[1] * 100); // Round the value to 2 decimal places + return { + name: formatDate(new Date(value[0]), 'd MMM', locale), // Format the timestamp as a date + value: value[1], + }; + }), + })); + + // Hack to get around yScaleMin and yScaleMax not working. + const target = formattedData.find((series) => series.name === 'Target'); + if (target) { + const start = target.series.find( + (point) => point.name === formatDate(new Date(startDate), 'd MMM', locale), + ); + const end = target.series.find( + (point) => point.name === formatDate(new Date(endDate), 'd MMM', locale), + ); + + if (start) start.value = 100; // Update start + if (end) end.value = 0; // Update end + } this.temp = JSON.parse(JSON.stringify(formattedData)); this.data = formattedData; diff --git a/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html new file mode 100644 index 0000000000..02f2e78dee --- /dev/null +++ b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html @@ -0,0 +1,12 @@ +
+ + +
diff --git a/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.scss b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts new file mode 100644 index 0000000000..1e9bb142d8 --- /dev/null +++ b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts @@ -0,0 +1,76 @@ +import {Component, OnInit, Input, SimpleChanges} from '@angular/core'; +import {Project, TaskStatus} from 'src/app/api/models/doubtfire-model'; +import {ChartBaseComponent} from 'src/app/common/chart-base/chart-base-component/chart-base-component.component'; + +@Component({ + selector: 'f-task-status-pie-chart', + templateUrl: './taskstatuspiechart.component.html', + styleUrls: ['./taskstatuspiechart.component.scss'], +}) +export class TaskStatusPieChartComponent extends ChartBaseComponent implements OnInit { + @Input() project: Project; + @Input() grade: number; + + data: {name: string; value: number}[] = []; + colors: {name: string; value: string}[]; + view: number[] = [700, 400]; + + ngOnInit(): void { + this.updateData(); + } + + ngOnChanges(changes: SimpleChanges): void { + if ('grade' in changes && changes.grade.currentValue !== undefined) { + this.updateData(); + } + } + + updateData(): void { + if (this.project) { + const taskCounts = new Map(TaskStatus.STATUS_KEYS.map((status) => [status, 0])); + const activeTasks = this.project.activeTasks(); + activeTasks.forEach((task) => { + if (task.status) { + taskCounts.set(task.status, (taskCounts.get(task.status) || 0) + 1); + } + }); + + const sortOrder = [ + 'not_started', + 'feedback_exceeded', + 'redo', + 'need_help', + 'working_on_it', + 'fix_and_resubmit', + 'ready_for_feedback', + 'discuss', + 'demonstrate', + 'complete', + 'fail', + 'time_exceeded', + ]; + + this.data = Array.from(taskCounts) + .map(([status, count]) => { + return { + name: TaskStatus.STATUS_LABELS.get(status), + value: count, + }; + }) + .filter((task) => task.value > 0 || sortOrder.includes(task.name)) + .sort((a, b) => { + let aIndex = sortOrder.indexOf(a.name); + let bIndex = sortOrder.indexOf(b.name); + + aIndex = aIndex === -1 ? sortOrder.length : aIndex; + bIndex = bIndex === -1 ? sortOrder.length : bIndex; + + return aIndex - bIndex; + }); + + this.colors = Array.from(TaskStatus.STATUS_COLORS).map(([status, color]) => { + return {name: TaskStatus.STATUS_LABELS.get(status), value: color}; + }); + } + } +}