diff --git a/src/app/courseflow/common/skills-summary-dialog/skills-summary-dialog.component.html b/src/app/courseflow/common/skills-summary-dialog/skills-summary-dialog.component.html new file mode 100644 index 0000000000..54cb1dd9dc --- /dev/null +++ b/src/app/courseflow/common/skills-summary-dialog/skills-summary-dialog.component.html @@ -0,0 +1,65 @@ +
+
+

Skills Summary

+ +
+ +
+ +
+

+ check_circle + Skills Acquired ({{ getCompletedUnits().length }} completed units) +

+
+
+
+ {{ unit.code }} - {{ unit.name }} + check_circle +
+
+ {{ unit.description }} +
+
+ No skills description available for this unit. +
+
+
+
+ + +
+

+ schedule + Skills in Development ({{ getInProgressUnits().length }} units in progress) +

+
+
+
+ {{ unit.code }} - {{ unit.name }} + schedule +
+
+ {{ unit.description }} +
+
+ No skills description available for this unit. +
+
+
+
+ + +
+ lightbulb_outline +

No Units Selected

+

Add units to your course plan to see a summary of skills you'll learn.

+
+
+ +
+ +
+
diff --git a/src/app/courseflow/common/skills-summary-dialog/skills-summary-dialog.component.scss b/src/app/courseflow/common/skills-summary-dialog/skills-summary-dialog.component.scss new file mode 100644 index 0000000000..b97e3d2f0b --- /dev/null +++ b/src/app/courseflow/common/skills-summary-dialog/skills-summary-dialog.component.scss @@ -0,0 +1,143 @@ +.skills-summary-dialog { + max-width: 800px; + width: 100%; + max-height: 80vh; +} + +.dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px 0; + border-bottom: 1px solid #e0e0e0; + margin-bottom: 20px; + + h2 { + margin: 0; + color: #333; + font-weight: 500; + } + + .close-button { + color: #666; + } +} + +.dialog-content { + padding: 0 24px; + max-height: 60vh; + overflow-y: auto; +} + +.section { + margin-bottom: 30px; + + .section-title { + display: flex; + align-items: center; + margin-bottom: 15px; + color: #333; + font-size: 18px; + font-weight: 500; + + .section-icon { + margin-right: 8px; + + &.completed { + color: #4caf50; + } + + &.in-progress { + color: #ff9800; + } + } + } +} + +.units-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.unit-item { + padding: 16px; + border-radius: 8px; + border: 1px solid #e0e0e0; + background-color: #fafafa; + + &.completed { + border-left: 4px solid #4caf50; + background-color: #f3f9f3; + } + + &.in-progress { + border-left: 4px solid #ff9800; + background-color: #fff8f0; + } + + .unit-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + + strong { + color: #333; + font-size: 16px; + } + + .status-icon { + font-size: 20px; + + &.completed { + color: #4caf50; + } + + &.in-progress { + color: #ff9800; + } + } + } + + .unit-description { + color: #555; + line-height: 1.6; + font-size: 14px; + } + + .no-description { + color: #999; + font-style: italic; + font-size: 14px; + } +} + +.no-units { + text-align: center; + padding: 40px 20px; + color: #666; + + .empty-icon { + font-size: 48px; + color: #ccc; + margin-bottom: 16px; + } + + h3 { + margin: 0 0 12px 0; + color: #555; + } + + p { + margin: 0; + color: #777; + } +} + +.dialog-actions { + padding: 16px 24px; + border-top: 1px solid #e0e0e0; + display: flex; + justify-content: flex-end; +} diff --git a/src/app/courseflow/common/skills-summary-dialog/skills-summary-dialog.component.ts b/src/app/courseflow/common/skills-summary-dialog/skills-summary-dialog.component.ts new file mode 100644 index 0000000000..21ffbd98a1 --- /dev/null +++ b/src/app/courseflow/common/skills-summary-dialog/skills-summary-dialog.component.ts @@ -0,0 +1,46 @@ +import {Component, Inject} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {MatDialogModule, MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {CourseUnit} from '../../models/course-map.models'; + +interface SkillsSummaryData { + units: CourseUnit[]; +} + +@Component({ + selector: 'skills-summary-dialog', + templateUrl: './skills-summary-dialog.component.html', + styleUrls: ['./skills-summary-dialog.component.scss'], + standalone: true, + imports: [CommonModule, MatDialogModule, MatButtonModule, MatIconModule], +}) +export class SkillsSummaryDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: SkillsSummaryData, + ) {} + + onClose(): void { + this.dialogRef.close(); + } + + getCompletedUnits(): CourseUnit[] { + return this.data.units.filter(unit => { + const unitWithCompletion = unit as CourseUnit & {isCompleted?: boolean}; + return unitWithCompletion.isCompleted; + }); + } + + getInProgressUnits(): CourseUnit[] { + return this.data.units.filter(unit => { + const unitWithCompletion = unit as CourseUnit & {isCompleted?: boolean}; + return !unitWithCompletion.isCompleted; + }); + } + + getAllPlacedUnits(): CourseUnit[] { + return this.data.units.filter(unit => unit !== null); + } +} diff --git a/src/app/courseflow/common/unit-card/unit-card.component.html b/src/app/courseflow/common/unit-card/unit-card.component.html index d69ca3d12a..e77ce80730 100644 --- a/src/app/courseflow/common/unit-card/unit-card.component.html +++ b/src/app/courseflow/common/unit-card/unit-card.component.html @@ -1,12 +1,28 @@ -
+
{{ unit.code }} - {{ unit.name }} + check_circle - + + diff --git a/src/app/courseflow/common/unit-card/unit-card.component.scss b/src/app/courseflow/common/unit-card/unit-card.component.scss index 332ccfd351..6a9d843451 100644 --- a/src/app/courseflow/common/unit-card/unit-card.component.scss +++ b/src/app/courseflow/common/unit-card/unit-card.component.scss @@ -26,6 +26,25 @@ background-color: #f2f2f2; transform: translateY(-2px); } + + &.completed { + background-color: #e8f5e8; + border-color: #4caf50; + + &:hover { + background-color: #d4edda; + } + } + + &.drag-disabled { + cursor: not-allowed; + opacity: 0.8; + + &:hover { + transform: none; + background-color: #e8f5e8; + } + } } .unit-menu-button { @@ -46,3 +65,13 @@ height: 18px; line-height: 18px; } + +.completion-indicator { + position: absolute; + top: 5px; + left: 5px; + color: #4caf50; + font-size: 16px; + width: 16px; + height: 16px; +} diff --git a/src/app/courseflow/common/unit-card/unit-card.component.ts b/src/app/courseflow/common/unit-card/unit-card.component.ts index d2955e9e1d..acdd779879 100644 --- a/src/app/courseflow/common/unit-card/unit-card.component.ts +++ b/src/app/courseflow/common/unit-card/unit-card.component.ts @@ -18,8 +18,29 @@ export class UnitCardComponent { @Input() dragData!: any; @Input() showMenu = false; @Output() removeUnit = new EventEmitter(); + @Output() toggleCompletion = new EventEmitter(); + @Output() showSkillsSummary = new EventEmitter(); + + // Track completion status locally if not available on the unit + get isCompleted(): boolean { + // Check if unit has a completion property, otherwise use local storage or default to false + const unitWithCompletion = this.unit as CourseUnit & {isCompleted?: boolean}; + return unitWithCompletion.isCompleted || false; + } onRemoveUnit(): void { + // Don't allow removal of completed units + if (this.isCompleted) { + return; + } this.removeUnit.emit(); } + + onToggleCompletion(): void { + this.toggleCompletion.emit(); + } + + onShowSkillsSummary(): void { + this.showSkillsSummary.emit(this.unit); + } } diff --git a/src/app/courseflow/services/course-map-state.service.ts b/src/app/courseflow/services/course-map-state.service.ts index 3289ef30e9..7582cb1c84 100644 --- a/src/app/courseflow/services/course-map-state.service.ts +++ b/src/app/courseflow/services/course-map-state.service.ts @@ -386,4 +386,16 @@ export class CourseMapStateService { requiredUnits: units, }); } + + toggleUnitCompletion(unit: CourseUnit): void { + // For now, we'll store completion status on the unit object itself + // In a real implementation, this might be stored in a service or backend + const unitWithCompletion = unit as CourseUnit & {isCompleted?: boolean}; + unitWithCompletion.isCompleted = !unitWithCompletion.isCompleted; + + // Trigger a state update to ensure components re-render + this.updateState({ + ...this.currentState, + }); + } } diff --git a/src/app/courseflow/states/coursemap/coursemap.component.html b/src/app/courseflow/states/coursemap/coursemap.component.html index c8c32910e2..f22c5ae49d 100644 --- a/src/app/courseflow/states/coursemap/coursemap.component.html +++ b/src/app/courseflow/states/coursemap/coursemap.component.html @@ -31,6 +31,7 @@

Study Periods

[yearIndex]="yIndex" [stateService]="stateService" (dropEvent)="handleDrop($event)" + (showSkillsSummary)="onShowSkillsSummary($event)" > diff --git a/src/app/courseflow/states/coursemap/coursemap.component.ts b/src/app/courseflow/states/coursemap/coursemap.component.ts index a21050fbe4..27e55ded8b 100644 --- a/src/app/courseflow/states/coursemap/coursemap.component.ts +++ b/src/app/courseflow/states/coursemap/coursemap.component.ts @@ -2,11 +2,12 @@ import {Component, OnInit, OnDestroy} from '@angular/core'; import {CommonModule} from '@angular/common'; import {MatIconModule} from '@angular/material/icon'; import {MatButtonModule} from '@angular/material/button'; +import {MatDialog} from '@angular/material/dialog'; import {DragDropModule} from '@angular/cdk/drag-drop'; import {Subject, takeUntil} from 'rxjs'; import {CourseMapStateService} from '../../services/course-map-state.service'; import {CourseMapDragDropService} from '../../services/course-map-drag-drop.service'; -import {CourseMapState} from '../../models/course-map.models'; +import {CourseMapState, CourseUnit} from '../../models/course-map.models'; import { UnitService, CourseService, @@ -23,6 +24,7 @@ import {CourseYearEditorComponent} from './directives/course-year-editor/course- import {RequiredUnitsListComponent} from './directives/required-units-list/required-units-list.component'; import {ElectiveUnitsListComponent} from './directives/elective-units-list/elective-units-list.component'; import {UnitSearchComponent} from './directives/unit-search/unit-search.component'; +import {SkillsSummaryDialogComponent} from '../../common/skills-summary-dialog/skills-summary-dialog.component'; @Component({ selector: 'coursemap', @@ -38,6 +40,7 @@ import {UnitSearchComponent} from './directives/unit-search/unit-search.componen RequiredUnitsListComponent, ElectiveUnitsListComponent, UnitSearchComponent, + SkillsSummaryDialogComponent, ], providers: [ UnitService, @@ -71,6 +74,7 @@ export class CoursemapComponent implements OnInit, OnDestroy { private courseMapUnitService: CourseMapUnitService, private authService: AuthenticationService, private alerts: AlertService, + private dialog: MatDialog, ) { this.state = this.stateService.currentState; } @@ -252,4 +256,32 @@ export class CoursemapComponent implements OnInit, OnDestroy { trackByYear(index: number, year: any): number { return year.year; } + + onShowSkillsSummary(unit: CourseUnit): void { + // Collect all units placed in the course map + const allPlacedUnits: CourseUnit[] = []; + + this.state.years.forEach(year => { + Object.keys(year).forEach(key => { + if (key.startsWith('trimester')) { + const trimester = year[key as keyof typeof year] as (CourseUnit | null)[]; + if (trimester) { + trimester.forEach(u => { + if (u) { + allPlacedUnits.push(u); + } + }); + } + } + }); + }); + + this.dialog.open(SkillsSummaryDialogComponent, { + width: '800px', + maxWidth: '90vw', + data: { + units: allPlacedUnits + } + }); + } } diff --git a/src/app/courseflow/states/coursemap/directives/course-year-editor/course-year-editor.component.html b/src/app/courseflow/states/coursemap/directives/course-year-editor/course-year-editor.component.html index bbc6465930..1d66c93137 100644 --- a/src/app/courseflow/states/coursemap/directives/course-year-editor/course-year-editor.component.html +++ b/src/app/courseflow/states/coursemap/directives/course-year-editor/course-year-editor.component.html @@ -21,6 +21,7 @@

Year {{ year.year }}

[stateService]="stateService" (dropEvent)="onTrimesterDrop($event)" (deleteTrimester)="deleteTrimester(tIndex)" + (showSkillsSummary)="onShowSkillsSummary($event)" > diff --git a/src/app/courseflow/states/coursemap/directives/course-year-editor/course-year-editor.component.ts b/src/app/courseflow/states/coursemap/directives/course-year-editor/course-year-editor.component.ts index e38f889b5f..deadd9d006 100644 --- a/src/app/courseflow/states/coursemap/directives/course-year-editor/course-year-editor.component.ts +++ b/src/app/courseflow/states/coursemap/directives/course-year-editor/course-year-editor.component.ts @@ -2,7 +2,7 @@ import {Component, Input, Output, EventEmitter} from '@angular/core'; import {CommonModule} from '@angular/common'; import {MatIconModule} from '@angular/material/icon'; import {MatButtonModule} from '@angular/material/button'; -import {CourseYear, TRIMESTER_KEYS} from '../../../../models/course-map.models'; +import {CourseYear, CourseUnit, TRIMESTER_KEYS} from '../../../../models/course-map.models'; import {CourseMapStateService} from '../../../../services/course-map-state.service'; import {TrimesterEditorComponent} from '../trimester-editor/trimester-editor.component'; @@ -19,6 +19,7 @@ export class CourseYearEditorComponent { @Input() stateService!: CourseMapStateService; // eslint-disable-next-line @typescript-eslint/no-explicit-any @Output() dropEvent = new EventEmitter(); + @Output() showSkillsSummary = new EventEmitter(); readonly trimesterKeys = TRIMESTER_KEYS; @@ -46,4 +47,8 @@ export class CourseYearEditorComponent { onTrimesterDrop(event: any): void { this.dropEvent.emit(event); } + + onShowSkillsSummary(unit: CourseUnit): void { + this.showSkillsSummary.emit(unit); + } } diff --git a/src/app/courseflow/states/coursemap/directives/trimester-editor/trimester-editor.component.html b/src/app/courseflow/states/coursemap/directives/trimester-editor/trimester-editor.component.html index bf3758676e..2ceb0f789c 100644 --- a/src/app/courseflow/states/coursemap/directives/trimester-editor/trimester-editor.component.html +++ b/src/app/courseflow/states/coursemap/directives/trimester-editor/trimester-editor.component.html @@ -12,6 +12,8 @@ [slotIndex]="slotIndex" (dropEvent)="onSlotDrop($event)" (removeUnit)="onRemoveUnit(slotIndex)" + (toggleCompletion)="onToggleCompletion($event)" + (showSkillsSummary)="onShowSkillsSummary($event)" >
diff --git a/src/app/courseflow/states/coursemap/directives/trimester-editor/trimester-editor.component.ts b/src/app/courseflow/states/coursemap/directives/trimester-editor/trimester-editor.component.ts index 2e36aac81e..3f239b2d58 100644 --- a/src/app/courseflow/states/coursemap/directives/trimester-editor/trimester-editor.component.ts +++ b/src/app/courseflow/states/coursemap/directives/trimester-editor/trimester-editor.component.ts @@ -21,6 +21,7 @@ export class TrimesterEditorComponent { @Input() stateService!: CourseMapStateService; @Output() dropEvent = new EventEmitter(); @Output() deleteTrimester = new EventEmitter(); + @Output() showSkillsSummary = new EventEmitter(); readonly slotIndices = [0, 1, 2, 3]; @@ -40,6 +41,14 @@ export class TrimesterEditorComponent { this.stateService.removeUnitFromSlot(this.yearIndex, this.trimesterKey, slotIndex); } + onToggleCompletion(unit: CourseUnit): void { + this.stateService.toggleUnitCompletion(unit); + } + + onShowSkillsSummary(unit: CourseUnit): void { + this.showSkillsSummary.emit(unit); + } + trackBySlotIndex(index: number): number { return index; } diff --git a/src/app/courseflow/states/coursemap/directives/unit-slot/unit-slot.component.html b/src/app/courseflow/states/coursemap/directives/unit-slot/unit-slot.component.html index 64de599fc8..8b13e158be 100644 --- a/src/app/courseflow/states/coursemap/directives/unit-slot/unit-slot.component.html +++ b/src/app/courseflow/states/coursemap/directives/unit-slot/unit-slot.component.html @@ -12,6 +12,8 @@ [dragData]="dragData" [showMenu]="true" (removeUnit)="onRemoveUnit()" + (toggleCompletion)="onToggleCompletion()" + (showSkillsSummary)="onShowSkillsSummary($event)" >
diff --git a/src/app/courseflow/states/coursemap/directives/unit-slot/unit-slot.component.ts b/src/app/courseflow/states/coursemap/directives/unit-slot/unit-slot.component.ts index a5ce8dc291..f096e46ac2 100644 --- a/src/app/courseflow/states/coursemap/directives/unit-slot/unit-slot.component.ts +++ b/src/app/courseflow/states/coursemap/directives/unit-slot/unit-slot.component.ts @@ -29,6 +29,8 @@ export class UnitSlotComponent { @Input() slotIndex!: number; @Output() dropEvent = new EventEmitter(); @Output() removeUnit = new EventEmitter(); + @Output() toggleCompletion = new EventEmitter(); + @Output() showSkillsSummary = new EventEmitter(); get dropListId(): string { return `${this.trimesterKey}-${this.yearIndex}-slot-${this.slotIndex}`; @@ -59,4 +61,14 @@ export class UnitSlotComponent { onRemoveUnit(): void { this.removeUnit.emit(); } + + onToggleCompletion(): void { + if (this.unit) { + this.toggleCompletion.emit(this.unit); + } + } + + onShowSkillsSummary(unit: CourseUnit): void { + this.showSkillsSummary.emit(unit); + } }