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;
+}