diff --git a/.gitignore b/.gitignore index 67f0bfbc..e1ab93ab 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,7 @@ generated #Files from development builds .cache/ + +#Other files + +auth-server/keycloak-theme/themes/corn/login/resources/css/styles.css diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintControllerImpl.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintControllerImpl.java index b9807f0d..5212021e 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintControllerImpl.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintControllerImpl.java @@ -28,6 +28,7 @@ import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.GET_SPRINT_BY_ID; import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.SPRINTS_AFTER_SPRINT; import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.SPRINTS_BEFORE_SPRINT; +import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.SPRINTS_BETWEEN_DATES; import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.SPRINT_API_ENDPOINT; import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.UPDATE_SPRINTS_DESCRIPTION; import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.UPDATE_SPRINTS_END_DATE; @@ -119,4 +120,13 @@ public final Page getSprintsBeforeSprint(@RequestParam long spri return sprintService.getSprintsBeforeSprint(sprintId, pageable, user); } + @Override + @GetMapping(value = SPRINTS_BETWEEN_DATES) + public final List getSprintsBetweenDates(@RequestParam LocalDate startDate, + @RequestParam LocalDate endDate, + @RequestParam long projectId, + @JwtAuthed User user) { + return sprintService.getSprintsBetweenDates(startDate, endDate, projectId, user); + } + } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintServiceImpl.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintServiceImpl.java index df6c439b..13476d1d 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintServiceImpl.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintServiceImpl.java @@ -38,6 +38,7 @@ public class SprintServiceImpl implements SprintService { private static final String FOUND_SPRINT_TO_UPDATE = "Found sprint to update: {}"; private static final String PROJECT_NOT_FOUND = "Project with projectId: %d does not exist"; private static final String SPRINTS_ON_PAGE = "Sprints found on page : {}"; + private static final String FOUND_PROJECT_WITH_ID = "Found project with id: {}"; private final SprintRepository sprintRepository; private final ProjectRepository projectRepository; private final BacklogItemRepository backlogItemRepository; @@ -212,7 +213,7 @@ public List getCurrentAndFutureSprints(long projectId, User user Project project = resolveProjectForProjectMember(projectId, user); - log.info("Found project with id: {}", project); + log.info(FOUND_PROJECT_WITH_ID, project); Pageable pageable = PageRequest.of(0, FUTURE_SPRINTS_PER_PAGE, Sort.by( Sort.Direction.ASC, SPRINT_START_DATE_FIELD_NAME) @@ -258,6 +259,28 @@ public Page getSprintsBeforeSprint(long sprintId, Pageable pagea return sprints.map(sprintMapper::toSprintResponse); } + @Override + public final List getSprintsBetweenDates(LocalDate startDate, LocalDate endDate, long projectId, + User user) { + log.info("Getting sprints between dates: {} and {} for project with id: {}", startDate, endDate, projectId); + + if(startDate.isAfter(endDate)) { + throw new SprintEndDateMustBeAfterStartDate(startDate, endDate); + } + + Project project = resolveProjectForProjectMember(projectId, user); + + log.info(FOUND_PROJECT_WITH_ID, project); + + List sprints = sprintRepository.findAllBetweenDates(startDate, endDate, project); + + log.info("Found and returning {} sprints", sprints.size()); + + return sprints.stream() + .map(sprintMapper::toSprintResponse) + .toList(); + } + private Project resolveProjectForProjectMember(long projectId, User user) { return projectRepository.findByIdWithProjectMember(projectId, user) .orElseThrow(() -> new ProjectDoesNotExistException( diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/constants/SprintMappings.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/constants/SprintMappings.java index 104d86ee..1b0b38be 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/constants/SprintMappings.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/constants/SprintMappings.java @@ -25,6 +25,8 @@ public final class SprintMappings { public static final String SPRINTS_BEFORE_SPRINT = "/getSprintsBeforeSprint"; + public static final String SPRINTS_BETWEEN_DATES = "/getSprintsBetweenDates"; + private SprintMappings() { } } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintController.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintController.java index 8430ebdc..6b84c1bd 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintController.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintController.java @@ -120,4 +120,6 @@ public interface SprintController { * @return Page of sprints occurring before the specified sprint */ Page getSprintsBeforeSprint(long sprintId, Pageable pageable, User user); + + List getSprintsBetweenDates(LocalDate startDate, LocalDate endDate, long projectId, User user); } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintService.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintService.java index a9a8ef7d..2cfcdbd0 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintService.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintService.java @@ -121,4 +121,6 @@ public interface SprintService { */ Page getSprintsBeforeSprint(long sprintId, Pageable pageable, User user); + List getSprintsBetweenDates(LocalDate startDate, LocalDate endDate, long projectId, User user); + } \ No newline at end of file diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/entities/sprint/interfaces/SprintRepository.java b/corn-backend/src/main/java/dev/corn/cornbackend/entities/sprint/interfaces/SprintRepository.java index 864997e4..331e513d 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/entities/sprint/interfaces/SprintRepository.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/entities/sprint/interfaces/SprintRepository.java @@ -11,6 +11,7 @@ import org.springframework.stereotype.Repository; import java.time.LocalDate; +import java.util.List; import java.util.Optional; /** @@ -128,5 +129,15 @@ select count(*) > 0 from Sprint s */ Page findAllByProjectAndEndDateBefore(Project project, LocalDate date, Pageable pageable); + @Query(""" + SELECT s FROM Sprint s + WHERE s.project = :project AND + ((s.startDate <= :startDate AND s.endDate >= :startDate) OR + (s.startDate <= :endDate AND s.endDate >= :endDate) OR + (s.startDate >= :startDate AND s.endDate <= :endDate)) + ORDER BY s.startDate ASC + """) + List findAllBetweenDates(LocalDate startDate, LocalDate endDate, Project project); + } diff --git a/corn-frontend/package-lock.json b/corn-frontend/package-lock.json index 874cc17c..bd44b1ec 100644 --- a/corn-frontend/package-lock.json +++ b/corn-frontend/package-lock.json @@ -15,6 +15,7 @@ "@angular/core": "^17.1.0", "@angular/forms": "^17.1.0", "@angular/material": "^17.1.1", + "@angular/material-moment-adapter": "^17.3.7", "@angular/platform-browser": "^17.1.0", "@angular/platform-browser-dynamic": "^17.1.0", "@angular/router": "^17.1.0", @@ -31,6 +32,7 @@ "@ng-icons/ux-aspects": "^26.3.0", "keycloak-angular": "^15.1.0", "keycloak-js": "^23.0.5", + "moment": "^2.30.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.3" @@ -366,9 +368,9 @@ } }, "node_modules/@angular/cdk": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.4.tgz", - "integrity": "sha512-/wbKUbc0YC3HGE2TCgW7D07Q99PZ/5uoRvMyWw0/wHa8VLNavXZPecbvtyLs//3HnqoCMSUFE7E2Mrd7jAWfcA==", + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.7.tgz", + "integrity": "sha512-aFEh8tzKFOwini6aNEp57S54Ocp9T7YIJfBVMESptu2TCPdMTlJ1HJTg5XS8NcQO+vwi9cFPGVwGF1frOx4LXA==", "dependencies": { "tslib": "^2.3.0" }, @@ -555,9 +557,9 @@ } }, "node_modules/@angular/material": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.4.tgz", - "integrity": "sha512-SgCroIlHKt3s9pTEYlhW4ww6Gm1sIzJKuk0wlputPZvQS5PTJ8YY8vDg4QohpQcltlaXCbutt4qw+CBNU9W9iA==", + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.7.tgz", + "integrity": "sha512-wjSKkk9KZE8QiBPkMd5axh5u/3pUSxoLKNO7OasFhEagMmSv5oYTLm40cErhtb4UdkSmbC19WuuluS6P3leoPA==", "dependencies": { "@material/animation": "15.0.0-canary.7f224ddd4.0", "@material/auto-init": "15.0.0-canary.7f224ddd4.0", @@ -610,7 +612,7 @@ }, "peerDependencies": { "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "17.3.4", + "@angular/cdk": "17.3.7", "@angular/common": "^17.0.0 || ^18.0.0", "@angular/core": "^17.0.0 || ^18.0.0", "@angular/forms": "^17.0.0 || ^18.0.0", @@ -618,6 +620,19 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/material-moment-adapter": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-17.3.7.tgz", + "integrity": "sha512-CwKB7kiVz9NOP9qaUksn4H8Un6VvAktp5uTPxa5QF186Ll1tMDPMGQ3B24le3tfUCUFZx7Jv2jA045pQ+8n/jA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/material": "17.3.7", + "moment": "^2.18.1" + } + }, "node_modules/@angular/platform-browser": { "version": "17.3.4", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.4.tgz", @@ -9518,6 +9533,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", diff --git a/corn-frontend/package.json b/corn-frontend/package.json index 3946599c..c6a5e8d7 100644 --- a/corn-frontend/package.json +++ b/corn-frontend/package.json @@ -18,6 +18,7 @@ "@angular/core": "^17.1.0", "@angular/forms": "^17.1.0", "@angular/material": "^17.1.1", + "@angular/material-moment-adapter": "^17.3.7", "@angular/platform-browser": "^17.1.0", "@angular/platform-browser-dynamic": "^17.1.0", "@angular/router": "^17.1.0", @@ -34,6 +35,7 @@ "@ng-icons/ux-aspects": "^26.3.0", "keycloak-angular": "^15.1.0", "keycloak-js": "^23.0.5", + "moment": "^2.30.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.3" diff --git a/corn-frontend/src/app/app.config.ts b/corn-frontend/src/app/app.config.ts index b726a357..ac884646 100644 --- a/corn-frontend/src/app/app.config.ts +++ b/corn-frontend/src/app/app.config.ts @@ -1,14 +1,13 @@ import { APP_INITIALIZER, ApplicationConfig, importProvidersFrom, isDevMode } from '@angular/core'; import { provideRouter } from '@angular/router'; - import { routes } from './app.routes'; import { provideServiceWorker } from '@angular/service-worker'; import { provideAnimations } from '@angular/platform-browser/animations'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { ErrorInterceptor } from '@core/interceptors/error.interceptor'; import { KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular'; -import { provideNativeDateAdapter } from "@angular/material/core"; import { DatePipe } from "@angular/common"; +import { provideNativeDateAdapter } from "@angular/material/core"; function initializeKeycloak(keycloak: KeycloakService) { return () => diff --git a/corn-frontend/src/app/core/enum/api-url.ts b/corn-frontend/src/app/core/enum/api-url.ts index a55809e4..10acfbd9 100644 --- a/corn-frontend/src/app/core/enum/api-url.ts +++ b/corn-frontend/src/app/core/enum/api-url.ts @@ -12,6 +12,7 @@ export enum ApiUrl { GET_SPRINTS_ON_PAGE = SPRINT_API_URL + '/getSprintsOnPage', GET_CURRENT_AND_FUTURE_SPRINTS = SPRINT_API_URL + '/currentAndFuture', + GET_SPRINTS_BETWEEN_DATES = SPRINT_API_URL + '/getSprintsBetweenDates', DELETE_SPRINT = SPRINT_API_URL + '/deleteSprint', UPDATE_SPRINTS_NAME = SPRINT_API_URL + '/updateSprintsName', UPDATE_SPRINTS_START_DATE = SPRINT_API_URL + '/updateSprintsStartDate', diff --git a/corn-frontend/src/app/core/services/boards/backlog/sprint/sprint.service.ts b/corn-frontend/src/app/core/services/boards/backlog/sprint/sprint.service.ts index 5492fcc6..aded12da 100644 --- a/corn-frontend/src/app/core/services/boards/backlog/sprint/sprint.service.ts +++ b/corn-frontend/src/app/core/services/boards/backlog/sprint/sprint.service.ts @@ -5,6 +5,7 @@ import { Sprint } from "@interfaces/boards/backlog/sprint"; import { ApiUrl } from "@core/enum/api-url"; import { StorageService } from "@core/services/storage.service"; import { StorageKey } from "@core/enum/storage-key.enum"; +import { Moment } from "moment"; import { SprintRequest } from "@interfaces/boards/backlog/sprint-request.interfaces"; @Injectable({ @@ -80,6 +81,16 @@ export class SprintService { }); } + getSprintsBetweenDates(startDate: Moment, endDate: Moment): Observable { + return this.http.get(ApiUrl.GET_SPRINTS_BETWEEN_DATES, { + params: { + startDate: startDate.format('YYYY-MM-DD'), + endDate: endDate.format('YYYY-MM-DD'), + projectId: this.storage.getValueFromStorage(StorageKey.PROJECT_ID) + } + }); + } + createSprint(result: SprintRequest): Observable { return this.http.post(ApiUrl.CREATE_SPRINT, result); } diff --git a/corn-frontend/src/app/pages/boards/timeline/day/day.component.html b/corn-frontend/src/app/pages/boards/timeline/day/day.component.html new file mode 100644 index 00000000..d5e95195 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/timeline/day/day.component.html @@ -0,0 +1,27 @@ +
+
+ @if (day > 0) { +

{{ day }}

+ } +
+
+ @if (sprint) { + @if(isFirst) { +
+
+

{{ sprint.sprintName }}

+
+
+ } @else if(isLast) { +
+
+ } @else { +
+
+ } + } +
+
diff --git a/corn-frontend/src/app/pages/boards/timeline/day/day.component.scss b/corn-frontend/src/app/pages/boards/timeline/day/day.component.scss new file mode 100644 index 00000000..83f9815c --- /dev/null +++ b/corn-frontend/src/app/pages/boards/timeline/day/day.component.scss @@ -0,0 +1,71 @@ +.bg-red-500 { + @apply bg-red-500/50; +} + +.bg-orange-500 { + @apply bg-orange-500/50; +} + +.bg-yellow-500 { + @apply bg-yellow-500/50; +} + +.bg-lime-500 { + @apply bg-lime-500/50; +} + +.bg-green-500 { + @apply bg-green-500/50; +} + +.bg-cyan-500 { + @apply bg-cyan-500/50; +} + +.bg-blue-500 { + @apply bg-blue-500/50; +} + +.bg-purple-500 { + @apply bg-purple-500/50; +} + +.bg-pink-500 { + @apply bg-pink-500/50; +} + +.bg-red-300 { + @apply bg-red-300/50; +} + +.bg-orange-300 { + @apply bg-orange-300/50; +} + +.bg-yellow-300 { + @apply bg-yellow-300/50; +} + +.bg-lime-300 { + @apply bg-lime-300/50; +} + +.bg-green-300 { + @apply bg-green-300/50; +} + +.bg-cyan-300 { + @apply bg-cyan-300/50; +} + +.bg-blue-300 { + @apply bg-blue-300/50; +} + +.bg-purple-300 { + @apply bg-purple-300/50; +} + +.bg-pink-300 { + @apply bg-pink-300/50; +} \ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/timeline/day/day.component.spec.ts b/corn-frontend/src/app/pages/boards/timeline/day/day.component.spec.ts new file mode 100644 index 00000000..af5579b7 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/timeline/day/day.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DayComponent } from './day.component'; + +describe('DayComponent', () => { + let component: DayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DayComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/corn-frontend/src/app/pages/boards/timeline/day/day.component.ts b/corn-frontend/src/app/pages/boards/timeline/day/day.component.ts new file mode 100644 index 00000000..d95fa6f3 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/timeline/day/day.component.ts @@ -0,0 +1,125 @@ +import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { MatGridList } from "@angular/material/grid-list"; +import { Subject, take, takeUntil } from "rxjs"; +import { Sprint } from "@interfaces/boards/backlog/sprint"; +import { MatDialog } from "@angular/material/dialog"; +import { BacklogEditFormComponent } from "@pages/boards/backlog/backlog-edit-form/backlog-edit-form.component"; +import { SprintEditData } from "@interfaces/boards/backlog/sprint-edit-data.interfaces"; +import { SprintService } from "@core/services/boards/backlog/sprint/sprint.service"; + + +@Component({ + selector: 'app-day', + standalone: true, + imports: [ + MatGridList + ], + templateUrl: './day.component.html', + styleUrl: './day.component.scss' +}) +export class DayComponent implements OnDestroy { + + @Input() day: number = 0; + @Input() isFirst: boolean = false; + @Input() isLast: boolean = false; + @Input() sprint: Sprint | null = null; + @Input() backgroundColor: string = 'cyan'; + + @Output() changeEmitter: EventEmitter = new EventEmitter(); + + color_value: string = '500'; + + private _eventEmitter: EventEmitter = new EventEmitter(); + + hovered = false; + + $destroy: Subject = new Subject(); + + constructor(private dialog: MatDialog, + private sprintService: SprintService) { + + } + + @Input() + set eventEmitter(emitter: EventEmitter | null) { + if (!emitter) { + return; + } + + this._eventEmitter = emitter; + + this._eventEmitter + .pipe(takeUntil(this.$destroy)) + .subscribe(value => { + this.hovered = value; + if (value) { + this.color_value = '300'; + } else { + this.color_value = '500'; + } + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + mouseOut(): void { + this._eventEmitter.emit(false); + } + + mouseOver(): void { + this._eventEmitter.emit(true); + } + + openSprint(): void { + const dialogRef = this.dialog.open(BacklogEditFormComponent, { + enterAnimationDuration: '300ms', + exitAnimationDuration: '100ms', + data: this.sprint + }) + + dialogRef.afterClosed() + .pipe(take(1)) + .subscribe((data: SprintEditData) => { + if (!data || !this.sprint) { + return; + } + + let changed: boolean = false; + + if (data.sprintName !== this.sprint.sprintName) { + changed = true; + this.sprintService.editSprintName(data.sprintName, this.sprint.sprintId) + .pipe(take(1)) + .subscribe() + } + + if (data.goal !== this.sprint.sprintDescription) { + changed = true; + this.sprintService.editSprintDescription(data.goal, this.sprint.sprintId) + .pipe(take(1)) + .subscribe() + } + + if (data.startDate !== this.sprint.startDate) { + changed = true; + this.sprintService.editSprintStartDate(data.startDate, this.sprint.sprintId) + .pipe(take(1)) + .subscribe() + } + + if (data.endDate !== this.sprint.endDate) { + changed = true; + this.sprintService.editSprintEndDate(data.endDate, this.sprint.sprintId) + .pipe(take(1)) + .subscribe() + } + + if (changed) { + this.changeEmitter.emit(); + } + }) + } +} diff --git a/corn-frontend/src/app/pages/boards/timeline/timeline.component.html b/corn-frontend/src/app/pages/boards/timeline/timeline.component.html index 97b9ee7d..873d5904 100644 --- a/corn-frontend/src/app/pages/boards/timeline/timeline.component.html +++ b/corn-frontend/src/app/pages/boards/timeline/timeline.component.html @@ -1 +1,60 @@ -

Timeline

+
+
+

Timeline

+
+ +
+
+
+ + + + Month and Year + + + + + + + + +
+ +
+ @for (day of days; track day) { +
+

{{ day }}

+
+ } +
+ +
+ @for (day of daysArray; track day; let i = $index) { +
+ +
+ } +
+
+
+
+ + + + diff --git a/corn-frontend/src/app/pages/boards/timeline/timeline.component.scss b/corn-frontend/src/app/pages/boards/timeline/timeline.component.scss new file mode 100644 index 00000000..aebd0216 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/timeline/timeline.component.scss @@ -0,0 +1,6 @@ +.arrow-button { + display: flex; + align-items: center; + justify-content: center; + width: max-content; +} \ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/timeline/timeline.component.ts b/corn-frontend/src/app/pages/boards/timeline/timeline.component.ts index 9505c29e..e1882ce1 100644 --- a/corn-frontend/src/app/pages/boards/timeline/timeline.component.ts +++ b/corn-frontend/src/app/pages/boards/timeline/timeline.component.ts @@ -1,10 +1,234 @@ -import { Component } from '@angular/core'; +import { Component, EventEmitter, OnInit } from '@angular/core'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatDatepicker, MatDatepickerModule } from '@angular/material/datepicker'; +import _moment, { default as _rollupMoment, Moment } from 'moment'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { NgIcon, provideIcons } from "@ng-icons/core"; +import { matArrowLeft, matArrowRight } from "@ng-icons/material-icons/baseline"; +import { MatIconButton } from "@angular/material/button"; +import { MatIcon } from "@angular/material/icon"; +import { MatGridList, MatGridTile } from "@angular/material/grid-list"; +import { DayComponent } from "@pages/boards/timeline/day/day.component"; +import { SprintService } from "@core/services/boards/backlog/sprint/sprint.service"; +import { take } from "rxjs"; +import { Sprint } from "@interfaces/boards/backlog/sprint"; +import { provideMomentDateAdapter } from "@angular/material-moment-adapter"; + +const moment = _rollupMoment || _moment; + +export const MY_FORMATS = { + parse: { + dateInput: 'MM/YYYY', + }, + display: { + dateInput: 'MM/YYYY', + monthYearLabel: 'MMM YYYY', + dateA11yLabel: 'LL', + monthYearA11yLabel: 'MMMM YYYY', + }, +}; + +/** @title Datepicker emulating a Year and month picker */ @Component({ - selector: 'app-timeline', + selector: 'app-timeline-component', + templateUrl: 'timeline.component.html', standalone: true, - imports: [], - templateUrl: './timeline.component.html', + imports: [ + MatFormFieldModule, + MatInputModule, + MatDatepickerModule, + FormsModule, + ReactiveFormsModule, + MatIconButton, + NgIcon, + MatIcon, + MatGridList, + MatGridTile, + DayComponent, + ], + providers: [provideIcons({matArrowLeft, matArrowRight}), provideMomentDateAdapter(MY_FORMATS)] }) -export class TimelineComponent { -} +export class TimelineComponent implements OnInit { + + days: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + + previousMonthDays: number[] = [0, 0]; + currentMonthDays: number = 0; + nextMonthDays: number = 0; + firstDay: Moment = moment().startOf('month'); + lastDay: Moment = moment().endOf('month'); + + date: FormControl<_moment.Moment | null> = new FormControl(moment()); + + sprints: Sprint[] = []; + daysArray: number[] = []; + indexArray: number[] = []; + + colors: string[] = [ + 'red', + 'orange', + 'yellow', + 'lime', + 'green', + 'cyan', + 'blue', + 'purple', + 'pink' + ] + + emitters: EventEmitter[] = []; + + constructor(private sprintService: SprintService) { + } + + setMonthAndYear(normalizedMonthAndYear: Moment, datepicker: MatDatepicker): void { + console.log(normalizedMonthAndYear); + datepicker.close(); + + const ctrlValue: Moment = this.date.value ?? moment(); + ctrlValue.month(normalizedMonthAndYear.month()); + ctrlValue.year(normalizedMonthAndYear.year()); + this.date.setValue(ctrlValue); + + this.setMonthDays(ctrlValue); + + this.getSprints(); + } + + changeMonth(numberOfMonths: number): void { + const ctrlValue: Moment = this.date.value!; + ctrlValue.add(numberOfMonths, 'months'); + this.date.setValue(ctrlValue); + + this.setMonthDays(ctrlValue); + + this.getSprints(); + } + + setMonthDays(value: Moment): void { + const firstDayOfMonth: Moment = value.clone().startOf('month'); + + const daysInCurrentMonth: number = value.daysInMonth(); + + const daysFromPreviousMonth: number = firstDayOfMonth.day() === 0 ? 6 : firstDayOfMonth.day() - 1; + + const daysFromNextMonth: number = 7 * 6 - daysInCurrentMonth - daysFromPreviousMonth; + + if (daysFromPreviousMonth == 0) { + this.previousMonthDays = [0, 0]; + this.firstDay = firstDayOfMonth.clone(); + } else { + this.firstDay = firstDayOfMonth.clone().subtract(daysFromPreviousMonth, 'days'); + this.previousMonthDays = [this.firstDay.date(), firstDayOfMonth.clone().subtract(1, 'days').date()]; + } + this.currentMonthDays = daysInCurrentMonth; + this.lastDay = firstDayOfMonth.clone().endOf('month').add(daysFromNextMonth, 'days'); + this.nextMonthDays = daysFromNextMonth; + } + + getArrayFromDays(firstDay: number, lastDay: number): number[] { + if(firstDay == 0) { + return []; + } + + return Array.from({length: lastDay - firstDay + 1}, (_, i) => firstDay + i); + } + + ngOnInit(): void { + this.setMonthDays(this.date.value!); + + this.getSprints(); + } + + getSprints(): void { + this.sprintService.getSprintsBetweenDates(this.firstDay, this.lastDay) + .pipe(take(1)) + .subscribe(sprints => { + this.sprints = sprints; + this.createDayArray(); + this.createIndexArray(); + this.createEmittersArray(); + }); + } + + createEmittersArray(): void { + let emitters: EventEmitter[] = []; + + for(let i = 0; i < this.sprints.length; i++) { + emitters.push(new EventEmitter()); + } + + this.emitters = emitters; + } + + createDayArray(): void { + let days: number[] = []; + + days = [...days, ...this.getArrayFromDays(this.previousMonthDays[0], this.previousMonthDays[1])] + days = [...days, ...this.getArrayFromDays(1, this.currentMonthDays)]; + days = [...days, ...this.getArrayFromDays(1, this.nextMonthDays)]; + + this.daysArray = days; + } + + createIndexArray(): void { + let days: number[] = []; + + if(this.sprints.length == 0) { + days = Array.from({length: 42}, (_, i) => -1); + this.indexArray = days; + return; + } + + let difference: number = moment(this.sprints[0].startDate).diff(this.firstDay, 'days'); + + for(let i = 0; i < difference; i++) { + days.push(-1); + } + + let previousSprintLastDay: Moment | null = null; + + for(let j = 0; j < this.sprints.length; j++) { + let sprint: Sprint = this.sprints[j]; + let startDate: Moment = moment(sprint.startDate); + + if(startDate.isBefore(this.firstDay)) { + startDate = this.firstDay; + } + + if(previousSprintLastDay) { + difference = startDate.diff(previousSprintLastDay, 'days'); + difference -= 1; + + for (let i = 0; i < difference; i++) { + days.push(-1); + } + } + + let endDate: Moment = moment(sprint.endDate); + + if(endDate.isAfter(this.lastDay)) { + endDate = this.lastDay; + } + + difference = endDate.diff(startDate, 'days'); + + for(let i = 0; i <= difference; i++) { + days.push(j); + } + + previousSprintLastDay = endDate; + } + + difference = this.lastDay.diff(previousSprintLastDay, 'days'); + + for(let i = 0; i < difference; i++) { + days.push(-1); + } + + this.indexArray = days; + } + +} \ No newline at end of file