diff --git a/package-lock.json b/package-lock.json index 1d298e4..c089a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,12 +31,14 @@ "dotenv": "^16.0.3", "eva-icons": "^1.1.3", "lodash": "^4.17.21", + "moment": "^2.30.1", "ngx-logger": "^5.0.12", "ngx-permissions": "^15.0.1", "ngx-webstorage-service": "^5.0.0", "or": "^0.2.0", "prettier": "^2.8.7", "rxjs": "~7.8.0", + "save-dev": "^0.0.1-security", "ts-node": "^10.9.1", "tslib": "^2.3.0", "zone.js": "~0.12.0" @@ -58,7 +60,7 @@ "cz-conventional-changelog": "^3.3.0", "cz-customizable": "^7.0.0", "dhtmlx-gantt": "^7.1.13", - "eslint": "^8.33.0", + "eslint": "^8.57.0", "husky": "^8.0.3", "jasmine-core": "~4.5.0", "karma": "~6.4.0", @@ -8393,6 +8395,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -12223,6 +12226,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/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -14344,6 +14355,11 @@ } } }, + "node_modules/save-dev": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/save-dev/-/save-dev-0.0.1-security.tgz", + "integrity": "sha512-k6knZTDNK8PKKbIqnvxiOveJinuw2LcQjqDoaorZWP9M5AR2EPsnpDeSbeoZZ0pHr5ze1uoaKdK8NBGQrJ34Uw==" + }, "node_modules/sax": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", diff --git a/package.json b/package.json index 1358ddf..b227a3a 100644 --- a/package.json +++ b/package.json @@ -40,12 +40,14 @@ "dotenv": "^16.0.3", "eva-icons": "^1.1.3", "lodash": "^4.17.21", + "moment": "^2.30.1", "ngx-logger": "^5.0.12", "ngx-permissions": "^15.0.1", "ngx-webstorage-service": "^5.0.0", "or": "^0.2.0", "prettier": "^2.8.7", "rxjs": "~7.8.0", + "save-dev": "^0.0.1-security", "ts-node": "^10.9.1", "tslib": "^2.3.0", "zone.js": "~0.12.0" @@ -67,7 +69,7 @@ "cz-conventional-changelog": "^3.3.0", "cz-customizable": "^7.0.0", "dhtmlx-gantt": "^7.1.13", - "eslint": "^8.33.0", + "eslint": "^8.57.0", "husky": "^8.0.3", "jasmine-core": "~4.5.0", "karma": "~6.4.0", diff --git a/projects/arc-docs/src/app/docs/auth-doc/components/configure-token-doc/configure-token-doc.component.scss b/projects/arc-docs/src/app/docs/auth-doc/components/configure-token-doc/configure-token-doc.component.scss index f6fd00e..3d5601d 100644 --- a/projects/arc-docs/src/app/docs/auth-doc/components/configure-token-doc/configure-token-doc.component.scss +++ b/projects/arc-docs/src/app/docs/auth-doc/components/configure-token-doc/configure-token-doc.component.scss @@ -1,4 +1,4 @@ a { - text-decoration: none; - color: #19a5ff; - } \ No newline at end of file + text-decoration: none; + color: #19a5ff; +} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.html b/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.html new file mode 100644 index 0000000..3cae2a0 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.html @@ -0,0 +1,12 @@ +
+ + +
diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.scss b/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.scss new file mode 100644 index 0000000..f43bcb2 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.scss @@ -0,0 +1,4 @@ +.gantt-scroll-icon { + height: 2rem; + width: 2rem; +} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.spec.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.spec.ts new file mode 100644 index 0000000..4292230 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.spec.ts @@ -0,0 +1,22 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {GanttScrollComponent} from './gantt-scroll.component'; + +describe('GanttScrollComponent', () => { + let component: GanttScrollComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GanttScrollComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(GanttScrollComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.ts new file mode 100644 index 0000000..e6e7e6d --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.ts @@ -0,0 +1,28 @@ +import {Component, Inject} from '@angular/core'; +import {AnyObject} from '@project-lib/core/api'; +import {GANTT_SCALES} from '../../const'; +import {GanttService} from '../../services'; +import {GanttScaleService} from '../../types'; + +@Component({ + selector: 'arc-gantt-scroll', + templateUrl: './gantt-scroll.component.html', + styleUrls: ['./gantt-scroll.component.scss'], +}) +export class GanttScrollComponent { + constructor( + private ganttService: GanttService, + @Inject(GANTT_SCALES) + private readonly scales: GanttScaleService[], + ) {} + scrollBack() { + const selectedScale = this.ganttService.selectedScale; + const scale = this.scales.find(s => s.scale === selectedScale); + scale?.scroll(false, this.ganttService); + } + scrollForward() { + const selectedScale = this.ganttService.selectedScale; + const scale = this.scales.find(s => s.scale === selectedScale); + scale?.scroll(true, this.ganttService); + } +} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.scss b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.scss index 3cef4c8..29e3f14 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.scss +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.scss @@ -96,6 +96,7 @@ hr { height: 10px; border-radius: 50%; margin-right: 10px; + &.active { background-color: green; } diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.html b/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.html new file mode 100644 index 0000000..8e29a41 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.html @@ -0,0 +1,27 @@ + + +
+ + + + + + +
+
+
diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.scss b/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.scss new file mode 100644 index 0000000..c4410ae --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.scss @@ -0,0 +1,8 @@ +.icon-wrapper { + display: flex; + gap: 16px; +} + +.icon { + cursor: pointer; +} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.spec.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.spec.ts new file mode 100644 index 0000000..84dcb3d --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.spec.ts @@ -0,0 +1,42 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {GanttZoomBarComponent} from './gantt-zoombar.component'; +import {AnyObject} from '@project-lib/core/api'; +import {CoreModule} from '@project-lib/core/core.module'; +import {LocalizationModule} from '@project-lib/core/localization'; +import {IconPacksManagerService} from '@project-lib/theme/services'; +import {ThemeModule} from '@project-lib/theme/theme.module'; +import {GanttProviders, GANTT_SCALES} from '../../const'; + +describe('GanttZoomBarComponent', () => { + let component: GanttZoomBarComponent; + let fixture: ComponentFixture>; + let service: IconPacksManagerService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GanttZoomBarComponent], + providers: [ + GanttProviders, + { + provide: GANTT_SCALES, + useValue: [], + }, + ], + imports: [ThemeModule.forRoot('arc'), LocalizationModule, CoreModule], + }).compileComponents(); + service = TestBed.inject(IconPacksManagerService); + service.registerFontAwesome(); + service.registerSvgs(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GanttZoomBarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.ts new file mode 100644 index 0000000..abaeb4e --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.ts @@ -0,0 +1,34 @@ +import {Component} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {TranslationService} from '@project-lib/core/localization'; +import {GanttService} from '../../services'; +import {AnyObject} from '@project-lib/core/api'; +import {GanttProviders} from '../../const'; +import {CustomGanttAdapter, GanttAdapter} from '../../types'; + +@Component({ + selector: 'arc-gantt-zoombar', + templateUrl: './gantt-zoombar.component.html', + styleUrls: ['./gantt-zoombar.component.scss'], +}) +export class GanttZoomBarComponent { + translate: TranslateService; + constructor( + private ganttService: GanttService, + private readonly translationService: TranslationService, + ) { + this.translate = this.translationService.translate; + } + + zoomIn() { + this.ganttService.zoomIn(); + } + + zoomOut() { + this.ganttService.zoomOut(); + } + + fitToScreen() { + this.ganttService.fitToScreen(); + } +} diff --git a/projects/arc-lib/src/lib/components/gantt/gantt.module.ts b/projects/arc-lib/src/lib/components/gantt/gantt.module.ts index c731dcb..0ab106c 100644 --- a/projects/arc-lib/src/lib/components/gantt/gantt.module.ts +++ b/projects/arc-lib/src/lib/components/gantt/gantt.module.ts @@ -2,19 +2,13 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {ReactiveFormsModule} from '@angular/forms'; import {ThemeModule} from '@project-lib/theme/theme.module'; -import {GANTT, GANTT_SCALES} from './const'; +import {GANTT, GANTT_SCALES, GanttProviders} from './const'; import {MonthlyScaleService} from './services/timeline-scales/monthly-scale.service'; import {QuarterlyScaleService} from './services/timeline-scales/quarterly-scale.service'; import {WeeklyScaleService} from './services/timeline-scales/weekly-scale.service'; import {GanttRoutingModule} from './gantt-routing.module'; -import {GanttService} from './services'; import {gantt} from 'dhtmlx-gantt'; -import { - CustomGanttAdapter, - GanttAdapter, - GanttLib, - GanttScaleService, -} from './types'; +import {CustomGanttAdapter, GanttAdapter} from './types'; import { GanttBarsComponent, @@ -23,6 +17,9 @@ import { GanttTooltipComponent, } from './components'; import {NbInputModule} from '@nebular/theme/components/input/input.module'; +import {GanttZoomBarComponent} from './components/gantt-zoombar/gantt-zoombar.component'; +import {GanttScrollComponent} from './components/gantt-scroll/gantt-scroll.component'; +import {DateOperationService} from './services/date-operation.service'; @NgModule({ declarations: [ @@ -30,6 +27,8 @@ import {NbInputModule} from '@nebular/theme/components/input/input.module'; GanttColumnComponent, GanttHeaderComponent, GanttTooltipComponent, + GanttZoomBarComponent, + GanttScrollComponent, ], imports: [CommonModule, ReactiveFormsModule, ThemeModule, GanttRoutingModule], exports: [ @@ -37,9 +36,11 @@ import {NbInputModule} from '@nebular/theme/components/input/input.module'; GanttColumnComponent, GanttHeaderComponent, GanttTooltipComponent, + GanttZoomBarComponent, + GanttScrollComponent, ], providers: [ - GanttService, + DateOperationService, { provide: GANTT, useValue: gantt, diff --git a/projects/arc-lib/src/lib/components/gantt/services/date-operation.service.ts b/projects/arc-lib/src/lib/components/gantt/services/date-operation.service.ts new file mode 100644 index 0000000..3e99d73 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/services/date-operation.service.ts @@ -0,0 +1,45 @@ +import {Injectable} from '@angular/core'; +import * as moment from 'moment'; + +@Injectable() +export class DateOperationService { + convertToMoment(date: moment.MomentInput) { + return moment(date); + } + + getTotalMonths(startDate: moment.Moment, endDate: moment.Moment) { + let months = 0; + const date = startDate.clone().startOf('month'); + const end = endDate.clone().endOf('month'); + while (date < end) { + months++; + date.add(1, 'month'); + } + return months; + } + + calculateWeeksBetweenDates(startDate: Date | string, endDate: Date | string) { + const startMoment = moment(startDate); + const endMoment = moment(endDate); + const totalWeeks = endMoment.diff(startMoment, 'weeks') + 1; + return totalWeeks; + } + + getNumberOfDaysBetweenDates(date1: Date, date2: Date): number { + const momentDate1 = moment(date1); + const momentDate2 = moment(date2); + + const daysDifference = momentDate2.diff(momentDate1, 'days'); + + return daysDifference; + } + + getNumberOfMonthsBetweenDates(date1: Date, date2: Date): number { + const momentDate1 = moment(date1); + const momentDate2 = moment(date2); + + const monthsDifference = momentDate2.diff(momentDate1, 'months') + 1; + + return monthsDifference; + } +} diff --git a/projects/arc-lib/src/lib/components/gantt/services/gantt.service.spec.ts b/projects/arc-lib/src/lib/components/gantt/services/gantt.service.spec.ts index f4ffab9..36d0c46 100644 --- a/projects/arc-lib/src/lib/components/gantt/services/gantt.service.spec.ts +++ b/projects/arc-lib/src/lib/components/gantt/services/gantt.service.spec.ts @@ -5,7 +5,7 @@ import {GanttModule} from '../gantt.module'; import {GanttService} from './gantt.service'; describe('GanttService', () => { - let service: GanttService; + let service: GanttService; beforeEach(() => { TestBed.configureTestingModule({ diff --git a/projects/arc-lib/src/lib/components/gantt/services/gantt.service.ts b/projects/arc-lib/src/lib/components/gantt/services/gantt.service.ts index a2a38c6..25f4f6b 100644 --- a/projects/arc-lib/src/lib/components/gantt/services/gantt.service.ts +++ b/projects/arc-lib/src/lib/components/gantt/services/gantt.service.ts @@ -13,13 +13,26 @@ import { Injector, Type, } from '@angular/core'; + import {GanttEventName} from 'dhtmlx-gantt/codebase/dhtmlxgantt'; -import {AnyObject} from '@project-lib/core/api'; -import {debounceTime, fromEventPattern, Subject} from 'rxjs'; +import {intersection} from 'lodash'; + +import {NgxPermissionsService} from 'ngx-permissions'; +import { + BehaviorSubject, + Subject, + Subscription, + distinct, + fromEventPattern, + map, + switchMap, + tap, +} from 'rxjs'; import {GanttHeaderComponent} from '../components/gantt-header/gantt-header.component'; import {GanttTooltipComponent} from '../components/gantt-tooltip/gantt-tooltip.component'; import { BUFFER_FOR_TODAY, + COLUMN_WIDTH, GANTT, GANTT_BAR_HEIGHT, GANTT_ROW_HEIGHT, @@ -27,39 +40,64 @@ import { GANTT_SCALE_HEIGHT, GANTT_SCROLL_BAR_HEIGHT, GANTT_TIMELINE_MIN_WIDTH, - isHTMLELement, + PARENT_ROW_HEIGHT_HEADINGS, RESIZER_WIDTH, + isHTMLELement, } from '../const'; import {GanttEventTypes} from '../enum'; import { + GanttAdapter, GanttEvent, - GanttLib as gantt, + GanttLib, GanttRenderOptions, GanttScaleOptions, GanttScaleService, GanttTaskValue, + KebabListItem, + TimelineArray, Timelines, } from '../types'; +import {AnyObject} from '@project-lib/core/api'; +import {DIGITS} from '@project-lib/core/constants'; +import {DateOperationService} from './date-operation.service'; +import * as moment from 'moment'; +import {roundToNearestMinutes} from 'date-fns'; -@Injectable({ - providedIn: 'root', -}) +const DEFAULT_TOP_OFFSET = 35; +const DEFAULT_BOTTOM_OFFSET = 5; +@Injectable() export class GanttService { - private _data!: GanttTaskValue[]; + private _data: GanttTaskValue[]; private _overlays: OverlayRef[] = []; - private _tooltipOverlay!: OverlayRef; - private _eventHandlers: string[] = []; - private _descSort!: boolean; - private _events = new Subject>(); - private _moveToToday = true; - private _markToday = true; - private _highlightRange?: [Date, Date]; + private _tooltipOverlay?: OverlayRef; + private _tooltipOpenEvent?: GanttEventTypes; + private _hoverSubcription?: Subscription; + private _descSort = false; + private _highlightRange: [Date, Date]; + private _options?: GanttRenderOptions; + private _events$ = new Subject>(); + private _offset$: BehaviorSubject = new BehaviorSubject(0); + private _reset$: Subject = new Subject(); + private _rendered = false; + private _userPermissions: string[] = []; + private _loadedPage = 0; + private _selectedScale: Timelines; + + get offset() { + return this._offset$.asObservable(); + } get events() { - return this._events.asObservable(); + return this._events$.asObservable(); } + + set options(options: GanttRenderOptions) { + this._options = options; + } + constructor( + private adapter: GanttAdapter, @Inject(GANTT) - private readonly gantt: gantt, + public readonly gantt: GanttLib, @Inject(GANTT_SCALES) private readonly scales: GanttScaleService[], // will have to use this for now @@ -67,9 +105,13 @@ export class GanttService { private resolver: ComponentFactoryResolver, private injector: Injector, private overlay: Overlay, + private dateOperationService: DateOperationService, private overlayPositionBuilder: OverlayPositionBuilder, - ) {} - + private ngxPermissionService: NgxPermissionsService, + ) { + const permissionsObject = this.ngxPermissionService.getPermissions(); + this._userPermissions = Object.keys(permissionsObject); + } /** * It renders the gantt chart in the container element. * It also sets the columns, templates, and other configurations. @@ -79,47 +121,80 @@ export class GanttService { */ render(container: ElementRef, options: GanttRenderOptions) { this._setColumnHeaders(options); + this.gantt.templates.grid_row_class = (start, end, task) => + task.rowClasses?.join(' '); + this.gantt.templates.task_row_class = (start, end, task) => + task.rowClasses?.join(' ').concat(task.columnClasses); this.gantt.templates.task_text = (start, end, task) => - this._renderComponent(options.barComponent, {item: task}); - this.gantt.templates.grid_open = () => ''; - this.gantt.templates.grid_folder = () => ''; - - this._moveToToday = options.moveToToday; - this._highlightRange = options.highlightRange; - this._markToday = options.markToday; + this._renderComponent(options.barComponent, { + item: task, + }); + this.gantt.templates.grid_open = task => ''; + this.gantt.templates.grid_folder = task => ''; + this._options = options; this.gantt.config.row_height = GANTT_ROW_HEIGHT; this.gantt.config.bar_height = GANTT_BAR_HEIGHT; this.gantt.config.scale_height = GANTT_SCALE_HEIGHT; this.gantt.config.readonly = true; this.gantt.config.keyboard_navigation_cells = true; + this.gantt.config.scroll_size = 20; + this.gantt.config.min_column_width = 48; this.gantt.config.layout = { css: 'gantt_container', rows: [ { cols: [ { - view: 'grid', - id: 'grid', - scrollX: 'scrollHor', - scrollY: 'scrollVer', + group: 'left', + rows: [ + { + view: 'grid', + id: 'grid', + scrollY: 'scrollVer', + width: options.columnWidth, + }, + ], width: options.columnWidth, }, { - view: 'timeline', - id: 'timeline', - scrollX: 'scrollHor', - scrollY: 'scrollVer', + group: 'right', + rows: [ + { + cols: [ + { + view: 'timeline', + id: 'timeline', + scrollX: 'scrollHor', + scrollY: 'scrollVer', + minWidth: GANTT_TIMELINE_MIN_WIDTH, + }, + {view: 'scrollbar', scroll: 'y', id: 'scrollVer'}, + ], + }, + ], minWidth: GANTT_TIMELINE_MIN_WIDTH, }, - {view: 'scrollbar', scroll: 'y', id: 'scrollVer'}, ], }, { - view: 'scrollbar', - scroll: 'x', - id: 'scrollHor', height: GANTT_SCROLL_BAR_HEIGHT, + cols: [ + { + group: 'left', + height: 0, + width: options.columnWidth, + css: 'gantt_horizontal_scroll', + }, + { + group: 'right', + view: 'scrollbar', + scroll: 'x', + id: 'scrollHor', + height: GANTT_SCROLL_BAR_HEIGHT, + minWidth: GANTT_TIMELINE_MIN_WIDTH, + }, + ], }, ], }; @@ -137,57 +212,68 @@ export class GanttService { }); // refer - https://forum.dhtmlx.com/t/custom-button-in-grid/34516 - this._eventHandlers.push( - this.gantt.attachEvent( - 'onTaskClick', - (id, e) => { - this._eventHandler(id, e, options); - }, - {}, - ), - ); - this._eventHandlers.push( - this.gantt.attachEvent( - 'onGridHeaderClick', - (id, e) => { - this._eventHandler(id, e, options); - }, - {}, - ), - ); + // this._eventHandlers.push( + // this.gantt.attachEvent( + // 'onTaskClick', + // (id, e) => { + // this._eventHandler(id, e, options); + // }, + // {}, + // ), + // ); + // this.gantt.attachEvent( + // 'onEmptyClick', + // (id, e) => { + // this._eventHandler(id, e, options); + // }, + // {}, + // ); + // this.gantt.attachEvent( + // 'onGridHeaderClick', + // (id, e) => { + // this._eventHandler(id, e, options); + // }, + // {}, + // ); - const hoverObservable = - this.convertToObservable<[string, MouseEvent]>('onMouseMove'); - const debounceTimeinMS = 100; - hoverObservable - .pipe(debounceTime(debounceTimeinMS)) - // eslint-disable-next-line - .subscribe(([id, event]) => { - this._hoverEventHandler(event, options); - }); + // const hoverObservable = + // this.convertToObservable<[string, MouseEvent]>('onMouseMove'); + // this._hoverSubcription = hoverObservable.subscribe(([id, event]) => { + // this._hoverEventHandler(event, options); + // }); - this._eventHandlers.push( - this.gantt.attachEvent( - 'onBeforeGanttRender', - () => { - const range = this.gantt.getSubtaskDates(); - if (range.start_date && range.end_date) { - const today = new Date(); - today.setDate(today.getDate() + BUFFER_FOR_TODAY); - // as per requirement, need to always show current date in gantt - this.gantt.config.start_date = new Date( - Math.min(range.start_date.getTime(), today.getTime()), - ); - this.gantt.config.end_date = new Date( - Math.max(range.end_date.getTime(), today.getTime()), - ); - } - }, - {}, - ), + this.gantt.attachEvent( + 'onBeforeGanttRender', + () => { + this._tooltipOverlay?.dispose(); + this._setGanttStartAndEndDates(options); + }, + {}, + ); + this.gantt.attachEvent( + 'onBeforeTaskDisplay', + (id, task) => { + if ( + this._options?.ganttStartDate && + moment(task.start_date).isSame(moment(task.end_date)) + ) { + task.start_date = this._options.ganttStartDate; + task.end_date = this._options.ganttStartDate; + } + if (task.isLabel) { + return !!this.gantt.hasChild(task.id); + } + return true; + }, + {}, ); this.gantt.init(container.nativeElement); + this._rendered = true; + if (options.infiniteScroll) { + this._setupPagination(); + this._reset$.next(0); + } this._renderTodayMarker(); this.setScale(options.defaultScale, false); } @@ -197,9 +283,28 @@ export class GanttService { * It also calls the adapter to convert the data to the format that the Gantt chart expects. * @param {T[]} data - The data that you want to feed to the Gantt chart. */ - feed() { - //this._data = this.adapter.adaptFrom(data); - this._refresh(); + feed(data: T[]) { + this._data = this.adapter.adaptFrom(data); + this.refresh(); + } + /** + * It adds new data to the Gantt chart, required when the gantt is in infinite scroll mode. + * @param {T[]} data - T[] - the data to add to the gantt chart + */ + add(data: T[]) { + if (this._rendered) { + // add groupings as they can not be added through adapter in infinite scroll mode + this._buildGroupings(); + const scrollState = this.gantt.getScrollState(); + let newData = this.adapter.adaptFrom(data); + newData = newData.filter(r => !this.gantt.isTaskExists(r.id)); + this._data = [...(this._data ?? []), ...newData]; + this.gantt.parse({ + tasks: newData, + }); + // to restore scroll state after new data is added + this.gantt.scrollTo(scrollState.x, scrollState.y); + } } /** @@ -209,17 +314,30 @@ export class GanttService { */ setScale(type: Timelines, options?: GanttScaleOptions, render = true) { const scale = this.scales.find(s => s.scale === type); + this._selectedScale = type; if (scale) { this.gantt.config.scales = scale.config(options); } if (scale && render) { - this._rerender(); + this.rerender(); } } + public get selectedScale() { + return this._selectedScale; + } highlightRange(range: [Date, Date]) { this._highlightRange = range; - this._refresh(); + this.refresh(); + } + + /** + * The function clears the data array, clears the gantt chart, and then emits a value to the reset + * observable for scroll offset + */ + refreshInfiniteScroll() { + this.clear(); + this._reset$.next(0); } /** @@ -227,16 +345,16 @@ export class GanttService { */ destroy() { this.gantt.destructor(); + this._closeOverlays(); + this._hoverSubcription?.unsubscribe(); } /** * It clears all the tasks and links from the Gantt chart */ clear() { + this._data = []; this.gantt.clearAll(); - for (const handlers of this._eventHandlers) { - this.gantt.detachEvent(handlers); - } } /** @@ -248,7 +366,7 @@ export class GanttService { } } - private _refresh() { + refresh() { this.gantt.clearAll(); this.gantt.parse({ tasks: this._data ?? [], @@ -256,7 +374,36 @@ export class GanttService { if (this._descSort !== undefined) { this.gantt.sort('name', this._descSort, undefined, true); } - this._rerender(); + this.rerender(); + } + + rerender(moveToSpecificDate = true) { + this.gantt.render(); + this._closeOverlays(); + this._renderTodayMarker(); + this._renderHighlighMarker(); + if (moveToSpecificDate) { + if (this._options?.moveToToday) { + this.gantt.showDate(new Date()); + } else { + this.gantt.showDate(this.gantt.config.start_date); + } + } + } + + private _buildGroupings() { + // const groupItems = []; + // for (let grouping of this._options?.groupings ?? []) { + // const [task] = this.gantt.getTaskBy('id', grouping); + // if (!task) { + // groupItems.push(this._buildLabelObject(grouping)); + // } + // } + // if (groupItems.length) { + // this.gantt.parse({ + // tasks: groupItems, + // }); + // } } private _renderHighlighMarker() { @@ -269,17 +416,8 @@ export class GanttService { } } - private _rerender() { - this.gantt.render(); - this._renderTodayMarker(); - this._renderHighlighMarker(); - if (this._moveToToday) { - this.gantt.showDate(new Date()); - } - } - private _renderTodayMarker() { - if (this._markToday) { + if (this._options?.markToday) { this.gantt.addMarker?.({ start_date: new Date(), css: 'today', @@ -294,62 +432,115 @@ export class GanttService { label: this._renderComponent(GanttHeaderComponent, { desc: this._descSort, name: options.columnName, - searchPlaceholder: options.searchPlaceholder, - showSearch: options.showSearch, + // showSorting: options.sorting, }), width: options.columnWidth, tree: true, - template: (item: GanttTaskValue) => - this._renderComponent(options.columnComponent, { + template: (item: GanttTaskValue) => { + let filteredItems = + (item && options.contextItemFilter?.(item)) ?? options.contextItems; + if (options.kebabOption && !filteredItems.length) { + filteredItems = options.kebabOption(this.gantt.getTask(item.id)); + } + filteredItems = filteredItems.filter(item => + // !item.permissions || + // intersection(this._userPermissions, item.permissions).length > 0, + console.log(item), + ); + return this._renderComponent(options.columnComponent, { item, - contextItems: options.contextItems, + contextItems: filteredItems, showKebab: options.showKebab, showParentInitials: options.showParentInitials, showChildInitials: options.showChildInitials, showOverallocatedIcon: options.showOverallocatedIcon, - contextItemFilter: options.contextItemFilter, - }), + }); + }, }, ]; } - private _eventHandler( - id: number, - event: MouseEvent, - options: GanttRenderOptions, - ) { - if (event.target && isHTMLELement(event.target)) { - const target = event.target.closest('[data-gantt-click]'); - if (!target) { - return; - } - const attribute = target.getAttribute('data-gantt-click'); - const task = this.gantt.getTask(id); - switch (attribute) { - case GanttEventTypes.Kebab: - this._handleKebabClick(id, event, options); - break; - case GanttEventTypes.Expand: - if (!task.$open) { - this.gantt.open(id); - } else { - this.gantt.close(id); - } - break; - case GanttEventTypes.Sort: - this._descSort = !this._descSort; - this._setColumnHeaders(options); - this.gantt.sort('name', this._descSort); - break; - default: - this._events.next({ - task: this.gantt.getTask(id), - event: attribute ?? GanttEventTypes.Unknown, - }); - } - } + private _setupPagination() { + const scroll$ = this.convertToObservable<[number, number]>('onGanttScroll'); + this._reset$ + .pipe( + tap(_ => { + this._loadedPage = 0; + this.gantt.scrollTo(0, 0); + this._offset$.next(0); + }), + switchMap(() => + scroll$.pipe( + map(() => { + const visibleTasks = this.gantt.getVisibleTaskCount(); + const lastVisibleTask = this.gantt.getTaskByIndex( + visibleTasks - 1, + ); + if (this.gantt.getTaskRowNode(lastVisibleTask?.id)) { + return lastVisibleTask?.id; + } + return 0; + }), + distinct(), + ), + ), + ) + .subscribe(_ => { + this._offset$.next(this._loadedPage++); + }); } + // private _eventHandler( + // id: number, + // event: MouseEvent, + // options: GanttRenderOptions, + // ) { + // this._tooltipClose(event?.target, GanttEventTypes.Hover); + // if (event?.target && isHTMLELement(event.target)) { + // const target = event.target.closest('[gantt-click]'); + // if (!target) { + // return; + // } + // const attribute = target.getAttribute('gantt-click'); + // switch (attribute) { + // case GanttEventValues.Kebab: + // if (options.kebabOption) { + // const list = options.kebabOption(this.gantt.getTask(id)); + // this._handleKebabClick(id, event, options, target, list); + // } else { + // this._handleKebabClick(id, event, options, target); + // } + // break; + // case GanttEventValues.Expand: { + // const task = this.gantt.getTask(id); + // if (!task.$open) { + // this.gantt.open(id); + // } else { + // this.gantt.close(id); + // } + // break; + // } + // case GanttEventValues.Sort: + // this._descSort = !this._descSort; + // this._setColumnHeaders(options); + // this.gantt.sort('name', this._descSort); + // break; + // case GanttEventValues.Tooltip: + // this._openTooltip(target, GanttEventTypes.Click, options, event); + // break; + // case GanttEventValues.ExpandBar: + // this._expandGanttBar(target, options); + // break; + // default: + // this._events$.next({ + // task: this.gantt.getTask(id), + // event: attribute ?? GanttEventValues.Unknown, + // permissions: this._userPermissions, + // }); + // } + // } + // } + private _renderComponent( c: Type, inputs: Partial<{[key in keyof T]: T[keyof T]}>, @@ -370,7 +561,10 @@ export class GanttService { id: number, e: MouseEvent, options: GanttRenderOptions, + target: Element, + contextItems?: KebabListItem[], ) { + this._markClicked(target); const positionStrategy = this.overlay .position() .global() @@ -381,100 +575,373 @@ export class GanttService { panelClass: ['gantt-menu-overlay'], backdropClass: 'modal-background', positionStrategy, - scrollStrategy: this.overlay.scrollStrategies.block(), }); const overlay = this.overlay.create(configs); + const contextItemsAttribute = target.getAttribute('gantt-kebab-items'); + + if (!contextItems && contextItemsAttribute) { + contextItems = JSON.parse(contextItemsAttribute); + } + + contextItems = contextItems?.filter( + item => + !item.permissions || + intersection(this._userPermissions, item.permissions).length > 0, + ); if (options.contextTemplate && options.viewContainerRef) { const item = this._data.find(d => d.id === id); overlay.attach( new TemplatePortal(options.contextTemplate, options.viewContainerRef, { item, - contextItems: - (item && options.contextItemFilter?.(item)) ?? options.contextItems, + contextItems, }), ); } overlay.backdropClick().subscribe(() => { overlay.dispose(); + this._unmarkClicked(target); }); this._overlays.push(overlay); } - private _hoverEventHandler( - event: MouseEvent, + // private _hoverEventHandler( + // event: MouseEvent, + // options: GanttRenderOptions, + // ) { + // if (event.target && isHTMLELement(event.target) && options.showTooltip) { + // const target = event.target.closest('[gantt-hover]'); + // const attribute = target?.getAttribute('gantt-hover')!; + // if (target) { + // switch (attribute) { + // case GanttEventValues.Tooltip: + // this._openTooltip(target, GanttEventTypes.Hover, options, event); + // return; + // case GanttEventValues.OpenedTooltip: + // return; + // } + // } + // } + // this._tooltipClose(event.target, GanttEventTypes.Hover); + // } + + private _openTooltip( + target: Element, + event: GanttEventTypes, options: GanttRenderOptions, + e: MouseEvent, ) { - if (event.target && isHTMLELement(event.target) && options.showTooltip) { - const target = event.target.closest('[gantt-hover]'); - const attribute = target?.getAttribute('gantt-hover'); - if (target) { - switch (attribute) { - case GanttEventTypes.Bar: - this._handleHoverOnBar(target, 'gantt-bar-data'); - return; - case GanttEventTypes.Tooltip: - return; - } - } - } - if (this._tooltipOverlay) { - this._tooltipOverlay.dispose(); - } - } + const component = target.getAttribute('gantt-tooltip-component'); - private _handleHoverOnBar(target: Element, tag: string) { if (this._tooltipOverlay) { - this._tooltipOverlay.dispose(); + this._tooltipClose(target, event); } - const offset = 35; + const bottomOffset = options.tooltipOffset; + const elementPosition = target.getBoundingClientRect().left; + const positionStrategy = this.overlayPositionBuilder .flexibleConnectedTo(target) .withPositions([ { - originX: 'center', + originX: 'start', originY: 'top', - overlayX: 'center', + overlayX: 'start', overlayY: 'top', - offsetY: offset, + offsetY: bottomOffset, + offsetX: e.clientX - elementPosition - DIGITS.FIVE, }, { - originX: 'center', + originX: 'start', originY: 'top', - overlayX: 'center', + overlayX: 'start', overlayY: 'bottom', - offsetY: -5, + offsetX: e.clientX - elementPosition - DIGITS.FIVE, + }, + { + originX: 'start', + originY: 'top', + overlayX: 'end', + overlayY: 'top', + offsetY: bottomOffset, + offsetX: e.clientX - elementPosition - DIGITS.FIVE, + }, + { + originX: 'start', + originY: 'top', + overlayX: 'end', + overlayY: 'bottom', + offsetX: e.clientX - elementPosition - DIGITS.FIVE, }, ]); - const configs = new OverlayConfig({ + const configString = target.getAttribute('gantt-tooltip-config'); + let tooltipConfig = { panelClass: ['gantt-tooltip-overlay'], positionStrategy, - }); + }; + if (configString) { + const config = JSON.parse(configString); + tooltipConfig = { + ...tooltipConfig, + ...config, + }; + } + const configs = new OverlayConfig(tooltipConfig); this._tooltipOverlay = this.overlay.create(configs); + this._tooltipOpenEvent = event; - const attributeHover = target.getAttribute(tag); - if (attributeHover && tag === 'gantt-bar-data') { + let tooltipData; + const dataAttributeValue = target.getAttribute('gantt-tooltip-data'); + const idAttributeValue = target.getAttribute('gantt-tooltip-data-row-id'); + if (dataAttributeValue) { + tooltipData = JSON.parse(dataAttributeValue); + } else if (idAttributeValue) { + tooltipData = { + item: this.gantt.getTask(idAttributeValue), + }; + } else { + // do nothing + } + const topOffset = 15; + if (tooltipData) { const tooltipRef = this._tooltipOverlay.attach( new ComponentPortal(GanttTooltipComponent), ); - tooltipRef.instance.item = JSON.parse(attributeHover); - this._overlays.push(this._tooltipOverlay); + tooltipRef.location.nativeElement.setAttribute('gantt-hover'); + this._buildPadAround(tooltipRef.location.nativeElement, topOffset); + Object.assign(tooltipRef.instance, tooltipData); + this._tooltipOverlay.backdropClick().subscribe(() => { + this._tooltipOverlay?.dispose(); + }); + } else { + this._tooltipOverlay.dispose(); } } convertToObservable(eventName: GanttEventName) { return fromEventPattern(handler => { - this._eventHandlers.push( - this.gantt.attachEvent( - eventName, - (id, e) => { - handler(id, e); - }, - {}, - ), + this.gantt.attachEvent( + eventName, + (id, e) => { + handler(id, e); + }, + {}, ); }); } + + private _buildPadAround( + element: HTMLElement, + top = DEFAULT_TOP_OFFSET, + bottom = DEFAULT_BOTTOM_OFFSET, + ) { + element.style.paddingTop = `${top}px`; + element.style.paddingBottom = `${bottom}px`; + } + + private _tooltipClose(element: EventTarget | null, event: GanttEventTypes) { + if (element === this._tooltipOverlay?.hostElement) { + return; + } + if (event !== this._tooltipOpenEvent) { + return; + } + this._tooltipOverlay?.dispose(); + } + + private _closeOverlays() { + this._overlays.forEach(o => o.dispose()); + this._overlays = []; + this._tooltipOverlay?.dispose(); + this._tooltipOverlay = undefined; + this._tooltipOpenEvent = undefined; + } + + private _buildLabelObject(id: string) { + return { + id: id, + start_date: new Date(), + end_date: new Date(), + name: id, + hasChildren: true, + isParent: true, + allocation: 0, + $open: true, + payload: {}, + barClasses: ['remove'], + isLabel: true, + row_height: PARENT_ROW_HEIGHT_HEADINGS, + rowClasses: ['border '], + }; + } + + // to keep the kebab visible while the menu is open + private _markClicked(target: Element) { + if (!isHTMLELement(target)) { + return; + } + target.parentElement?.classList.add('clicked-gantt-item'); + } + private _unmarkClicked(target: Element) { + if (!isHTMLELement(target)) { + return; + } + target.parentElement?.classList.remove('clicked-gantt-item'); + } + private _isHTMLElement(target: Element): target is HTMLElement { + return !!(target as HTMLElement).parentElement; + } + + private _setGanttStartAndEndDates(options: GanttRenderOptions) { + const range = this.gantt.getSubtaskDates(); + if (range.start_date && range.end_date) { + const today = new Date(); + today.setDate(today.getDate() + BUFFER_FOR_TODAY); + // as per requirement, need to always show current date in gantt + if (!options.ganttStartDate) { + this.gantt.config.start_date = new Date( + Math.min(range.start_date.getTime(), today.getTime()), + ); + } + if (this._options?.ganttStartDate) { + this.gantt.config.start_date = this._options.ganttStartDate!; + } + this.gantt.config.end_date = new Date( + Math.max(range.end_date.getTime(), today.getTime()), + ); + } + } + + private _expandGanttBar(target: Element, options: GanttRenderOptions) { + let dataAttribute: GanttTaskValue | undefined; + const ganttRowIdAttribute = target.getAttribute('gantt-row-id'); + const ganttRowAttribute = target.getAttribute('gantt-row'); + if (ganttRowAttribute) { + dataAttribute = JSON.parse(ganttRowAttribute); + } else if (ganttRowIdAttribute) { + dataAttribute = this.gantt.getTask(ganttRowIdAttribute); + } else { + // do nothing + } + + if (dataAttribute) { + const parentTaskId = dataAttribute.parent; + + if (parentTaskId) { + const parentTask = this.gantt.getTask(parentTaskId); + const task = this.gantt.getTask(dataAttribute.id); + + this._setBarHeight(parentTask, task, dataAttribute, options); + + task.$open = !task.$open; + + this.gantt.updateTask(parentTaskId as string, parentTask); + this.gantt.updateTask(dataAttribute.id as string, task); + + this.rerender(false); + } + } + } + + private _setBarHeight( + parentTask: GanttTaskValue, + task: GanttTaskValue, + dataAttribute: GanttTaskValue, + options: GanttRenderOptions, + ) { + const rowConfig = options.ganttRowConfig; + const noOfBars = dataAttribute.payload['noOfBars']; + + if (!task.$open) { + /* If the task is expanded, we need to increase bar height and row height + of its parent task. If the calculated bar height and row height are more than + bar height and row height of parent task, we will update the heights of parent task*/ + const barHeight = rowConfig.rowHeight + rowConfig.rowBuffer * noOfBars; + const rowHeight = + rowConfig.rowHeight + + rowConfig.rowBuffer * noOfBars + + rowConfig.actualRowSize; + if ( + barHeight > (parentTask.bar_height ?? 0) && + rowHeight > (parentTask.row_height ?? 0) + ) { + parentTask.bar_height = barHeight; + parentTask.row_height = rowHeight; + } + } else { + /* If any task is collpased, we will update the height of parent task to + max height of its opened sibling tasks */ + + const siblings = this.gantt + .getSiblings(dataAttribute.id) + .map(sibling => this.gantt.getTask(sibling)) + .filter(sibling => sibling.id !== dataAttribute.id && sibling.$open); + + let maxSize = -1; + + parentTask.bar_height = this.gantt.config.bar_height as number; + parentTask.row_height = this.gantt.config.row_height; + + siblings.forEach(sibling => { + const siblingBarSize = sibling.payload['noOfBars']; + if (maxSize < siblingBarSize) { + maxSize = siblingBarSize; + parentTask.bar_height = + rowConfig.rowHeight + rowConfig.rowBuffer * maxSize; + parentTask.row_height = + rowConfig.rowHeight + + rowConfig.rowBuffer * maxSize + + rowConfig.actualRowSize; + } + }); + } + } + + fitToScreen() { + const maxMinDates = this.gantt.getSubtaskDates(); + const ganttArea = + this.gantt['$layout'].$gantt.$task.getBoundingClientRect(); + const noOfColumns = Math.floor(ganttArea.width / COLUMN_WIDTH); + const noOfDays = this.dateOperationService.getNumberOfDaysBetweenDates( + maxMinDates.start_date, + maxMinDates.end_date, + ); + const noOfWeeks = this.dateOperationService.calculateWeeksBetweenDates( + maxMinDates.start_date, + maxMinDates.end_date, + ); + const noOfMonths = this.dateOperationService.getNumberOfMonthsBetweenDates( + maxMinDates.start_date, + maxMinDates.end_date, + ); + + switch (true) { + case noOfDays < noOfColumns: + this.setScale(Timelines.Weekly); + break; + case noOfWeeks < noOfColumns: + this.setScale(Timelines.Monthly); + break; + case noOfMonths < noOfColumns: + this.setScale(Timelines.Quarterly); + break; + default: + this.setScale(Timelines.Yearly); + } + } + + zoomOut() { + const currentIndex = TimelineArray.indexOf(this._selectedScale); + if (currentIndex < TimelineArray.length - 1) { + this.setScale(TimelineArray[currentIndex + 1]); + } + } + + zoomIn() { + const currentIndex = TimelineArray.indexOf(this._selectedScale); + if (currentIndex > 0) { + this.setScale(TimelineArray[currentIndex - 1]); + } + } } diff --git a/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/monthly-scale.service.ts b/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/monthly-scale.service.ts index 2cd0d0e..cf4ff02 100644 --- a/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/monthly-scale.service.ts +++ b/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/monthly-scale.service.ts @@ -1,6 +1,9 @@ import {Injectable} from '@angular/core'; import {GanttScaleUnits} from '../../enum'; import {GanttScaleService, Timelines} from '../../types'; +import {AnyObject} from '@project-lib/core/api'; +import {DIGITS} from '@project-lib/core/constants'; +import {GanttService} from '../gantt.service'; @Injectable() export class MonthlyScaleService implements GanttScaleService { @@ -21,4 +24,29 @@ export class MonthlyScaleService implements GanttScaleService { }, ]; } + + scroll(forward: boolean, ganttService: GanttService): void { + const currentScrollState: number = ganttService.gantt.getScrollState().x; + const currentScrollDate: Date = + ganttService.gantt.dateFromPos(currentScrollState); + const newScrollDate: Date = ganttService.gantt.date.add( + currentScrollDate, + forward ? +DIGITS.ONE : -DIGITS.ONE, + 'month', + ); + const newScrollState: number = + ganttService.gantt.posFromDate(newScrollDate); + ganttService.gantt.scrollTo(newScrollState, null); + } + moveToToday(ganttService: GanttService): void { + const dateToday: Date = new Date(); + const newScrollDate: Date = ganttService.gantt.date.add( + dateToday, + -DIGITS.ONE, + 'month', + ); + const newScrollState: number = + ganttService.gantt.posFromDate(newScrollDate); + ganttService.gantt.scrollTo(newScrollState, null); + } } diff --git a/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/quarterly-scale.service.ts b/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/quarterly-scale.service.ts index c39eddb..fd9735c 100644 --- a/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/quarterly-scale.service.ts +++ b/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/quarterly-scale.service.ts @@ -2,6 +2,9 @@ import {Injectable} from '@angular/core'; import {MONTHS_IN_QUARTER} from '../../const'; import {GanttScaleUnits} from '../../enum'; import {GanttScaleService, Timelines} from '../../types'; +import {AnyObject} from '@project-lib/core/api'; +import {DIGITS} from '@project-lib/core/constants'; +import {GanttService} from '../gantt.service'; @Injectable() export class QuarterlyScaleService implements GanttScaleService { @@ -21,6 +24,30 @@ export class QuarterlyScaleService implements GanttScaleService { }, ]; } + scroll(forward: boolean, ganttService: GanttService): void { + const currentScrollState: number = ganttService.gantt.getScrollState().x; + const currentScrollDate: Date = + ganttService.gantt.dateFromPos(currentScrollState); + const newScrollDate: Date = ganttService.gantt.date.add( + currentScrollDate, + forward ? +DIGITS.FOUR : -DIGITS.FOUR, + 'month', + ); + const newScrollState: number = + ganttService.gantt.posFromDate(newScrollDate); + ganttService.gantt.scrollTo(newScrollState, null); + } + moveToToday(ganttService: GanttService): void { + const dateToday: Date = new Date(); + const newScrollDate: Date = ganttService.gantt.date.add( + dateToday, + -DIGITS.FOUR, + 'month', + ); + const newScrollState: number = + ganttService.gantt.posFromDate(newScrollDate); + ganttService.gantt.scrollTo(newScrollState, null); + } private _formatQuarterScale(date: Date) { const month = date.getMonth(); diff --git a/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/weekly-scale.service.ts b/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/weekly-scale.service.ts index efba020..747b8e3 100644 --- a/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/weekly-scale.service.ts +++ b/projects/arc-lib/src/lib/components/gantt/services/timeline-scales/weekly-scale.service.ts @@ -1,6 +1,9 @@ import {Injectable} from '@angular/core'; import {GanttScaleUnits} from '../../enum'; import {GanttScaleService, Timelines} from '../../types'; +import {AnyObject} from '@project-lib/core/api'; +import {DIGITS} from '@project-lib/core/constants'; +import {GanttService} from '../gantt.service'; @Injectable() export class WeeklyScaleService implements GanttScaleService { @@ -22,6 +25,30 @@ export class WeeklyScaleService implements GanttScaleService { ]; } + scroll(forward: boolean, ganttService: GanttService): void { + const currentScrollState: number = ganttService.gantt.getScrollState().x; + const currentScrollDate: Date = + ganttService.gantt.dateFromPos(currentScrollState); + const newScrollDate: Date = ganttService.gantt.date.add( + currentScrollDate, + forward ? +DIGITS.ONE : -DIGITS.ONE, + 'week', + ); + const newScrollState: number = + ganttService.gantt.posFromDate(newScrollDate); + ganttService.gantt.scrollTo(newScrollState, null); + } + moveToToday(ganttService: GanttService): void { + const dateToday: Date = new Date(); + const newScrollDate: Date = ganttService.gantt.date.add( + dateToday, + -DIGITS.ONE, + 'week', + ); + const newScrollState: number = + ganttService.gantt.posFromDate(newScrollDate); + ganttService.gantt.scrollTo(newScrollState, null); + } private _formatWeeklyScale(date: Date) { const noOfDigits = 2; return `${date.toLocaleString('default', {month: 'short'})} ${date diff --git a/projects/arc-lib/src/lib/components/gantt/testing/gantt-service-stub.ts b/projects/arc-lib/src/lib/components/gantt/testing/gantt-service-stub.ts new file mode 100644 index 0000000..9023b04 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/testing/gantt-service-stub.ts @@ -0,0 +1,55 @@ +import {ElementRef, Injectable} from '@angular/core'; +import {AnyObject} from '@project-lib/core/api'; + +import {Subject} from 'rxjs'; +import { + GanttEvent, + GanttRenderOptions, + Timelines, + GanttScaleOptions, +} from '../types'; + +@Injectable() +export class GanttServiceStub { + private _events = new Subject>(); + private _offset = new Subject(); + get events() { + return this._events.asObservable(); + } + + get offset() { + return this._offset.asObservable(); + } + + render(container: ElementRef, options: GanttRenderOptions) { + // this is intentional + } + + feed(data: T[]) { + // this is intentional + } + + setScale(type: Timelines, options?: GanttScaleOptions, render = true) { + // this is intentional + } + + refreshInfiniteScroll() { + // this is intentional + } + + destroy() { + // this is intentional + } + + clear() { + // this is intentional + } + + closeContextMenu() { + // this is intentional + } + + triggerEvent(event: GanttEvent) { + this._events.next(event); + } +} diff --git a/projects/arc-lib/src/lib/components/gantt/types.ts b/projects/arc-lib/src/lib/components/gantt/types.ts index f4dc372..6976ed3 100644 --- a/projects/arc-lib/src/lib/components/gantt/types.ts +++ b/projects/arc-lib/src/lib/components/gantt/types.ts @@ -4,6 +4,7 @@ import {DIGITS, ONE_MIN} from '@project-lib/core/constants'; import {NbMenuItem} from '@nebular/theme'; import {RANDOM_SIZE} from './const'; import {gantt} from 'dhtmlx-gantt'; +import {GanttService} from './services'; /** * `GanttTaskValue` is a type that represents a task in the Gantt chart. @@ -67,7 +68,7 @@ export interface GanttTaskValueWithSubAllocation extends BaseTaskValue { subAllocations: SubAllocation[]; } -export type GanttScaleService = { +export type GanttScaleService = { scale: Timelines; config(options?: GanttScaleOptions): { unit: string; @@ -75,6 +76,8 @@ export type GanttScaleService = { format: (date: Date) => string; css?: (date: Date) => string; }[]; + scroll(forward: boolean, ganttService: GanttService): void; + moveToToday(ganttService: GanttService): void; }; // will be required for custom scale @@ -93,6 +96,7 @@ export type GanttEvent = { export type GanttRenderOptions = { contextItems: NbMenuItem[]; contextTemplate?: TemplateRef; + // toolTip?: Type>; viewContainerRef?: ViewContainerRef; columnName?: string; showKebab: boolean; @@ -102,18 +106,28 @@ export type GanttRenderOptions = { barComponent: Type>; columnWidth: number; resizer: boolean; - searchPlaceholder?: string; - showSearch: boolean; + sorting: boolean; moveToToday: boolean; highlightRange?: [Date, Date]; showOverallocatedIcon: boolean; + showNonBillableIcon: boolean; contextItemFilter?: ContextItemFilter; defaultScale: Timelines; markToday: boolean; showTooltip?: boolean; + showBillingRate?: boolean; + groupings?: string[]; childIndent: boolean; + // tooltipComponents: Record>>; + tooltipOffset?: number; + infiniteScroll: boolean; + batchSize: number; + searchPlaceholder?: string; + showSearch: boolean; + ganttStartDate?: Date; + kebabOption: (task: GanttTaskValue) => KebabListItem[]; + ganttRowConfig: GanttRowConfig; }; - export type GanttAllocationFields = { startDate: Date; endDate: Date; @@ -127,6 +141,7 @@ export enum Timelines { Monthly, Quarterly, Custom, + Yearly, } export const GanttTimelineMap: { @@ -136,6 +151,7 @@ export const GanttTimelineMap: { [Timelines.Monthly]: 'Monthly', [Timelines.Quarterly]: 'Quarterly', [Timelines.Custom]: 'Custom', + [Timelines.Yearly]: 'Yearly', }; export abstract class GanttAdapter { abstract adaptFrom(data: T[]): GanttTaskValue[]; @@ -216,6 +232,27 @@ export type IBarComponent = { item: GanttTaskValue; }; +export type GanttRowConfig = { + rowHeight: number; + actualRowSize: number; + rowBuffer: number; +}; + +export const TimelineArray: Timelines[] = [ + Timelines.Weekly, + Timelines.Monthly, + Timelines.Quarterly, + Timelines.Yearly, +]; +export interface KebabListItem extends NbMenuItem { + itemClass?: string[]; + iconClass?: string[]; + titleClass?: string[]; + permissions?: string[]; + tooltipData?: string; + disabled?: boolean; +} + export type SubAllocation = { percent: number; allocation: number; diff --git a/projects/arc/src/app/app-routing.module.ts b/projects/arc/src/app/app-routing.module.ts index 80ac72b..52eecad 100644 --- a/projects/arc/src/app/app-routing.module.ts +++ b/projects/arc/src/app/app-routing.module.ts @@ -3,44 +3,46 @@ import {RouterModule, Routes} from '@angular/router'; import {environment} from '../environments/environment'; import {AuthGuard, LoggedInGuard} from '@project-lib/core/auth'; import {GanttDemoComponent} from './components/gantt-demo/gantt-demo.component'; +import {GanttZoomBarComponent} from '@project-lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component'; +import {GanttScrollComponent} from '@project-lib/components/gantt/components/gantt-scroll/gantt-scroll.component'; +import {GanttTooltipComponent} from '@project-lib/components/gantt/components'; const routes: Routes = [ - { - path: 'auth', - loadChildren: () => - import('projects/arc-lib/src/lib/components/auth/auth.module').then( - m => m.AuthModule, - ), - canActivate: [LoggedInGuard], - }, - { - path: 'main', - loadChildren: () => import('./main/main.module').then(m => m.MainModule), - canActivate: [AuthGuard], - }, - { - path: 'gantt', - loadChildren: () => - import('../../../arc-lib/src/lib/components/gantt/gantt.module').then( - m => m.GanttModule, - ), - canActivate: [AuthGuard], - }, - + // { + // path: 'auth', + // loadChildren: () => + // import('projects/arc-lib/src/lib/components/auth/auth.module').then( + // m => m.AuthModule, + // ), + // canActivate: [LoggedInGuard], + // }, + // { + // path: 'main', + // loadChildren: () => import('./main/main.module').then(m => m.MainModule), + // canActivate: [AuthGuard], + // }, + // { + // path: 'gantt', + // loadChildren: () => + // import('../../../arc-lib/src/lib/components/gantt/gantt.module').then( + // m => m.GanttModule, + // ), + // canActivate: [AuthGuard], + // }, { path: 'gantt-demo', component: GanttDemoComponent, }, - { - path: '', - redirectTo: environment.homePath, - pathMatch: 'full', - }, - { - path: '**', - redirectTo: environment.homePath, - }, + // { + // path: '', + // redirectTo: environment.homePath, + // pathMatch: 'full', + // }, + // { + // path: '**', + // redirectTo: environment.homePath, + // }, ]; @NgModule({ diff --git a/projects/arc/src/app/app.module.ts b/projects/arc/src/app/app.module.ts index 3e380bb..985babd 100644 --- a/projects/arc/src/app/app.module.ts +++ b/projects/arc/src/app/app.module.ts @@ -23,7 +23,6 @@ import {GanttModule} from '@project-lib/components/index'; import {SelectModule} from '@project-lib/components/selector'; import {HeaderComponent} from '@project-lib/components/header/header.component'; import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.component'; -import {GanttService} from '@project-lib/components/gantt'; import {GanttDemoComponent} from './components/gantt-demo/gantt-demo.component'; @NgModule({ diff --git a/projects/arc/src/app/components/gantt-demo/gantt-demo.component.html b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.html index 8f5e367..f64fcd0 100644 --- a/projects/arc/src/app/components/gantt-demo/gantt-demo.component.html +++ b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.html @@ -37,3 +37,5 @@ + + diff --git a/projects/arc/src/app/components/gantt-demo/gantt-demo.component.ts b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.ts index 6ffdfbd..e2f9fee 100644 --- a/projects/arc/src/app/components/gantt-demo/gantt-demo.component.ts +++ b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.ts @@ -1,11 +1,23 @@ import {Component} from '@angular/core'; import {NbSidebarService} from '@nebular/theme'; +import { + GanttProviders, + GanttAdapter, + CustomGanttAdapter, +} from '@project-lib/components/gantt'; import {Item, empData} from '@project-lib/components/gantt/model/item.model'; @Component({ selector: 'arc-gantt-demo', templateUrl: './gantt-demo.component.html', styleUrls: ['./gantt-demo.component.scss'], + providers: [ + GanttProviders, + { + provide: GanttAdapter, + useClass: CustomGanttAdapter, + }, + ], }) export class GanttDemoComponent { // data for tooltip component diff --git a/projects/arc/src/app/components/gantt.component.html b/projects/arc/src/app/components/gantt.component.html new file mode 100644 index 0000000..d7165ba --- /dev/null +++ b/projects/arc/src/app/components/gantt.component.html @@ -0,0 +1,34 @@ +
+ +
+ +
+ + +
+ +
+ + + + +
+
+
diff --git a/projects/arc/src/app/components/gantt.component.scss b/projects/arc/src/app/components/gantt.component.scss new file mode 100644 index 0000000..e7ea411 --- /dev/null +++ b/projects/arc/src/app/components/gantt.component.scss @@ -0,0 +1,39 @@ +@use 'sass:map'; + +@import '../../../../arc-lib/src/lib/theme/styles/_variables.scss'; +//@use "projects/arc-lib/src/lib/theme/styles/_variables.scss"; + +.layout-container { + display: flex; + flex-direction: column; + height: 100vh; + + .header { + width: 100%; + background-color: #f8f9fa; + padding: 1rem; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .content { + display: flex; + flex-grow: 1; + + .sidebar { + width: 20%; + background-color: #e9ecef; + padding: 1rem; + box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1); + overflow-y: auto; + } + + .main { + flex-grow: 1; + padding: 1rem; + background-color: #ffffff; + box-shadow: -2px 0 4px rgba(0, 0, 0, 0.1); + overflow-y: auto; + } + } +} diff --git a/projects/arc/src/app/components/gantt.component.spec.ts b/projects/arc/src/app/components/gantt.component.spec.ts new file mode 100644 index 0000000..efe241f --- /dev/null +++ b/projects/arc/src/app/components/gantt.component.spec.ts @@ -0,0 +1,22 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {GanttComponent} from './gantt.component'; + +describe('GanttComponent', () => { + let component: GanttComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GanttComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(GanttComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/arc/src/app/components/gantt.component.ts b/projects/arc/src/app/components/gantt.component.ts new file mode 100644 index 0000000..6450fe1 --- /dev/null +++ b/projects/arc/src/app/components/gantt.component.ts @@ -0,0 +1,88 @@ +import {Component} from '@angular/core'; +import {Item, empData} from '@project-lib/components/gantt/model/item.model'; + +@Component({ + selector: 'arc-gantt', + templateUrl: './gantt.component.html', + styleUrls: ['./gantt.component.scss'], +}) +export class GanttComponent { + // data for tooltip component + itemData: Item = { + allocatedHours: 1600, + billingRate: 100, + startDate: new Date('2024-01-01'), + endDate: new Date('2024-12-31'), + allotedDeals: [ + {name: 'Deal 1', allocatedHours: 800, status: 'approved'}, + {name: 'Deal 2', allocatedHours: 900, status: 'pending'}, + ], + }; + + allocationMap = new Map([ + ['Deal 1', true], + ['Deal 2', false], + ]); + + // Data for GanttColumnComponent + items: empData[] = [ + { + name: 'john Doe teena', + subtitle: 'Manager', + hasChildren: false, + isParent: false, + $open: false, + overallocated: false, + }, + { + name: 'kelly', + subtitle: 'Assistant Manager', + hasChildren: false, + isParent: false, + $open: false, + overallocated: false, + }, + { + name: 'Clove', + subtitle: 'Software Developer', + hasChildren: false, + isParent: false, + $open: false, + overallocated: false, + }, + { + name: 'Classy', + subtitle: 'DevOps', + hasChildren: false, + isParent: false, + $open: false, + overallocated: false, + }, + ]; + showParentInitials = true; + showChildInitials = true; + showOverallocatedIcon = true; + + allocationTypes = { + PlaceholderResource: 'PlaceholderResource', + }; + + allocationBase = 40; + + item: Item = { + type: 'ActualResource', + allocation: 32, + payload: {dealStage: 'closedwon', billingRate: 100}, + classes: ['example-class'], + subAllocations: [ + {percent: 50, allocation: 16, allocatedHours: 16, classes: ['class1']}, + {percent: 50, allocation: 16, allocatedHours: 16, classes: ['class2']}, + ], + }; + + // Data for GanttHeaderComponent + headerDesc = true; + headerName = 'Dynamic Project Gantt'; + headerSearchPlaceholder = 'Search your tasks'; + headerShowSearch = true; +}