{{ keyResult.title }}
*ngIf="keyResultMetric.lastCheckIn as lastCheckIn"
class="keyResult-detail-attribute-show rounded-5 p-2 metric-col d-flex justify-content-center gap-1"
[ngClass]="{
- 'border-error': isInValid(keyResultMetric.baseline, keyResultMetric.stretchGoal, +lastCheckIn.value)
+ 'border-error': calculateCurrentPercentage(keyResultMetric) < 1
}"
>
-
!
+
!
Aktuell:
{{ +lastCheckIn.value | unitValueTransformation: keyResultMetric.unit.toString() }}
diff --git a/frontend/src/app/keyresult-detail/keyresult-detail.component.ts b/frontend/src/app/keyresult-detail/keyresult-detail.component.ts
index 172a1b7c72..5326c6d430 100644
--- a/frontend/src/app/keyresult-detail/keyresult-detail.component.ts
+++ b/frontend/src/app/keyresult-detail/keyresult-detail.component.ts
@@ -12,7 +12,7 @@ import { CloseState } from '../shared/types/enums/CloseState';
import { CheckInFormComponent } from '../shared/dialog/checkin/check-in-form/check-in-form.component';
import { State } from '../shared/types/enums/State';
import { CONFIRM_DIALOG_WIDTH, DATE_FORMAT } from '../shared/constantLibary';
-import { isInValid, isMobileDevice } from '../shared/common';
+import { calculateCurrentPercentage, isLastCheckInNegative, isMobileDevice } from '../shared/common';
import { KeyresultDialogComponent } from '../shared/dialog/keyresult-dialog/keyresult-dialog.component';
import { ConfirmDialogComponent } from '../shared/dialog/confirm-dialog/confirm-dialog.component';
@@ -28,7 +28,7 @@ export class KeyresultDetailComponent implements OnInit {
keyResult$: BehaviorSubject = new BehaviorSubject({} as KeyResult);
isComplete: boolean = false;
protected readonly DATE_FORMAT = DATE_FORMAT;
- protected readonly isInValid = isInValid;
+ protected readonly isLastCheckInNegative = isLastCheckInNegative;
constructor(
private keyResultService: KeyresultService,
@@ -189,4 +189,6 @@ export class KeyresultDetailComponent implements OnInit {
backToOverview() {
this.router.navigate(['']);
}
+
+ protected readonly calculateCurrentPercentage = calculateCurrentPercentage;
}
diff --git a/frontend/src/app/objective-filter/objective-filter.component.html b/frontend/src/app/objective-filter/objective-filter.component.html
index fef3342c88..02963c8fa7 100644
--- a/frontend/src/app/objective-filter/objective-filter.component.html
+++ b/frontend/src/app/objective-filter/objective-filter.component.html
@@ -18,9 +18,9 @@
diff --git a/frontend/src/app/objective-filter/objective-filter.component.scss b/frontend/src/app/objective-filter/objective-filter.component.scss
index 1b39d79369..90f7058c21 100644
--- a/frontend/src/app/objective-filter/objective-filter.component.scss
+++ b/frontend/src/app/objective-filter/objective-filter.component.scss
@@ -1,11 +1,6 @@
.search-button {
border-radius: 0 4px 4px 0;
- mat-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- transform: scale(1.25);
- }
+ padding-left: 15px;
}
#objective-form-field {
diff --git a/frontend/src/app/objective/objective.component.html b/frontend/src/app/objective/objective.component.html
index 515e6a7835..63cca208cf 100644
--- a/frontend/src/app/objective/objective.component.html
+++ b/frontend/src/app/objective/objective.component.html
@@ -7,8 +7,8 @@
tabindex="0"
>
-
-
+
+
- {{ objective.title }}
+ {{ objective.title }}
@@ -49,7 +49,7 @@ {{ objective.title }}
*ngIf="!isComplete && isWritable"
mat-button
color="primary"
- class="fw-bold"
+ class="fw-bold px-0 pe-2 ms-2"
[attr.data-testId]="'add-keyResult'"
(click)="openAddKeyResultDialog(); $event.stopPropagation()"
(keydown.enter)="$event.stopPropagation()"
diff --git a/frontend/src/app/objective/objective.component.scss b/frontend/src/app/objective/objective.component.scss
index 489a8ef45e..bdff62c98c 100644
--- a/frontend/src/app/objective/objective.component.scss
+++ b/frontend/src/app/objective/objective.component.scss
@@ -8,6 +8,7 @@
0px 15px 9px 0px rgba(0, 0, 0, 0.05),
0px 27px 11px 0px rgba(0, 0, 0, 0.01),
0px 42px 12px 0px rgba(0, 0, 0, 0);
+
&:hover {
box-shadow:
2px 8px 19px 0px rgba(0, 0, 0, 0.1),
@@ -22,13 +23,14 @@
color: $pz-dark-blue;
display: flex;
justify-content: center;
- align-items: start;
- width: 35px;
- height: 32px;
+ align-items: center;
+ width: 36px;
+ height: 36px;
+ margin-right: 3px;
&:hover {
border-radius: 1rem;
- background-color: lightgray;
+ background-color: #f5f7fa;
}
}
@@ -38,3 +40,7 @@
margin-top: 2px;
margin-left: 4px;
}
+
+.objective-title {
+ width: 90%;
+}
diff --git a/frontend/src/app/objective/objective.component.spec.ts b/frontend/src/app/objective/objective.component.spec.ts
index ecdf43dd01..8467049416 100644
--- a/frontend/src/app/objective/objective.component.spec.ts
+++ b/frontend/src/app/objective/objective.component.spec.ts
@@ -9,7 +9,7 @@ import { By } from '@angular/platform-browser';
import { State } from '../shared/types/enums/State';
import { RouterTestingModule } from '@angular/router/testing';
import { OverviewService } from '../shared/services/overview.service';
-import { objectiveMin } from '../shared/testData';
+import { objective, objectiveMin } from '../shared/testData';
import { MatMenuHarness } from '@angular/material/menu/testing';
import { KeyresultComponent } from '../keyresult/keyresult.component';
import { MatDialogModule } from '@angular/material/dialog';
@@ -21,10 +21,22 @@ import { ConfidenceComponent } from '../confidence/confidence.component';
import { ReactiveFormsModule } from '@angular/forms';
import * as de from '../../assets/i18n/de.json';
import { TranslateTestingModule } from 'ngx-translate-testing';
+import { ObjectiveMin } from '../shared/types/model/ObjectiveMin';
+import { MenuEntry } from '../shared/types/menu-entry';
+import { ObjectiveService } from '../shared/services/objective.service';
+import { of } from 'rxjs';
const overviewServiceMock = {
getObjectiveWithKeyresults: jest.fn(),
};
+
+const objectiveServiceMock = {
+ getFullObjective(objectiveMin: ObjectiveMin) {
+ let ongoingObjective = objective;
+ ongoingObjective.state = State.ONGOING;
+ return of(ongoingObjective);
+ },
+};
describe('ObjectiveColumnComponent', () => {
let component: ObjectiveComponent;
let fixture: ComponentFixture;
@@ -48,7 +60,10 @@ describe('ObjectiveColumnComponent', () => {
de: de,
}),
],
- providers: [{ provide: OverviewService, useValue: overviewServiceMock }],
+ providers: [
+ { provide: OverviewService, useValue: overviewServiceMock },
+ { provide: ObjectiveService, useValue: objectiveServiceMock },
+ ],
}).compileComponents();
fixture = TestBed.createComponent(ObjectiveComponent);
@@ -99,4 +114,46 @@ describe('ObjectiveColumnComponent', () => {
const button = fixture.debugElement.query(By.css('[data-testId="add-keyResult"]'));
expect(button).toBeFalsy();
});
+
+ it('Correct method should be called when back to draft is clicked', () => {
+ jest.spyOn(component, 'objectiveBackToDraft');
+ component.objective$.next(objectiveMin);
+ fixture.detectChanges();
+ const menuEntry: MenuEntry =
+ component.getOngoingMenuActions()[
+ component
+ .getOngoingMenuActions()
+ .map((menuAction) => menuAction.action)
+ .indexOf('todraft')
+ ];
+ component.handleDialogResult(menuEntry, { endState: '', comment: null, objective: objective });
+ fixture.detectChanges();
+ expect(component.objectiveBackToDraft).toHaveBeenCalled();
+ });
+
+ test('Should set isBacklogQuarter right', async () => {
+ expect(component.isBacklogQuarter).toBeFalsy();
+
+ objectiveMin.quarter.label = 'Backlog';
+
+ component.objective = objectiveMin;
+ fixture.detectChanges();
+ component.ngOnInit();
+
+ expect(component.isBacklogQuarter).toBeTruthy();
+ });
+
+ test('Should return correct menu entries when backlog', async () => {
+ objectiveMin.quarter.label = 'Backlog';
+ component.objective = objectiveMin;
+ fixture.detectChanges();
+ component.ngOnInit();
+
+ let menuActions = component.getDraftMenuActions();
+
+ expect(menuActions.length).toEqual(3);
+ expect(menuActions[0].displayName).toEqual('Objective bearbeiten');
+ expect(menuActions[1].displayName).toEqual('Objective duplizieren');
+ expect(menuActions[2].displayName).toEqual('Objective veröffentlichen');
+ });
});
diff --git a/frontend/src/app/objective/objective.component.ts b/frontend/src/app/objective/objective.component.ts
index 0440d3c86d..349f7808c5 100644
--- a/frontend/src/app/objective/objective.component.ts
+++ b/frontend/src/app/objective/objective.component.ts
@@ -15,6 +15,7 @@ import { Objective } from '../shared/types/model/Objective';
import { isMobileDevice, trackByFn } from '../shared/common';
import { KeyresultDialogComponent } from '../shared/dialog/keyresult-dialog/keyresult-dialog.component';
import { TranslateService } from '@ngx-translate/core';
+import { GJ_REGEX_PATTERN } from '../shared/constantLibary';
@Component({
selector: 'app-objective-column',
@@ -28,6 +29,7 @@ export class ObjectiveComponent implements OnInit {
menuEntries: MenuEntry[] = [];
isComplete: boolean = false;
+ isBacklogQuarter: boolean = false;
protected readonly trackByFn = trackByFn;
@ViewChild('menuButton') private menuButton!: ElementRef;
@@ -50,6 +52,9 @@ export class ObjectiveComponent implements OnInit {
if (this.objective$.value.state.includes('successful') || this.objective$.value.state.includes('not-successful')) {
this.isComplete = true;
}
+ if (!GJ_REGEX_PATTERN.test(this.objective$.value.quarter.label)) {
+ this.isBacklogQuarter = true;
+ }
}
formatObjectiveState(state: string): string {
@@ -86,26 +91,35 @@ export class ObjectiveComponent implements OnInit {
action: 'complete',
dialog: { dialog: CompleteDialogComponent, data: { objectiveTitle: this.objective$.value.title } },
},
- ],
- ];
- }
-
- getDraftMenuActions() {
- return [
- ...this.getDefaultMenuActions(),
- ...[
{
- displayName: 'Objective veröffentlichen',
- action: 'release',
+ displayName: 'Objective als Draft speichern',
+ action: 'todraft',
dialog: {
dialog: ConfirmDialogComponent,
- data: { title: 'Objective', action: 'release' },
+ data: { title: 'Objective', action: 'todraft' },
},
},
],
];
}
+ getDraftMenuActions() {
+ let menuEntries = {
+ displayName: 'Objective veröffentlichen',
+ action: this.isBacklogQuarter ? 'releaseBacklog' : 'release',
+ dialog: {
+ dialog: this.isBacklogQuarter ? ObjectiveFormComponent : ConfirmDialogComponent,
+ data: {
+ title: 'Objective',
+ action: this.isBacklogQuarter ? 'releaseBacklog' : 'release',
+ objectiveId: this.isBacklogQuarter ? this.objective$.value.id : undefined,
+ },
+ },
+ };
+
+ return [...this.getDefaultMenuActions(), menuEntries];
+ }
+
getDefaultMenuActions() {
return [
{
@@ -145,7 +159,7 @@ export class ObjectiveComponent implements OnInit {
height: 'auto',
};
- if (menuEntry.action == 'release') {
+ if (menuEntry.action == 'release' || menuEntry.action == 'todraft') {
dialogConfig.width = 'auto';
}
const matDialogRef = this.matDialog.open(menuEntry.dialog.dialog, {
@@ -180,6 +194,10 @@ export class ObjectiveComponent implements OnInit {
this.releaseObjective(objective);
} else if (menuEntry.action == 'duplicate') {
this.refreshDataService.markDataRefresh();
+ } else if (menuEntry.action == 'releaseBacklog') {
+ this.refreshDataService.markDataRefresh();
+ } else if (menuEntry.action == 'todraft') {
+ this.objectiveBackToDraft(objective);
}
});
} else {
@@ -212,6 +230,13 @@ export class ObjectiveComponent implements OnInit {
});
}
+ objectiveBackToDraft(objective: Objective) {
+ objective.state = 'DRAFT' as State;
+ this.objectiveService.updateObjective(objective).subscribe(() => {
+ this.refreshDataService.markDataRefresh();
+ });
+ }
+
reopenRedirect(menuEntry: MenuEntry) {
if (menuEntry.action === 'reopen') {
this.objectiveService.getFullObjective(this.objective$.value.id).subscribe((objective) => {
diff --git a/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts b/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts
index bccb1cbc9f..9a578a6310 100644
--- a/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts
+++ b/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts
@@ -22,6 +22,7 @@ const overviewService = {
};
const quarters = [
+ { id: 199, label: 'Backlog', startDate: null, endDate: null },
{ ...quarter, id: 2 },
{ ...quarter, id: 5 },
{ ...quarter, id: 7 },
@@ -72,8 +73,8 @@ describe('QuarterFilterComponent', () => {
expect(quarterSelect).toBeTruthy();
component.ngOnInit();
fixture.detectChanges();
- expect(component.quarterId).toBe(quarters[1].id);
- expect(await quarterSelect.getValueText()).toBe(quarters[1].label + ' Aktuell');
+ expect(component.quarterId).toBe(quarters[2].id);
+ expect(await quarterSelect.getValueText()).toBe(quarters[2].label + ' Aktuell');
expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(0);
});
@@ -82,15 +83,15 @@ describe('QuarterFilterComponent', () => {
const routerHarnessPromise = RouterTestingHarness.create();
const quarterSelectPromise = loader.getHarness(MatSelectHarness);
await Promise.all([routerHarnessPromise, quarterSelectPromise]).then(async ([routerHarness, quarterSelect]) => {
- await routerHarness.navigateByUrl('/?quarter=' + quarters[2].id);
+ await routerHarness.navigateByUrl('/?quarter=' + quarters[3].id);
expect(quarterSelect).toBeTruthy();
routerHarness.detectChanges();
component.ngOnInit();
fixture.detectChanges();
- expect(component.quarterId).toBe(quarters[2].id);
- expect(await quarterSelect.getValueText()).toBe(quarters[2].label);
+ expect(component.quarterId).toBe(quarters[3].id);
+ expect(await quarterSelect.getValueText()).toBe(quarters[3].label);
expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(1);
});
});
@@ -106,9 +107,9 @@ describe('QuarterFilterComponent', () => {
routerHarness.detectChanges();
component.ngOnInit();
fixture.detectChanges();
- expect(component.quarterId).toBe(quarters[1].id);
- expect(await quarterSelect.getValueText()).toBe(quarters[1].label + ' Aktuell');
+ expect(component.quarterId).toBe(quarters[2].id);
+ expect(await quarterSelect.getValueText()).toBe(quarters[2].label + ' Aktuell');
expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(1);
- expect(router.url).toBe('/?quarter=' + quarters[1].id);
+ expect(router.url).toBe('/?quarter=' + quarters[2].id);
});
});
diff --git a/frontend/src/app/quarter-filter/quarter-filter.component.ts b/frontend/src/app/quarter-filter/quarter-filter.component.ts
index fe08e114d7..10168b7bab 100644
--- a/frontend/src/app/quarter-filter/quarter-filter.component.ts
+++ b/frontend/src/app/quarter-filter/quarter-filter.component.ts
@@ -32,7 +32,7 @@ export class QuarterFilterComponent implements OnInit {
this.quarterId = quarterId;
this.changeDisplayedQuarter();
} else {
- this.quarterId = quarters[1].id;
+ this.quarterId = quarters[2].id;
if (quarterQuery !== undefined) {
this.changeDisplayedQuarter();
} else {
diff --git a/frontend/src/app/shared/common.ts b/frontend/src/app/shared/common.ts
index 7e5519cfff..ec0d3de2b3 100644
--- a/frontend/src/app/shared/common.ts
+++ b/frontend/src/app/shared/common.ts
@@ -34,16 +34,15 @@ export function optionalValue(param: object): { [p: string]: any } {
}),
);
}
-export function isInValid(baseline: number, stretchGoal: number, value: number): boolean {
- if (value < baseline && baseline <= stretchGoal) return true;
- return value > baseline && baseline > stretchGoal;
+export function isLastCheckInNegative(baseline: number, stretchGoal: number, value: number): boolean {
+ return (value > baseline && baseline > stretchGoal) || (value < baseline && baseline <= stretchGoal);
}
export function calculateCurrentPercentage(keyResultMetric: KeyResultMetricMin): number {
let value: number = +keyResultMetric.lastCheckIn?.value!;
let baseline: number = +keyResultMetric.baseline;
let stretchGoal: number = +keyResultMetric.stretchGoal;
- if (isInValid(baseline, stretchGoal, value)) return 0;
+ if (isLastCheckInNegative(baseline, stretchGoal, value)) return 0;
if (value == stretchGoal) return 100;
return (Math.abs(value - baseline) / Math.abs(stretchGoal - baseline)) * 100;
@@ -89,7 +88,7 @@ export function formInputCheck(form: FormGroup, propertyName: string) {
}
export function getQuarterLabel(quarter: any, index: number): string {
- return index == 1 ? quarter.label + ' Aktuell' : quarter.label;
+ return index == 2 ? quarter.label + ' Aktuell' : quarter.label;
}
export function isMobileDevice() {
diff --git a/frontend/src/app/shared/constantLibary.ts b/frontend/src/app/shared/constantLibary.ts
index 13f526305a..eb52e397f9 100644
--- a/frontend/src/app/shared/constantLibary.ts
+++ b/frontend/src/app/shared/constantLibary.ts
@@ -26,6 +26,8 @@ export const DATE_FORMAT = 'dd.MM.yyyy';
export const CONFIRM_DIALOG_WIDTH: string = '450px';
export const DRAWER_ROUTES = ['objective', 'keyresult'];
+export const GJ_REGEX_PATTERN = /^GJ \d{2}\/\d{2}-Q\d$/;
+
export const SUCCESS_MESSAGE_MAP: MessageKeyMap = {
teams: {
KEY: 'TEAM',
diff --git a/frontend/src/app/shared/custom/scoring/scoring.component.html b/frontend/src/app/shared/custom/scoring/scoring.component.html
index 784a6a9bba..938a1fee4a 100644
--- a/frontend/src/app/shared/custom/scoring/scoring.component.html
+++ b/frontend/src/app/shared/custom/scoring/scoring.component.html
@@ -12,10 +12,20 @@
Target
-
-
+
-
+
+
+ !
+
+
diff --git a/frontend/src/app/shared/custom/scoring/scoring.component.scss b/frontend/src/app/shared/custom/scoring/scoring.component.scss
index 90d296e5a3..7e088b41a9 100644
--- a/frontend/src/app/shared/custom/scoring/scoring.component.scss
+++ b/frontend/src/app/shared/custom/scoring/scoring.component.scss
@@ -1,6 +1,12 @@
+.scoring-error-badge {
+ aspect-ratio: 1/1;
+ height: 90%;
+ margin-left: 1.05px;
+}
+
.scoring-container {
display: flex;
- align-items: center;
+ align-items: start;
}
.okr-score-label {
@@ -51,10 +57,6 @@
background-color: #1e8a29;
}
-.border-right {
- border-right: 1px solid gray;
-}
-
.score-stretch {
background-image: url("../../../../assets/images/scoring-stars.svg");
}
diff --git a/frontend/src/app/shared/custom/scoring/scoring.component.spec.ts b/frontend/src/app/shared/custom/scoring/scoring.component.spec.ts
index d7a7d32cf7..43f00d5e31 100644
--- a/frontend/src/app/shared/custom/scoring/scoring.component.spec.ts
+++ b/frontend/src/app/shared/custom/scoring/scoring.component.spec.ts
@@ -57,12 +57,6 @@ describe('ScoringComponent', () => {
let color: string | null = component.getScoringColorClassAndSetBorder();
expect(color).toBe(object.className);
-
- if (object.borderClass !== 'none') {
- expect(
- fixture.debugElement.query(By.css('[data-testId="' + object.borderClass + '"]')).nativeElement.classList,
- ).toContain('border-right');
- }
});
});
diff --git a/frontend/src/app/shared/custom/scoring/scoring.component.ts b/frontend/src/app/shared/custom/scoring/scoring.component.ts
index f36e606d37..bdf7bf89f4 100644
--- a/frontend/src/app/shared/custom/scoring/scoring.component.ts
+++ b/frontend/src/app/shared/custom/scoring/scoring.component.ts
@@ -14,7 +14,7 @@ import { KeyresultMin } from '../../types/model/KeyresultMin';
import { Zone } from '../../types/enums/Zone';
import { KeyResultMetricMin } from '../../types/model/KeyResultMetricMin';
import { Observable, of } from 'rxjs';
-import { calculateCurrentPercentage } from '../../common';
+import { calculateCurrentPercentage, isLastCheckInNegative } from '../../common';
@Component({
selector: 'app-scoring',
@@ -31,6 +31,7 @@ export class ScoringComponent implements OnInit, AfterViewInit, OnChanges {
targetPercent: number = 0;
labelPercentage: Observable;
stretched: boolean = false;
+ protected readonly isLastCheckInNegative = isLastCheckInNegative;
@ViewChild('fail')
private failElement: ElementRef | undefined = undefined;
@@ -135,25 +136,17 @@ export class ScoringComponent implements OnInit, AfterViewInit, OnChanges {
if (this.targetPercent > 100) {
return 'score-stretch';
} else if (this.targetPercent > 0 || (this.commitPercent == 100 && this.keyResult.keyResultType === 'metric')) {
- this.setBorder(this.targetElement!);
return 'score-green';
} else if (this.commitPercent > 0 || (this.failPercent == 100 && this.keyResult.keyResultType === 'metric')) {
- this.setBorder(this.commitElement!);
return 'score-yellow';
- } else if (this.failPercent > 0) {
- this.setBorder(this.failElement!);
+ } else if (this.failPercent >= 3.3333) {
+ // 3.3333% because if lower fail is not visible in overview and we display !
return 'score-red';
} else {
return null;
}
}
- setBorder(element: ElementRef) {
- if (this.keyResult.keyResultType != 'ordinal') {
- element.nativeElement.classList.add('border-right');
- }
- }
-
ngOnChanges(changes: SimpleChanges): void {
if (changes['keyResult']?.currentValue !== undefined || changes['keyResult']?.currentValue !== null) {
if (this.commitElement != undefined) {
@@ -184,4 +177,6 @@ export class ScoringComponent implements OnInit, AfterViewInit, OnChanges {
castToMetric(): KeyResultMetricMin {
return this.keyResult as KeyResultMetricMin;
}
+
+ protected readonly calculateCurrentPercentage = calculateCurrentPercentage;
}
diff --git a/frontend/src/app/shared/dialog/checkin/check-in-form-metric/check-in-form-metric.component.html b/frontend/src/app/shared/dialog/checkin/check-in-form-metric/check-in-form-metric.component.html
index d16ee9f51b..45d9ce03ec 100644
--- a/frontend/src/app/shared/dialog/checkin/check-in-form-metric/check-in-form-metric.component.html
+++ b/frontend/src/app/shared/dialog/checkin/check-in-form-metric/check-in-form-metric.component.html
@@ -1,16 +1,26 @@