diff --git a/projects/arc-lib/README.md b/projects/arc-lib/README.md index 966c495d..0c988788 100644 --- a/projects/arc-lib/README.md +++ b/projects/arc-lib/README.md @@ -2,18 +2,19 @@ ## Description -- In an Angular multiproject workspace, a shared library is a package that contains reusable code and +- In an Angular multiproject workspace, a shared library is a package that contains reusable code and functionality that can be used across multiple projects within the workspace. -- A shared library can include components, services, pipes, directives, and other modules that can be +- A shared library can include components, services, pipes, directives, and other modules that can be used by other projects in the workspace. By using a shared library, you can avoid duplicating code and functionality across multiple projects, which can save time and effort. -- By separating your reusable code into a shared library, you can also maintain consistency and +- By separating your reusable code into a shared library, you can also maintain consistency and standardization across your projects. If you need to make changes to the shared code, you can update the library and all projects that use it will be automatically updated as well. a shared library is a powerful tool for managing code and functionality in an Angular multiproject workspace, and can help improve code quality, reduce duplication, and increase productivity. -### Structure of the arc-lib is +### Structure of the arc-lib is The Structure of the arc-lib is as follows + ``` ├── arc-lib │ └── src @@ -24,7 +25,7 @@ The Structure of the arc-lib is as follows │ └── theme ``` -- Angular arc library provides various features and components to help developers get started with +- Angular arc library provides various features and components to help developers get started with their projects quickly. Here are some of the features and components that provides: ## Core Module: @@ -111,12 +112,13 @@ For more details about Theme Module,refer [Here](/projects/arc-lib/src/lib/theme ### Select Component - This component supports auto-completion, filtering of options by search terms, and the - ability to add new tags that are not present in the list of options. There are also several configurable options, such as the ability to select multiple items, the placeholder text for the input field, and the width and height of the dropdown panel and provides a reusable component for displaying a searchable list of items with a selectable checkbox and dropdown with customized states. +This component supports auto-completion, filtering of options by search terms, and the +ability to add new tags that are not present in the list of options. There are also several configurable options, such as the ability to select multiple items, the placeholder text for the input field, and the width and height of the dropdown panel and provides a reusable component for displaying a searchable list of items with a selectable checkbox and dropdown with customized states. ### Gantt Component: - In this The boilerplate usingbb.gantt,bb.gantt charts,bb.gantt bars a Gantt chart is a user interface component that displays project tasks or events over a timeline, allowing users to visualize the schedule and progress of a project. +In this The boilerplate usingbb.gantt,bb.gantt charts,bb.gantt bars a Gantt chart is a user interface component that displays project tasks or events over a timeline, allowing users to visualize the schedule and progress of a project. +or further reference you can refer [Here](/projects/arc-lib/src/lib/components/gantt/readme.md) ### Login Component: @@ -130,7 +132,7 @@ For more details about Theme Module,refer [Here](/projects/arc-lib/src/lib/theme - Auth component is a module that handles the authentication and authorization of users. It is responsible for managing user sessions, verifying user credentials, and granting access to protected resources based on the user's role and permissions. -- The auth component typically includes a login as well as a registration form for new users to create +- The auth component typically includes a login as well as a registration form for new users to create an account. The component may also handle password reset functionality and provide options for users to manage their accounts -For more details about Components ,refer [Here](/projects/arc-lib/src/lib/components/readme.md) \ No newline at end of file +For more details about Components ,refer [Here](/projects/arc-lib/src/lib/components/readme.md) diff --git a/projects/arc-lib/src/lib/components/gantt/components/bb-gantt.component.html b/projects/arc-lib/src/lib/components/gantt/components/bb-gantt.component.html deleted file mode 100644 index 4408045d..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/bb-gantt.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
-
- - -
- -
-
diff --git a/projects/arc-lib/src/lib/components/gantt/components/bb-gantt.component.scss b/projects/arc-lib/src/lib/components/gantt/components/bb-gantt.component.scss deleted file mode 100644 index ac35a2a9..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/bb-gantt.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -@use 'sass:map'; -@import 'projects/arc-lib/src/lib/theme/styles/_variables.scss'; -//@use "projects/arc-lib/src/lib/theme/styles/_variables.scss"; - -.gantt-container { - position: relative; - border-top: 0.063rem solid map.get($color, gantt-lines); - height: 100%; -} - -.gantt-menu { - background-color: map.get($color, light); - box-shadow: 0px 0.938rem 1.25rem 0px #00000029; - width: 11.75rem; -} diff --git a/projects/arc-lib/src/lib/components/gantt/components/bb-gantt.component.ts b/projects/arc-lib/src/lib/components/gantt/components/bb-gantt.component.ts deleted file mode 100644 index 84d44893..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/bb-gantt.component.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { - AfterViewInit, - Component, - ElementRef, - EventEmitter, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, - TemplateRef, - Type, - ViewChild, - ViewContainerRef, -} from '@angular/core'; -import {AnyObject} from '@project-lib/core/api'; -import {ComponentBaseDirective} from '@project-lib/core/component-base'; -import {NbMenuItem, NbMenuService} from '@nebular/theme'; -import {takeUntil} from 'rxjs'; -import {GANTT_COLUMN_WIDTH} from '../const'; -import {GanttService} from '../services'; -import { - ContextItemClickEvent, - ContextItemFilter, - GanttEvent, - GanttRenderOptions, - GanttTaskValue, - IBarComponent, - IColumnComponent, - Timelines, -} from '../types'; -import {GanttBarsComponent} from './gantt-bars/gantt-bars.component'; -import {GanttColumnComponent} from './gantt-column/gantt-column.component'; - -@Component({ - selector: 'gantt', - templateUrl: './bb-gantt.component.html', - styleUrls: ['./bb-gantt.component.scss'], -}) -export class BbGanttComponent - extends ComponentBaseDirective - implements OnChanges, OnInit, AfterViewInit, OnDestroy, GanttRenderOptions -{ - @ViewChild('gantt', {static: true}) ganttContainer!: ElementRef; - - @Input() - data!: T[]; - - @Input() - contextItems: NbMenuItem[] = []; - - @Input() - contextItemFilter!: ContextItemFilter; - - @Output() - contextItemClick = new EventEmitter>(); - - @Output() - event = new EventEmitter>(); - - @Input() - showParentInitials = false; - - @Input() - showChildInitials = false; - - @ViewChild('menu', {static: true}) - contextTemplate!: TemplateRef; - - @Input() - columnName = 'Employee Name'; - - @Input() - showKebab = true; - - @Input() - columnWidth = GANTT_COLUMN_WIDTH; - - @Input() - resizer = true; - - @Input() - columnComponent: Type> = GanttColumnComponent; - - @Input() - barComponent: Type> = GanttBarsComponent; - - @Input() - searchPlaceholder?: string; - - @Input() - showSearch = true; - - @Input() - showBorder = true; - - @Input() - moveToToday = true; - - @Input() - highlightRange!: [Date, Date]; - - @Input() - showOverallocatedIcon = true; - - @Input() - defaultScale: Timelines = Timelines.Monthly; - - @Input() - showGridBorder = true; - - @Input() - showTooltip = false; - - @Input() - markToday = true; - - @Input() - childIndent = true; - - constructor( - private readonly ganttSvc: GanttService, - private readonly menuService: NbMenuService, - public readonly viewContainerRef: ViewContainerRef, - ) { - super(); - } - - ngOnInit() { - this.menuService - .onItemClick() - .pipe(takeUntil(this._destroy$)) - .subscribe(event => { - this.contextItemClick.emit({ - event: event.item, - task: event.tag as unknown as GanttTaskValue, - }); - this.ganttSvc.closeContextMenu(); - }); - this.ganttSvc.events.pipe(takeUntil(this._destroy$)).subscribe(event => { - this.event.emit(event); - }); - } - - ngOnChanges() { - this.ganttSvc.feed(); - } - - ngAfterViewInit() { - this.ganttSvc.render(this.ganttContainer, this); - } - - override ngOnDestroy() { - this.ganttSvc.destroy(); - super.ngOnDestroy(); - } -} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars-routing.module.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars-routing.module.ts deleted file mode 100644 index 03520731..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars-routing.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; - -const routes: Routes = []; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class GanttBarsRoutingModule {} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.html b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.html index 90e2d17f..dbfdda0e 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.html +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.html @@ -16,6 +16,8 @@ class="bar actual-bar" [class.with-suballocations]="hasSubAllocation(item)" [class.closed-won]="item.payload?.['dealStage'] === 'closedwon'" + [attr.gantt-tooltip-data]="'abc'" + gantt-hover="tooltip" [ngClass]="item.classes ?? []" data-gantt-click="bar" > @@ -32,18 +34,26 @@ {{ formatter(item.payload?.['billingRate']) }}
- +
+
+ +
{{ formatAllocation( diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.scss b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.scss index 46f1769e..cba773cd 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.scss +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.scss @@ -1,6 +1,5 @@ @use 'sass:map'; -// @use "../../../../theme/styles/variables" as *; -@use 'projects/arc-lib/src/lib/theme/styles/_variables.scss' as *; +@use '../../../../theme/styles/variables' as *; .actual-rate { padding: 0.25rem 0.5rem; @@ -13,12 +12,22 @@ margin-right: 0.75rem; } +.bar { + position: relative; + .showTooltip-wrapper { + position: absolute; + top: 2rem; + } +} + .bar-container { display: flex; flex-grow: 1; background: white; } - +.showTooltip-wrapper { + position: absolute; +} .bar { font-family: var(--font-family-primary); font-weight: map.get($font-weight, bold); @@ -28,7 +37,7 @@ display: flex; justify-content: space-between; align-items: center; - height: 100%; + height: 20px; overflow: hidden; &.actual-bar { @@ -78,7 +87,7 @@ justify-content: space-between; align-items: center; height: 100%; - overflow: hidden; + background: repeating-linear-gradient( 305deg, diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.spec.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.spec.ts index 2800e0b9..e80f1f75 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.spec.ts +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.spec.ts @@ -1,34 +1,16 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; -import {AnyObject} from '@project-lib/core/api'; -import {TranslationService} from '@project-lib/core/localization'; -import { - TranslateFakeLoader, - TranslateLoader, - TranslateModule, - TranslateService, -} from '@ngx-translate/core'; + import {GanttBarsComponent} from './gantt-bars.component'; describe('GanttBarsComponent', () => { - let component: GanttBarsComponent; - let fixture: ComponentFixture>; + let component: GanttBarsComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [GanttBarsComponent], - providers: [TranslateService, TranslationService], - imports: [ - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateFakeLoader, - }, - }), - ], }).compileComponents(); - }); - beforeEach(() => { fixture = TestBed.createComponent(GanttBarsComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.ts index 135d8091..e885e84a 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.ts +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.component.ts @@ -1,42 +1,30 @@ -import {Component} from '@angular/core'; -import {AnyObject} from '@project-lib/core/api'; -import {MAX_ALLOCATION} from '@project-lib/core/constants'; -import {TranslationService} from '@project-lib/core/localization'; -import {TranslateService} from '@ngx-translate/core'; -import { - GanttTaskValue, - GanttTaskValueWithSubAllocation, - SubAllocation, -} from '../../types'; +import {Component, Input, OnChanges, SimpleChanges} from '@angular/core'; +import {AllocationBar, Item} from '../../model/item.model'; @Component({ - selector: 'gantt-bars', + selector: 'arc-gantt-bars', templateUrl: './gantt-bars.component.html', styleUrls: ['./gantt-bars.component.scss'], }) -export class GanttBarsComponent { - item!: GanttTaskValue; - allocationTypes: AnyObject; - allocationBase = MAX_ALLOCATION; - private translate: TranslateService; - constructor(private translateSvc: TranslationService) { - this.translate = translateSvc.translate; - } +export class GanttBarsComponent { + @Input() item: any; + @Input() allocationTypes: any; + @Input() allocationBase: number; + showTooltip = -1; - stringify(subAllocation: SubAllocation) { - return JSON.stringify(subAllocation); + formatAllocation(value: number): string { + return `${value} hours`; } - formatter(rate: number) { - return `$${rate}/${this.translate.instant('hr')}`; + + formatter(value: number): string { + return `$${value}/hour`; } - formatAllocation(allocation: number) { - return `${allocation}${this.translate.instant('h/d')}`; + hasSubAllocation(item: Item): boolean { + return item.subAllocations && item.subAllocations.length > 0; } - hasSubAllocation( - item: GanttTaskValue, - ): item is GanttTaskValueWithSubAllocation { - return !!(item as GanttTaskValueWithSubAllocation).subAllocations; + stringify(allocationBar: AllocationBar): string { + return JSON.stringify(allocationBar); } } diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.module.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.module.ts deleted file mode 100644 index bffe28d3..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-bars/gantt-bars.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; - -import {GanttBarsRoutingModule} from './gantt-bars-routing.module'; -import {GanttBarsComponent} from './gantt-bars.component'; -import {ThemeModule} from '@project-lib/theme/theme.module'; - -@NgModule({ - declarations: [GanttBarsComponent], - imports: [CommonModule, GanttBarsRoutingModule, ThemeModule], - exports: [GanttBarsComponent], -}) -export class GanttBarsModule {} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column-routing.module.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column-routing.module.ts deleted file mode 100644 index 9ae3f0ff..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column-routing.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; - -const routes: Routes = []; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class GanttColumnRoutingModule {} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.html b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.html index a6559da9..b80847b1 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.html +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.html @@ -1,4 +1,8 @@ -
+
{{ item.name }}
@@ -32,13 +36,4 @@
This resource is over allocated
-
- -
diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.scss b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.scss index 5a8b7f4d..04386efb 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.scss +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.scss @@ -1,7 +1,13 @@ -// @use "../../../../theme/styles/variables" as *; -@use 'projects/arc-lib/src/lib/theme/styles/_variables.scss' as *; +@use '../../../../theme/styles/variables' as *; @use 'sass:map'; -.column-container { + +.employee-list-item { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 10px; + border-bottom: 1px solid #d9d9d9; + padding-bottom: 10px; .kebab-container { position: absolute; background: linear-gradient( @@ -14,10 +20,12 @@ opacity: 0; transition: opacity 0.2s ease-in; height: 100%; + width: 70%; .kebab { padding: 0 2rem; } } + &:hover, &.active { .kebab-container { @@ -32,6 +40,7 @@ .employee-icon { min-width: 2.375rem; + max-width: 2.375rem; height: 2.375rem; border-radius: 50%; background: map.get($color, 'menu-item-active'); @@ -43,6 +52,7 @@ margin: auto; } } + .with-designation { overflow: hidden; flex-grow: 1; @@ -72,6 +82,7 @@ text-overflow: ellipsis; } } + .without-designation { overflow: hidden; flex-grow: 1; @@ -108,6 +119,7 @@ right: -4.063rem; top: -0.313rem; } + .overallocation_icon:hover div { display: flex; line-height: 1; diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.spec.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.spec.ts new file mode 100644 index 00000000..e55dcf7c --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.spec.ts @@ -0,0 +1,22 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {GanttColumnComponent} from './gantt-column.component'; + +describe('GColumnComponent', () => { + let component: GanttColumnComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GanttColumnComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(GanttColumnComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.ts index 83e11025..bb357289 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.ts +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.component.ts @@ -1,35 +1,29 @@ -import {Component, Input} from '@angular/core'; -import {AnyObject} from '@project-lib/core/api'; -import {NbMenuItem} from '@nebular/theme'; -import {ContextItemFilter, GanttTaskValue} from '../../types'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {empData} from '../../model/item.model'; @Component({ - selector: 'gantt-column', + selector: 'arc-gantt-column', templateUrl: './gantt-column.component.html', styleUrls: ['./gantt-column.component.scss'], }) -export class GanttColumnComponent { +export class GanttColumnComponent { @Input() - item!: GanttTaskValue; + items: empData[]; @Input() - contextItems: NbMenuItem[] = []; + showParentInitials: boolean; @Input() - active!: boolean; + showChildInitials: boolean; @Input() - showKebab!: boolean; + showOverallocatedIcon: boolean; + // for testing - @Input() - showParentInitials!: boolean; - - @Input() - showChildInitials!: boolean; + @Output() itemSelected = new EventEmitter(); - @Input() - showOverallocatedIcon!: boolean; - - @Input() - contextItemFilter!: ContextItemFilter; + onItemClick(item: empData): void { + this.itemSelected.emit(item); + console.log('hi tiny'); + } } diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.module.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.module.ts deleted file mode 100644 index bbac0283..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-column/gantt-column.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; - -import {GanttColumnRoutingModule} from './gantt-column-routing.module'; -import {GanttColumnComponent} from './gantt-column.component'; -import {ThemeModule} from '@project-lib/theme/theme.module'; - -@NgModule({ - declarations: [GanttColumnComponent], - imports: [CommonModule, GanttColumnRoutingModule, ThemeModule], - exports: [GanttColumnComponent], -}) -export class GanttColumnModule {} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header-routing.module.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header-routing.module.ts deleted file mode 100644 index f13d934c..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header-routing.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; - -const routes: Routes = []; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class GanttHeaderRoutingModule {} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.html b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.html index 6ee77585..4ed997e4 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.html +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.html @@ -1,22 +1,17 @@ -
- -
-
{{ name }}
-
- - +
+
+

{{ name }}

+
+

Description is enabled.

+
+ +
diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.scss b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.scss index 3e9aeb60..d1651093 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.scss +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.scss @@ -1,6 +1,7 @@ // @use "../../../../theme/styles/variables" as *; -@use 'projects/arc-lib/src/lib/theme/styles/_variables.scss' as *; +// @use 'projects/arc-lib/src/lib/theme/styles/_variables.scss' as *; @use 'sass:map'; +@use '../../../../theme/styles/variables' as *; .header-container { line-height: 1; width: 100%; @@ -27,6 +28,7 @@ bottom: 70.83%; } } + .title-bar { display: flex; align-items: center; @@ -50,3 +52,26 @@ color: map.get($color, icon); } } + +.header-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + width: calc(100vw - 1rem); +} + +.project-title { + font-size: x-large; + line-height: 1; +} + +.desc-wrapper { + margin-top: 6px; + p{ + margin:0; + } +} + +.project-title{ + margin:0; +} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.spec.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.spec.ts index 875fb124..5f3bff98 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.spec.ts +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.spec.ts @@ -1,6 +1,5 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; -import {ThemeModule} from '@project-lib/theme/theme.module'; -import {GanttModule} from '../../gantt.module'; + import {GanttHeaderComponent} from './gantt-header.component'; describe('GanttHeaderComponent', () => { @@ -9,11 +8,9 @@ describe('GanttHeaderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [GanttModule, ThemeModule.forRoot('boiler')], + declarations: [GanttHeaderComponent], }).compileComponents(); - }); - beforeEach(() => { fixture = TestBed.createComponent(GanttHeaderComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.ts index 85414a19..d7f11e5d 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.ts +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.component.ts @@ -1,19 +1,12 @@ import {Component, Input} from '@angular/core'; - @Component({ - selector: 'gantt-header', + selector: 'arc-gantt-header', templateUrl: './gantt-header.component.html', styleUrls: ['./gantt-header.component.scss'], }) export class GanttHeaderComponent { - @Input() - desc!: boolean; - - @Input() - name?: string; - + @Input() desc!: boolean; + @Input() name?: string; @Input() searchPlaceholder = 'Enter your search here'; - - @Input() - showSearch!: boolean; + @Input() showSearch!: boolean; } diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.module.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.module.ts deleted file mode 100644 index b3772d10..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-header/gantt-header.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; - -import {GanttHeaderRoutingModule} from './gantt-header-routing.module'; -import {GanttHeaderComponent} from './gantt-header.component'; -import {ThemeModule} from '@project-lib/theme/theme.module'; - -@NgModule({ - declarations: [GanttHeaderComponent], - imports: [CommonModule, GanttHeaderRoutingModule, ThemeModule], - exports: [GanttHeaderComponent], -}) -export class GanttHeaderModule {} 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 00000000..3b848b4d --- /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 00000000..6c3a03a8 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-scroll/gantt-scroll.component.scss @@ -0,0 +1,11 @@ +.gantt-scroll-icon { + height: 2rem; + width: 2rem; +} + +.icon-wrapper{ + display: flex; + justify-content: flex-start; + align-items: center; + margin-top: -24px; +} \ No newline at end of file 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 00000000..4292230a --- /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 00000000..e6e7e6dd --- /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-routing.module.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip-routing.module.ts deleted file mode 100644 index 7be6108f..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip-routing.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; - -const routes: Routes = []; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class GanttTooltipRoutingModule {} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.html b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.html index 49d1c36e..cc38b143 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.html +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.html @@ -1,41 +1,48 @@ -
-
-
Start Date
-
- {{ formatDate(item.startDate) | date: 'dd/MM/YYYY' }} + +
+
+
+
Hours Per Day
+
{{ formatAllocation(itemData.allocatedHours) }}
-
-
-
End Date
-
- {{ formatDate(item.endDate) | date: 'dd/MM/YYYY' }} +
+
Hourly Rate
+
{{ formatter(itemData.billingRate) }}
-
-
-
Hourly Rate Charged
-
{{ formatter(item.billingRate!) }}
-
-
-
Allocated Hours
-
{{ formatAllocation(item.allocatedHours) }}
-
-
-
-
{{ 'currentAllocationsLbl' | translate }}
-
-
- - - {{ formatAllocation(item.allocation - maxAllocation) }} - +
+
Start Date
+
+ {{ formatDate(itemData.startDate) | date: 'dd/MM/yyyy' }}
-
- {{ formatAllocation(item.allocation) }} +
+
+
End Date
+
+ {{ formatDate(itemData.endDate) | date: 'dd/MM/yyyy' }} +
+
+
+
+
+ {{ deal.name }} + + + + + + +
+
+
+ + {{ + displayStatus(deal.status) | titlecase + }} +
+
+ {{ formatAllocation(deal.allocatedHours) }} +
-
-
-
{{ deal.name }}
-
{{ formatAllocation(deal.allocatedHours) }}
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 a83b0278..dd7cb9cf 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 @@ -1,16 +1,21 @@ @use '../../../../theme/styles/variables' as *; @use 'sass:map'; +.tooltip-wrapper { + display: flex; + justify-content: center; +} .gantt-tooltip { background-color: map.get($color, dark); + color: white; box-shadow: 0 0.25rem 0.375rem map.get($color, tooltip-box-shadow); padding: 1.488rem; border-radius: 0.5rem; font-family: var(--font-family-primary); font-weight: map.get($font-weight, light); font-size: map.get($font-size, default); - width: 35rem; - max-height: 20rem; + width: 25rem; + max-height: 25rem; overflow-y: auto; } @@ -72,7 +77,7 @@ hr { .deal-name { text-decoration: underline; - width: 24rem !important; + width: 20rem !important; color: map.get($color, light) !important; font-weight: map.get($font-weight, bold); text-overflow: ellipsis; @@ -80,3 +85,25 @@ hr { white-space: nowrap; padding-left: 1rem; } +.deal-key { + display: flex; + gap: 15px; + justify-content: end; +} +.status-circle { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 10px; + + &.active { + background-color: green; + } + &.pending { + background-color: rgb(255, 153, 0); + } + &.unapproved { + background-color: red; + } +} diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.spec.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.spec.ts index 848f4190..16755f37 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.spec.ts +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.spec.ts @@ -1,11 +1,4 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; -import {TranslationService} from '@project-lib/core/localization'; -import { - TranslateFakeLoader, - TranslateLoader, - TranslateModule, - TranslateService, -} from '@ngx-translate/core'; import {GanttTooltipComponent} from './gantt-tooltip.component'; @@ -16,19 +9,8 @@ describe('GanttTooltipComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [GanttTooltipComponent], - providers: [TranslateService, TranslationService], - imports: [ - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateFakeLoader, - }, - }), - ], }).compileComponents(); - }); - beforeEach(() => { fixture = TestBed.createComponent(GanttTooltipComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.ts index 2cceff2c..339b875e 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.ts +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.component.ts @@ -1,28 +1,39 @@ -import {Component} from '@angular/core'; -import {MAX_ALLOCATION} from '@project-lib/core/constants'; +import {Component, Input} from '@angular/core'; import {TranslateService} from '@ngx-translate/core'; -import {SubAllocation} from '../../types'; +import {Item} from '../../model/item.model'; @Component({ - selector: 'gantt-tooltip', + selector: 'arc-gantt-tooltip', templateUrl: './gantt-tooltip.component.html', styleUrls: ['./gantt-tooltip.component.scss'], }) export class GanttTooltipComponent { - item!: SubAllocation; - maxAllocation = MAX_ALLOCATION; + @Input() + itemData: Item; + + @Input() + allocationMap = new Map([]); constructor(private translate: TranslateService) {} - formatDate(date: Date) { - return new Date(date); + formatDate(date: Date): string { + const options: Intl.DateTimeFormatOptions = { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }; + return new Intl.DateTimeFormat('en-GB', options).format(date); + } + + formatter(value: number): string { + return `$${value}`; } - formatter(rate: number) { - return `$${rate}/${this.translate.instant('hr')}`; + formatAllocation(hours: number): string { + return `${hours} hours`; } - formatAllocation(allocation: number) { - return `${allocation}${this.translate.instant('h/d')}`; + displayStatus(status: string): string { + return status; } } diff --git a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.module.ts b/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.module.ts deleted file mode 100644 index de659980..00000000 --- a/projects/arc-lib/src/lib/components/gantt/components/gantt-tooltip/gantt-tooltip.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; - -import {GanttTooltipRoutingModule} from './gantt-tooltip-routing.module'; -import {GanttTooltipComponent} from './gantt-tooltip.component'; -import {TranslateModule} from '@ngx-translate/core'; - -@NgModule({ - declarations: [GanttTooltipComponent], - imports: [CommonModule, GanttTooltipRoutingModule, TranslateModule], - exports: [GanttTooltipComponent], -}) -export class GanttTooltipModule {} 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 00000000..7ba05b8f --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component.html @@ -0,0 +1,17 @@ +
+ + + + + +
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 00000000..c4410aeb --- /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 00000000..84dcb3d2 --- /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 00000000..3b86896f --- /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 {TimelineArray} 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; + console.log(TimelineArray); + } + + zoomIn() { + this.ganttService.zoomIn(); + } + + zoomOut() { + this.ganttService.zoomOut(); + } + + fitToScreen() { + this.ganttService.fitToScreen(); + } +} diff --git a/projects/arc-lib/src/lib/components/gantt/components/index.ts b/projects/arc-lib/src/lib/components/gantt/components/index.ts index c5ebfd07..6df38daf 100644 --- a/projects/arc-lib/src/lib/components/gantt/components/index.ts +++ b/projects/arc-lib/src/lib/components/gantt/components/index.ts @@ -1,4 +1,6 @@ -export * from './bb-gantt.component'; export * from './gantt-bars/gantt-bars.component'; export * from './gantt-column/gantt-column.component'; export * from './gantt-header/gantt-header.component'; +export * from './gantt-tooltip/gantt-tooltip.component'; +export * from './gantt-scroll/gantt-scroll.component'; +export * from './gantt-zoombar/gantt-zoombar.component'; diff --git a/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.html b/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.html new file mode 100644 index 00000000..23488d48 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.html @@ -0,0 +1 @@ +
diff --git a/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.scss b/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.scss new file mode 100644 index 00000000..f6309975 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.scss @@ -0,0 +1,49 @@ +/* gantt.component.scss */ + +/* Ensure the Gantt chart container takes the full width and height */ +#gantt_here { + width: 100%; + height: 600px; + background-color: #f5f5f5; /* Light grey background for the Gantt container */ + border: 1px solid #ddd; /* Light border to distinguish the container */ + } + + /* Customize Gantt chart grid */ + .gantt_grid_scale, .gantt_grid_head_cell, .gantt_grid_data .gantt_cell { + background-color: #e6e6e6; /* Light grey background for grid header and cells */ + border-color: #ccc; /* Light grey border for grid cells */ + } + + /* Customize Gantt chart tasks */ + .gantt_task_line { + background-color: #4CAF50; /* Green background for tasks */ + border-color: #4CAF50; /* Green border for tasks */ + } + + .gantt_task_progress { + background-color: #81C784; /* Lighter green for task progress */ + } + + /* Customize Gantt chart timeline scale */ + .gantt_scale_line { + background-color: #e6e6e6; /* Light grey background for timeline scale */ + border-color: #ccc; /* Light grey border for timeline scale */ + } + + /* Customize Gantt chart subscale */ + .gantt_task_scale { + background-color: #f0f0f0; /* Slightly lighter grey for task scale */ + border-color: #ddd; /* Light grey border for task scale */ + } + + /* Add custom styling for weekends */ + .gantt_task_cell.week_end { + background-color: #f2dede !important; /* Light red background for weekends */ + } + + /* Add custom styling for today marker */ + .gantt_today { + background-color: #ffeb3b !important; /* Yellow background for today marker */ + opacity: 0.3; /* Semi-transparent */ + } + \ No newline at end of file diff --git a/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.spec.ts b/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.spec.ts new file mode 100644 index 00000000..d0dcccb5 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.spec.ts @@ -0,0 +1,22 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {TimelineComponent} from './timeline.component'; + +describe('TimelineComponent', () => { + let component: TimelineComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TimelineComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TimelineComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.ts b/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.ts new file mode 100644 index 00000000..5d1640f5 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/components/timeline/timeline.component.ts @@ -0,0 +1,125 @@ +import { + AfterViewInit, + Component, + ElementRef, + OnDestroy, + OnInit, + TemplateRef, + Type, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import {NbMenuItem} from '@nebular/theme'; +import { + GanttProviders, + GanttAdapter, + CustomGanttAdapter, + GanttService, + GanttRenderOptions, + ContextItemFilter, + GanttRowConfig, + GanttTaskValue, + IBarComponent, + IColumnComponent, + KebabListItem, + Timelines, + MONTHS_IN_QUARTER, +} from '@project-lib/components/gantt'; +import {GanttScaleUnits} from '@project-lib/components/gantt/enum'; +import {Item, empData} from '@project-lib/components/gantt/model/item.model'; +import {AnyObject} from '@project-lib/core/api'; +import {BehaviorSubject, takeUntil} from 'rxjs'; +declare let gantt: any; +@Component({ + selector: 'arc-timeline-gantt', + templateUrl: './timeline.component.html', + styleUrls: ['./timeline.component.scss'], +}) +export class TimelineComponent + implements AfterViewInit, GanttRenderOptions +{ + private _data: BehaviorSubject = new BehaviorSubject([]); + @ViewChild('gantt', {static: true}) ganttContainer!: ElementRef; + // data for tooltip component + showTooltip = false; + selectedItem: Item; + infiniteScroll = false; + constructor( + private readonly ganttSvc: GanttService, + public readonly viewContainerRef: ViewContainerRef, + ) {} + showParentInitials: boolean; + showChildInitials: boolean; + showOverallocatedIcon: boolean; + ngAfterViewInit(): void { + this.initializeGantt(); + } + + columnComponent: Type>; + barComponent: Type>; + contextItems: NbMenuItem[]; + contextTemplate?: TemplateRef; + columnName?: string; + showKebab?: boolean; + columnWidth: number; + resizer: boolean; + sorting: boolean; + moveToToday: boolean; + highlightRange?: [Date, Date]; + showNonBillableIcon: boolean; + contextItemFilter?: ContextItemFilter; + defaultScale: Timelines; + markToday: boolean; + showBillingRate?: boolean; + groupings?: string[]; + childIndent: boolean; + tooltipOffset?: number; + batchSize?: number; + searchPlaceholder?: string; + showSearch: boolean; + ganttStartDate?: Date; + kebabOption: (task: GanttTaskValue) => KebabListItem[]; + ganttRowConfig: GanttRowConfig; + private _formatWeeklyScale(date: Date) { + const noOfDigits = 2; + return `${date.toLocaleString('default', {month: 'short'})} ${date + .getDate() + .toString() + .padStart(noOfDigits, '0')}, ${date.toLocaleString('default', { + year: 'numeric', + })}`; + } + private _formatQuarterScale(date: Date) { + const month = date.getMonth(); + const year = date.getFullYear(); + return `Q${Math.ceil((month + 1) / MONTHS_IN_QUARTER)} ` + year; + } + private initializeGantt(): void { + gantt.config.scale_unit = 'year'; + gantt.config.date_scale = '%Y'; + gantt.config.subscales = [ + { + unit: GanttScaleUnits.Quarter, + step: 1, + format: (date: Date) => this._formatQuarterScale(date), + }, + { + unit: GanttScaleUnits.Week, + step: 1, + format: (date: Date) => this._formatWeeklyScale(date), + }, + ]; + gantt.config.start_date = new Date(); + gantt.config.end_date = new Date( + gantt.config.start_date.getFullYear() + 1, + 0, + 1, + ); + + gantt.config.columns = []; + gantt.init(this.ganttContainer.nativeElement); + gantt.parse({ + data: [{}], + }); + } +} diff --git a/projects/arc-lib/src/lib/components/gantt/const.ts b/projects/arc-lib/src/lib/components/gantt/const.ts index ef0dd91f..4198e551 100644 --- a/projects/arc-lib/src/lib/components/gantt/const.ts +++ b/projects/arc-lib/src/lib/components/gantt/const.ts @@ -9,13 +9,7 @@ export const GANTT_SCALES = new InjectionToken( 'gantt.scales', ); -export const GanttProviders = [ - GanttService, - { - provide: GANTT, - useFactory: () => Gantt.getGanttInstance(), - }, -]; +export const GanttProviders = [GanttService]; export function isHTMLELement(element: EventTarget): element is HTMLElement { return !!(element as HTMLElement).closest; @@ -29,6 +23,11 @@ export const GANTT_SCROLL_BAR_HEIGHT = 20; export const GANTT_COLUMN_WIDTH = 300; export const RESIZER_WIDTH = 1; export const BUFFER_FOR_TODAY = 5; - +export const TOOLTIP_OFFSET = 30; export const MONTHS_IN_QUARTER = 3; export const RANDOM_SIZE = 36; +export const ROW_HEIGHT = 35; +export const BUFFER_FOR_EACH_ROW = 35; +export const ACTUAL_ROW_SIZE = 24; +export const COLUMN_WIDTH = 35; +export const PARENT_ROW_HEIGHT_HEADINGS = 35; diff --git a/projects/arc-lib/src/lib/components/gantt/enum.ts b/projects/arc-lib/src/lib/components/gantt/enum.ts index 43c7383d..a9285bf2 100644 --- a/projects/arc-lib/src/lib/components/gantt/enum.ts +++ b/projects/arc-lib/src/lib/components/gantt/enum.ts @@ -1,11 +1,14 @@ -export enum GanttEventTypes { +export enum GanttEventValues { Kebab = 'kebab', Expand = 'expand', Name = 'name', Sort = 'sort', + SubAllocation = 'sub-allocation', Bar = 'bar', Tooltip = 'tooltip', Unknown = 'unknown', + OpenedTooltip = 'opened-tooltip', + ExpandBar = 'expand-bar', } export enum GanttScaleUnits { @@ -15,3 +18,8 @@ export enum GanttScaleUnits { Year = 'year', Quarter = 'quarter', } + +export enum GanttEventTypes { + Click = 'click', + Hover = 'hover', +} diff --git a/projects/arc-lib/src/lib/components/gantt/gantt-routing.module.ts b/projects/arc-lib/src/lib/components/gantt/gantt-routing.module.ts index be744e5e..0d17e19a 100644 --- a/projects/arc-lib/src/lib/components/gantt/gantt-routing.module.ts +++ b/projects/arc-lib/src/lib/components/gantt/gantt-routing.module.ts @@ -1,43 +1,7 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; -import {BbGanttComponent} from './components'; -const routes: Routes = [ - { - path: '', - component: BbGanttComponent, - children: [ - { - path: 'gantt-bars', - loadChildren: () => - import('./components/gantt-bars/gantt-bars.module').then( - m => m.GanttBarsModule, - ), - }, - { - path: 'gantt-columns', - loadChildren: () => - import('./components/gantt-column/gantt-column.module').then( - m => m.GanttColumnModule, - ), - }, - { - path: 'gantt-header', - loadChildren: () => - import('./components/gantt-header/gantt-header.module').then( - m => m.GanttHeaderModule, - ), - }, - { - path: 'gantt-tootltip', - loadChildren: () => - import('./components/gantt-tooltip/gantt-tooltip.module').then( - m => m.GanttTooltipModule, - ), - }, - ], - }, -]; +const routes: Routes = []; @NgModule({ imports: [RouterModule.forChild(routes)], 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 468fdb44..850ad720 100644 --- a/projects/arc-lib/src/lib/components/gantt/gantt.module.ts +++ b/projects/arc-lib/src/lib/components/gantt/gantt.module.ts @@ -2,25 +2,52 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {ReactiveFormsModule} from '@angular/forms'; import {ThemeModule} from '@project-lib/theme/theme.module'; -import {BbGanttComponent} from './components'; -import {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 {GanttBarsModule} from './components/gantt-bars/gantt-bars.module'; +import {gantt} from 'dhtmlx-gantt'; +import {CustomGanttAdapter, GanttAdapter} from './types'; + +import { + GanttBarsComponent, + GanttColumnComponent, + GanttHeaderComponent, + GanttTooltipComponent, +} from './components'; +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'; +import {TimelineComponent} from './components/timeline/timeline.component'; @NgModule({ - declarations: [BbGanttComponent], - imports: [ - CommonModule, - ReactiveFormsModule, - ThemeModule, - GanttRoutingModule, - GanttBarsModule, + declarations: [ + GanttBarsComponent, + GanttColumnComponent, + GanttHeaderComponent, + GanttTooltipComponent, + GanttZoomBarComponent, + GanttScrollComponent, + TimelineComponent, + ], + imports: [CommonModule, ReactiveFormsModule, ThemeModule, GanttRoutingModule], + exports: [ + GanttBarsComponent, + GanttColumnComponent, + GanttHeaderComponent, + GanttTooltipComponent, + GanttZoomBarComponent, + GanttScrollComponent, + TimelineComponent, ], - exports: [BbGanttComponent], providers: [ + GanttProviders, + DateOperationService, + { + provide: GANTT, + useValue: gantt, + }, { provide: GANTT_SCALES, multi: true, @@ -36,6 +63,10 @@ import {GanttBarsModule} from './components/gantt-bars/gantt-bars.module'; multi: true, useClass: QuarterlyScaleService, }, + { + provide: GanttAdapter, + useClass: CustomGanttAdapter, + }, ], }) export class GanttModule {} diff --git a/projects/arc-lib/src/lib/components/gantt/model/item.model.ts b/projects/arc-lib/src/lib/components/gantt/model/item.model.ts new file mode 100644 index 00000000..03c17d99 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/model/item.model.ts @@ -0,0 +1,81 @@ +import {TemplateRef, ViewContainerRef} from '@angular/core'; +import {NbMenuItem} from '@nebular/theme'; +import {AnyObject} from '@project-lib/core/api/backend-filter'; +import {Type} from 'typescript'; +import { + IColumnComponent, + IBarComponent, + ContextItemFilter, + Timelines, + GanttTaskValue, + KebabListItem, + GanttRowConfig, +} from '../types'; + +export interface Deal { + name: string; + allocatedHours: number; + status: string; +} + +export interface Item { + allocatedHours?: number; + billingRate?: number; + startDate?: Date; + endDate?: Date; + allotedDeals?: Deal[]; + type?: string | number; + allocation?: number; + payload?: {[key: string]: any}; + classes?: string[]; + subAllocations?: AllocationBar[]; +} + +export interface empData { + name: string; + subtitle: string; + hasChildren: boolean; + isParent: boolean; + $open: boolean; + overallocated: boolean; +} + +export interface AllocationBar { + percent: number; + allocation: number; + allocatedHours: number; + classes?: string[]; +} + +export type GanttOptions = { + contextItems: NbMenuItem[]; + contextTemplate?: TemplateRef; + // toolTip?: Type>; + viewContainerRef?: ViewContainerRef; + columnName?: string; + showKebab?: boolean; + showParentInitials: boolean; + showChildInitials: boolean; + // columnComponent: Type>; + // barComponent: Type>; + columnWidth: number; + resizer: 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; + batchSize?: number; + searchPlaceholder?: string; + showSearch: boolean; + ganttStartDate?: Date; + kebabOption: (task: GanttTaskValue) => KebabListItem[]; + ganttRowConfig: GanttRowConfig; +}; diff --git a/projects/arc-lib/src/lib/components/gantt/readme.md b/projects/arc-lib/src/lib/components/gantt/readme.md new file mode 100644 index 00000000..93e93a63 --- /dev/null +++ b/projects/arc-lib/src/lib/components/gantt/readme.md @@ -0,0 +1,222 @@ +# Gantt Module + +## Overview + +The Gantt Module is designed to display project timelines, resource allocation, and scheduling in a visually intuitive way. This module includes several components, each handling a specific aspect of the Gantt chart visualization. + +## GanttBarComponent + +- The component is used to render the bars for the Gantt chart. It is generic and can be used with any type of task value. The component contains properties and methods for rendering the bars. +- GanttBarsComponent is responsible for visualizing resource allocations in a Gantt chart format. It supports: + + 1. Actual allocations with sub-allocations + 2. Hour and rate displays + 3. Tooltips for detailed information + 4. Over-allocation indicators. + +### Usage Example + +``` +// Component usage + + + +``` + +## GanttColumnComponent + +This component handles hierarchical employee data display with expandable parent-child relationships, initials display, and overallocation warnings. + +- This Component is used to displays a single column of the Gantt chart. It has several input properties + like +- `contextItems`: An array of NbMenuItem objects representing the context menu items to display for the + task. +- `showKebab`: A boolean value indicating whether to show a kebab icon for the task or not. +- `showParentInitials`: A boolean value indicating whether to show the initials of the parent task or not. +- `showChildInitials`: A boolean value indicating whether to show the initials of the child tasks or not. +- `showOverallocatedIcon`: A boolean value indicating whether to show an overallocated icon for the task or + not. +- `contextItemFilter`: A function that takes a GanttTaskValue object as input and returns a boolean + indicating whether to display the context menu items for that task or not. + +### Usage Example + +``` +// Component usage + + + +``` + +## GanttHeaderComponent + +- This component provides a customizable header with an optional project title, description indicator, and search functionality. +- The GanttHeaderComponent serves as the top section of a Gantt chart, featuring: + + 1. Project title display + 2. Optional description indicator + 3. Configurable search input field + +### Usage Example + +``` +// Component usage + + + +``` + +## GanttTooltipComponent + +- The GanttTooltipComponent provides a detailed popup + tooltip displaying: + + 1. Hours per day allocation + 2. Hourly billing rates + 3. Project date ranges + 4. Associated deals with status + +### Usage Example + +``` +// Component usage + + + +``` + +## TimelineComponent + +- The timeline component that implements a customizable Gantt chart timeline with multiple scale units, tooltips, and advanced configuration options. This component integrates with the DHTMLX Gantt library and provides extensive customization capabilities. +- The TimelineComponent is a generic component that provides: + + 1. Multi-level time scale visualization + 2. Customizable columns and bars + 3. Context menu integration + 4. Infinite scrolling support + 5. Custom tooltip system + 6. Advanced grouping and filtering + +### Usage Example + +``` +// Component usage + +@Component({ + selector: 'app-custom-gantt', + template: ` + + + ` +}) +``` + +## GanttZoomBarComponent + +- This Component provides zoom and fit-to-screen controls for a Gantt chart interface. This component offers intuitive controls for adjusting the Gantt chart's view scale and fit. +- The GanttZoomBarComponent is a control panel + component that provides: + 1. Zoom in/out functionality + 2. Fit to screen capability + +### Usage Example + +``` +// Component usage + + +``` + +## Service: + +### GanttService + +- The GanttService class has several private properties, including ` _data`, `_overlays`, `_tooltipOverlay`, + `_eventHandlers` etc +- The render method takes an ElementRef representing the container containing options to configure the Gantt + chart. The method sets several properties such as the `row_height, bar_height, scale_height`, and readonly properties. It also sets up the layout of the Gantt chart and registers event handlers for task clicks and + grid header clicks. + +### Timeline-scale + +- `MonthlyScaleService` : This is an service for generating a monthly scale for a Gantt chart It provides a configuration for displaying the Monthly timeline scale for a Gantt chart. The `config()` method, which returns an array of objects representing the different units in the timeline scale.The `GanttScaleService` interface defines a contract for a service that can be used to generate scale configurations for different timelines. The `MonthlyScaleService` class implements this interface by defining the scale property, which is set to Timelines.Monthly + +- `QuarterlyScaleService` : This is an service for generating a quarterly scale for a Gantt chart. The service implements the `GanttScaleService` interface and provides a `config()` method that returns an array of scale configuration objects.The `QuarterlyScaleService` class has a scale property that is set to Timelines.Quarterly, indicating that this service generates a quarterly scale. + +- `WeeklyScaleService` : This defines service called `WeeklyScaleService` which implements the `GanttScaleService` interface. The `GanttScaleService` interface defines the contract that all Gantt scale services must adhere to. The `WeeklyScaleService` service provides a configuration for a Gantt chart with a weekly timeline. The configuration includes an array of objects that specify the scale units, step size, and formatting for each unit.The `config()` method returns an array of two objects. The first object represents the weekly scale unit and formats the date using the `_formatWeeklyScale` private method. The second object represents the daily scale unit and formats the date using the `toLocaleString()` + +## GanttDemoComponent + +- This component demonstrating a full-featured Gantt chart implementation with sidebars, headers, tooltips, and timeline visualization. +- The GanttDemoComponent showcases various features of a Gantt chart implementation including: + + 1. Resource allocation visualization + 2. Timeline management + 3. Project hierarchy display + 4. Custom tooltips + 5. Search functionality + 6. Header customization + +### Resource Allocation + +``` +allocationTypes = { + PlaceholderResource: 'PlaceholderResource' +}; + +allocationBase = 40; + +item = { + type: 'ActualResource', + allocation: 32, + payload: { + dealStage: 'closedown', + billingRate: 100 + }, + subAllocations: [ + {percent: 50, allocation: 20, allocatedHours: 20, classes: ['class1']}, + {percent: 50, allocation: 15, allocatedHours: 15, classes: ['class2']} + ] +}; +``` + +### Sample Data + +``` +items: empData[] = [ + { + name: 'John Doe', + subtitle: 'Manager', + hasChildren: false, + isParent: false, + $open: false, + overallocated: false + }, + // ... additional employees +]; +``` + +### Usage + +1. Import the required modules and configure them in your AppModule. +2. Set up data models to define resources, tasks, and allocations. +3. Configure the GanttDemoComponent route `/gantt-demo` for easy access. 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 00000000..3e99d73b --- /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 f4ffab94..36d0c465 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 a2a38c65..dcb81e43 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,63 @@ 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 {GanttEventTypes, GanttEventValues} 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'; -@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 = 0; + on; + 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 +104,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. @@ -78,48 +119,81 @@ export class GanttService { * @param {GanttRenderOptions} options - GanttRenderOptions */ render(container: ElementRef, options: GanttRenderOptions) { - this._setColumnHeaders(options); - 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._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 = 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 +211,67 @@ 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.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.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); - }); + 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; + console.log(this.gantt); + // if (options.infiniteScroll) { + // this._setupPagination(); + // this._reset$.next(0); + // } this._renderTodayMarker(); this.setScale(options.defaultScale, false); } @@ -197,9 +281,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 +312,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 +343,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 +364,7 @@ export class GanttService { } } - private _refresh() { + refresh() { this.gantt.clearAll(); this.gantt.parse({ tasks: this._data ?? [], @@ -256,9 +372,25 @@ 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() {} + private _renderHighlighMarker() { if (this._highlightRange) { this.gantt.addMarker?.({ @@ -269,17 +401,9 @@ 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) { + console.log(this._options); + if (this._options?.markToday) { this.gantt.addMarker?.({ start_date: new Date(), css: 'today', @@ -294,57 +418,104 @@ export class GanttService { label: this._renderComponent(GanttHeaderComponent, { desc: this._descSort, name: options.columnName, - searchPlaceholder: options.searchPlaceholder, - showSearch: options.showSearch, }), 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 => 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 _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, + options?: GanttRenderOptions, ) { - if (event.target && isHTMLELement(event.target)) { - const target = event.target.closest('[data-gantt-click]'); + 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('data-gantt-click'); - const task = this.gantt.getTask(id); + const attribute = target.getAttribute('gantt-click'); switch (attribute) { - case GanttEventTypes.Kebab: - this._handleKebabClick(id, event, options); + 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 GanttEventTypes.Expand: + case GanttEventValues.Expand: { + const task = this.gantt.getTask(id); if (!task.$open) { this.gantt.open(id); } else { this.gantt.close(id); } break; - case GanttEventTypes.Sort: + } + 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({ + this._events$.next({ task: this.gantt.getTask(id), - event: attribute ?? GanttEventTypes.Unknown, + event: attribute ?? GanttEventValues.Unknown, }); } } @@ -370,7 +541,10 @@ export class GanttService { id: number, e: MouseEvent, options: GanttRenderOptions, + target: Element, + contextItems?: KebabListItem[], ) { + this._markClicked(target); const positionStrategy = this.overlay .position() .global() @@ -381,22 +555,32 @@ 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); } @@ -406,75 +590,338 @@ export class GanttService { 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 GanttEventTypes.Bar: - this._handleHoverOnBar(target, 'gantt-bar-data'); - return; - case GanttEventTypes.Tooltip: - return; - } - } - } - if (this._tooltipOverlay) { - this._tooltipOverlay.dispose(); + // 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 _handleHoverOnBar(target: Element, tag: string) { + private _openTooltip( + target: Element, + event: GanttEventTypes, + options: GanttRenderOptions, + e: MouseEvent, + ) { + const component = target.getAttribute('gantt-tooltip-component'); + 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 2cd0d0ef..cf4ff024 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 c39eddbf..fd9735ce 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 efba020d..747b8e34 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 00000000..9023b04d --- /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 f4dc3726..39422be9 100644 --- a/projects/arc-lib/src/lib/components/gantt/types.ts +++ b/projects/arc-lib/src/lib/components/gantt/types.ts @@ -1,9 +1,16 @@ import {TemplateRef, Type, ViewContainerRef} from '@angular/core'; import {AnyObject} from '@project-lib/core/api'; -import {DIGITS, ONE_MIN} from '@project-lib/core/constants'; +import { + DIGITS, + MAX_ALLOCATION, + ONE_DAY, + ONE_MIN, + PERCENT, +} 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 +74,7 @@ export interface GanttTaskValueWithSubAllocation extends BaseTaskValue { subAllocations: SubAllocation[]; } -export type GanttScaleService = { +export type GanttScaleService = { scale: Timelines; config(options?: GanttScaleOptions): { unit: string; @@ -75,6 +82,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,27 +102,38 @@ export type GanttEvent = { export type GanttRenderOptions = { contextItems: NbMenuItem[]; contextTemplate?: TemplateRef; + // toolTip?: Type>; viewContainerRef?: ViewContainerRef; columnName?: string; - showKebab: boolean; + showKebab?: boolean; showParentInitials: boolean; showChildInitials: boolean; columnComponent: Type>; 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 +147,7 @@ export enum Timelines { Monthly, Quarterly, Custom, + Yearly, } export const GanttTimelineMap: { @@ -136,6 +157,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[]; @@ -173,13 +195,243 @@ export abstract class GanttAdapter { export class CustomGanttAdapter extends GanttAdapter { // Implement the abstract method adaptFrom - adaptFrom(): GanttTaskValue[] { - // Your implementation logic here to adapt data to GanttTaskValue - // ... + // adaptFrom(): GanttTaskValue[] { + // // Your implementation logic here to adapt data to GanttTaskValue + // // ... + + // return; + // } + adaptFrom(data: any[]): GanttTaskValue[] { + const result: GanttTaskValue[] = []; + data.forEach(sow => { + let startDate: Date | undefined; + let endDate: Date | undefined; + const resourceWiseMap = new Map< + string, + GanttTaskValueWithSubAllocation[] + >(); + for (let allocation of sow.allocations ?? []) { + allocation.startDate = this._addTimezoneOffset(allocation.startDate); + allocation.endDate = this._nextDay( + this._addTimezoneOffset(allocation.endDate), + ); + if (!startDate || allocation.startDate < startDate) { + startDate = allocation.startDate; + } + if (!endDate || allocation.endDate > endDate) { + endDate = allocation.endDate; + } + const task: GanttTaskValueWithSubAllocation = { + start_date: allocation.startDate, + end_date: allocation.endDate, + name: allocation.name, + subtitle: allocation.roleName, + type: allocation.type, + payload: {...allocation, billingType: sow?.billingType}, + id: allocation.id ?? '', + hasChildren: false, + isParent: false, + subAllocations: [], + }; + this._checkForShadowAllocation(task, allocation); + if (allocation.allocationPerDay) { + const {overallocated, allocations, min, max} = + this._findOverAllocationRanges(allocation, sow?.currency?.symbol); + task.subAllocations = allocations; + task.overallocated = overallocated; + task.start_date = new Date(min); + task.end_date = new Date(max); + } + resourceWiseMap.set( + allocation.resourceId, + (resourceWiseMap.get(allocation.resourceId) ?? []).concat(task), + ); + } + const task: GanttTaskValue = { + start_date: startDate ?? sow.startDate ?? new Date(), + end_date: endDate ?? sow.endDate ?? new Date(), + name: sow.name, + allocation: 0, + type: 'SOW', + payload: sow, + id: sow.id!, + hasChildren: true, + isParent: true, + $open: true, + }; - return; + // add mock allocation for staggered subtask + // this._pushDummyParentForStaggeredAllocations( + // resourceWiseMap, + // result, + // sow.id!, + // ); + + // no row if no allocations for a project + if (sow.allocations?.length) { + result.push(task); + } + }); + return result; } + private _checkForShadowAllocation( + task: GanttTaskValueWithSubAllocation, + allocation: any, + ) {} + + private _findOverAllocationRanges(item: any, currency?: string) { + let allocations: SubAllocation[] = []; + let overallocated = false; + let min = 0; + let max = 0; + if (!item.allocationPerDay?.length) { + return { + overallocated, + allocations: [ + { + percent: PERCENT, + allocation: item.allocation, + }, + ], + min: new Date(item.startDate).getTime(), + max: new Date(item.endDate).getTime(), + }; + } + + let suballocations: any[] = item.allocationPerDay; + suballocations.sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + min = new Date(suballocations[0].date).getTime(); + max = + new Date(suballocations[suballocations.length - 1].date).getTime() + + ONE_DAY; // one day added because gantt is drawn upto 0:00 of the next day + const duration = (max - min) / ONE_DAY; + let lastInstance: SubAllocation | undefined; + enum STATE { + OVERALLOCATED, + NONOVERALLOCATION, + } + let state: STATE = STATE.NONOVERALLOCATION; + for (let suballocation of suballocations) { + let {allocatedHours, allotedDeals} = this._getAllotedDeals( + suballocation, + item.dealId, + ); + overallocated = + overallocated || this._checkIfOverallocated(suballocation); // if any day is overallocated, then the whole allocation is overallocated + switch (state) { + case STATE.OVERALLOCATED: + if (suballocation.totalAllocation <= MAX_ALLOCATION) { + state = STATE.NONOVERALLOCATION; + lastInstance = this._createLastInstance( + duration, + suballocation.totalAllocation, + suballocation.date, + item.billingRate, + allocatedHours, + allotedDeals, + currency, + ); + allocations.push(lastInstance); + } else { + this._updateLastInstance(duration, lastInstance); + } + break; + case STATE.NONOVERALLOCATION: + if (suballocation.totalAllocation > MAX_ALLOCATION) { + state = STATE.OVERALLOCATED; + lastInstance = this._createLastInstance( + duration, + suballocation.totalAllocation, + suballocation.date, + item.billingRate, + allocatedHours, + allotedDeals, + currency, + ); + allocations.push(lastInstance); + // update and check in one go + } else if (!this._updateLastInstance(duration, lastInstance)) { + lastInstance = this._createLastInstance( + duration, + suballocation.totalAllocation, + suballocation.date, + item.billingRate, + allocatedHours, + allotedDeals, + currency, + ); + allocations.push(lastInstance); + } else { + // do nothing + } + break; + } + } + + return { + allocations, + overallocated, + min: this._addTimezoneOffset(new Date(min)), + max: this._addTimezoneOffset(new Date(max)), + }; + } + + private _updateLastInstance( + duration: number, + lastInstance?: SubAllocation, + ) { + if (lastInstance) { + const prevCount = (lastInstance.percent * duration) / PERCENT; + lastInstance.percent = ((prevCount + 1) / duration) * PERCENT; + lastInstance.endDate = this._nextDay(lastInstance.endDate); + return true; + } + return false; + } + private _createLastInstance( + duration: number, + allocation: number, + date: Date, + billingRate: number, + allocatedHours: number, + allotedDeals: any, + currency?: string, + ) { + return { + percent: (DIGITS.ONE / duration) * PERCENT, + allocation, + startDate: this._addTimezoneOffset(date), + endDate: this._addTimezoneOffset(date), + billingRate, + allocatedHours, + allotedDeals, + currency, + }; + } + private _checkIfOverallocated(suballocation: any): boolean { + throw new Error('Method not implemented.'); + } + + private _getAllotedDeals(suballocation: any, dealId: string) { + let allocatedHours = 0; + const allotedDeals: any = []; + + suballocation.allocations.forEach(allocation => { + if (allocation.dealId === dealId) { + allocatedHours = allocation.allocation; + } + allotedDeals.push({ + name: allocation.deal?.name!, + allocatedHours: allocation.allocation, + billingRate: allocation.billingRate, + status: allocation.deal?.status, + }); + }); + return {allocatedHours, allotedDeals}; + } // You can optionally override or use the inherited protected methods protected _nextDay(date: Date) { // Custom implementation or call the parent method @@ -205,7 +457,7 @@ export type IColumnComponent = { item: GanttTaskValue; contextItems: NbMenuItem[]; active: boolean; - showKebab: boolean; + showKebab?: boolean; showParentInitials: boolean; showChildInitials: boolean; showOverallocatedIcon: boolean; @@ -216,6 +468,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-lib/src/lib/components/readme.md b/projects/arc-lib/src/lib/components/readme.md index 7b53578d..a3687746 100644 --- a/projects/arc-lib/src/lib/components/readme.md +++ b/projects/arc-lib/src/lib/components/readme.md @@ -1,57 +1,11 @@ # COMPONENTS -## Gantt +## GanttModule -### GanttBarComponent +- The Gantt module is a project management tool that provides a visual representation of a project schedule. +- It displays tasks along a timeline,, allowing users to see task durations, dependencies, and progress at a glance. Users can easily adjust timelines by dragging tasks, set milestones, and allocate resources, making it an effective tool for planning and tracking project timelines. -- The component is used to render the bars for the Gantt chart. It is generic and can be used with any type - of task value. The component contains properties and methods for rendering the bars and sub-allocations, formatting the allocation values, and translating labels using the `TranslationService` and `TranslateService`. - -### GanttColumnComponent - -- This Component is used to displays a single column of the Gantt chart. It has several input properties - like -- `contextItems`: An array of NbMenuItem objects representing the context menu items to display for the - task. -- `showKebab`: A boolean value indicating whether to show a kebab icon for the task or not. -- `showParentInitials`: A boolean value indicating whether to show the initials of the parent task or not. -- `showChildInitials`: A boolean value indicating whether to show the initials of the child tasks or not. -- `showOverallocatedIcon`: A boolean value indicating whether to show an overallocated icon for the task or - not. -- `contextItemFilter`: A function that takes a GanttTaskValue object as input and returns a boolean - indicating whether to display the context menu items for that task or not. - -### GanttHeaderComponent - -- The component that represents the header of a Gantt chart. It has several inputs, including - `searchPlaceholder` string representing the placeholder text to be displayed in the search box and `showSearch` A boolean value indicating whether the search box should be displayed or not. - -### GanttChartTooltipComponent - -- The component which is displayed when hovering over a Gantt chart bar.The tooltip displays information - about a SubAllocation object, including its start and end dates, allocation rate, and allocated hours per day. The SubAllocation object is passed to the component using the item input property.The component has methods for formatting the allocation rate and allocated hours per day values into readable strings - -## Service: - -### GanttService - -- The GanttService class has several private properties, including ` _data`, `_overlays`, `_tooltipOverlay`, - `_eventHandlers` etc -- The render method takes an ElementRef representing the container containing options to configure the Gantt - chart. The method sets several properties such as the `row_height, bar_height, scale_height`, and readonly properties. It also sets up the layout of the Gantt chart and registers event handlers for task clicks and - grid header clicks. - -### Timeline-scale - -- `MonthlyScaleService` : This is an service for generating a monthly scale for a Gantt chart It provides a configuration for displaying the Monthly timeline scale for a Gantt chart. The `config()` method, which returns an array of objects representing the different units in the timeline scale.The `GanttScaleService` interface defines a contract for a service that can be used to generate scale configurations for different timelines. The `MonthlyScaleService` class implements this interface by defining the scale property, which is set to Timelines.Monthly - -- `QuarterlyScaleService` : This is an service for generating a quarterly scale for a Gantt chart. The service implements the `GanttScaleService` interface and provides a `config()` method that returns an array of scale configuration objects.The `QuarterlyScaleService` class has a scale property that is set to Timelines.Quarterly, indicating that this service generates a quarterly scale. - -- `WeeklyScaleService` : This defines service called `WeeklyScaleService` which implements the `GanttScaleService` interface. The `GanttScaleService` interface defines the contract that all Gantt scale services must adhere to. The `WeeklyScaleService` service provides a configuration for a Gantt chart with a weekly timeline. The configuration includes an array of objects that specify the scale units, step size, and formatting for each unit.The `config()` method returns an array of two objects. The first object represents the weekly scale unit and formats the date using the `_formatWeeklyScale` private method. The second object represents the daily scale unit and formats the date using the `toLocaleString()` - -### GanttModule - -- This is an Angular module called GanttModule which declares and exports a components and Modules related to Gantt.The module also provides three different `GanttScaleService` implementations as providers using the GANTT_SCALES injection token: `MonthlyScaleService`, `WeeklyScaleService`, and `QuarterlyScaleService`as we discuss above +For further reference you can refer [Here](/projects/arc-lib/src/lib/components/gantt/readme.md) ## Resize @@ -68,16 +22,16 @@ - This component implements the behavior of a search and select component. It has several methods, the methods are creating a Set of removed items, assigning a copy of the options to the visibleList array, sorting the list by groups, and subscribing to changes in the value of a search control. -- It also detects changes to the options property and updates the visibleList array accordingly,initialized +- It also detects changes to the options property and updates the visibleList array accordingly,initialized and focuses on the search input field if it exists,emits an event and calls the closed event if the allowInput property is true. -- It also retrieves the name of an item by getting the value of the name field of the item,removes an item +- It also retrieves the name of an item by getting the value of the name field of the item,removes an item from the list and emits an event,method checks if an item is a placeholder item,selects the first item in the list if the user presses enter on the search box and there are items in the list and groups the visibleList array according to the groupConfig property. ## Selector ### SelectComponent -- The component is used to display a list of options that can be selected by the user. The selected options +- The component is used to display a list of options that can be selected by the user. The selected options can be displayed as tags. The component has various inputs and outputs - The component uses a `SelectionModel` class to keep track of the selected items. The component can be configured to allow multiple or single selection by using the multiple input. The component also has a search box to filter the options, and the allowInput input allows the user to enter a custom value that is not in the list. @@ -90,10 +44,10 @@ - This component defines the behavior of a select dropdown in the UI.The options property is an array of `NameIdRequired` objects that will be displayed as the available options in the dropdown. -- The multiple, allowInput, disabled, and search properties are boolean values that control various aspects +- The multiple, allowInput, disabled, and search properties are boolean values that control various aspects of the dropdown behavior. `multiple` specifies whether multiple options can be selected at once, `allowInput` specifies whether the user can input a custom option, `disabled` specifies whether the dropdown is disabled or not, and `search` specifies whether a search box is displayed to filter the options. -## Auth +## Auth ### Login Component: @@ -105,22 +59,21 @@ ### Auth Component: -- Auth component is a module that handles the authentication and authorization of users. It is responsible +- Auth component is a module that handles the authentication and authorization of users. It is responsible for managing user sessions, verifying user credentials, and granting access to protected resources based on the user's role and permissions. -- The auth component typically includes a login as well as a registration form for new users to create an +- The auth component typically includes a login as well as a registration form for new users to create an account. The component may also handle password reset functionality and provide options for users to manage their accounts -- This component defines an Angular component that extends the `NbAuthComponent` class, which is a part of +- This component defines an Angular component that extends the `NbAuthComponent` class, which is a part of the `@nebular/auth` package. This component is responsible for handling the authentication process in the Angular application. It is using dependency injection to inject the `NbAuthService` and `Location` dependencies into the component's constructor. - ### Animations -- These animations could be used to provide visual feedback when elements are added or removed fro DOM, +- These animations could be used to provide visual feedback when elements are added or removed fro DOM, or to animate UI elements such as buttons or icons.and it also defines two Angular animations, dropdownAnimation and rotateAnimation. -- `DropdownAnimation`: The `dropdownAnimation` trigger defines two states: `void'` and `open`. The `void` - state is applied to an element that does not exist in the DOM. The `open` state is applied to an element that is visible in the DOM. -- `RotateAnimation` : The `rotateAnimation` trigger defines two states: `closed` and `open`. The `closed` - state sets the element's rotation angle to 0 degrees. The`open` state sets the element's rotation angle to 180 degrees. +- `DropdownAnimation`: The `dropdownAnimation` trigger defines two states: `void'` and `open`. The `void` + state is applied to an element that does not exist in the DOM. The `open` state is applied to an element that is visible in the DOM. +- `RotateAnimation` : The `rotateAnimation` trigger defines two states: `closed` and `open`. The `closed` + state sets the element's rotation angle to 0 degrees. The`open` state sets the element's rotation angle to 180 degrees. diff --git a/projects/arc-lib/src/lib/components/selector/select/select.component.html b/projects/arc-lib/src/lib/components/selector/select/select.component.html index 56648a9a..e00453cf 100644 --- a/projects/arc-lib/src/lib/components/selector/select/select.component.html +++ b/projects/arc-lib/src/lib/components/selector/select/select.component.html @@ -38,7 +38,7 @@ *ngFor="let item of visibleTags" [text]="asString(item && item[nameField])" [ngStyle]="{ - maxWidth: 'calc(100% - ' + suffixCount * suffixWidth + 'px)', + maxWidth: 'calc(100% - ' + suffixCount * suffixWidth + 'px)' }" [removable]="!disabled" (remove)="toggle(item); toggleDropdown()" @@ -61,7 +61,7 @@ (click)="$event.stopPropagation(); !allowInput && toggleDropdown()" [ngStyle]="{ minWidth: inputMinWidth + 'px', - marginRight: suffixCount * suffixWidth + 'px', + marginRight: suffixCount * suffixWidth + 'px' }" /> diff --git a/projects/arc/src/app/app-routing.module.ts b/projects/arc/src/app/app-routing.module.ts index 583bf642..ea5a884f 100644 --- a/projects/arc/src/app/app-routing.module.ts +++ b/projects/arc/src/app/app-routing.module.ts @@ -1,7 +1,12 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; +import {GanttDemoComponent} from './components/gantt-demo/gantt-demo.component'; +import {GanttZoomBarComponent} from '@project-lib/components/gantt/components/gantt-zoombar/gantt-zoombar.component'; +import {GanttComponent} from './components/gantt.component'; +import {GanttTooltipComponent} from '@project-lib/components/gantt/components'; +import {TimelineComponent} from '@project-lib/components/gantt/components/timeline/timeline.component'; +import {LoggedInGuard, AuthGuard} from '@project-lib/core/auth'; import {environment} from '../environments/environment'; -import {AuthGuard, LoggedInGuard} from '@project-lib/core/auth'; const routes: Routes = [ { @@ -25,6 +30,10 @@ const routes: Routes = [ ), canActivate: [AuthGuard], }, + { + path: 'gantt-demo', + component: GanttDemoComponent, + }, { path: '', redirectTo: environment.homePath, diff --git a/projects/arc/src/app/app.module.ts b/projects/arc/src/app/app.module.ts index 24ba47bb..adbb36bb 100644 --- a/projects/arc/src/app/app.module.ts +++ b/projects/arc/src/app/app.module.ts @@ -23,9 +23,12 @@ 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 {GanttDemoComponent} from './components/gantt-demo/gantt-demo.component'; +import {GanttComponent} from './components/gantt.component'; +import {TimelineComponent} from '@project-lib/components/gantt/components/timeline/timeline.component'; @NgModule({ - declarations: [AppComponent], + declarations: [AppComponent, GanttDemoComponent, GanttComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [ BrowserModule, 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 new file mode 100644 index 00000000..0ab04345 --- /dev/null +++ b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.html @@ -0,0 +1,43 @@ + + +
+
+ +
+
+
+ + + + + + + + + + + + +
+ + + + diff --git a/projects/arc/src/app/components/gantt-demo/gantt-demo.component.scss b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.scss new file mode 100644 index 00000000..dfa4b151 --- /dev/null +++ b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.scss @@ -0,0 +1,140 @@ +@use 'sass:map'; +@import '../../../../../arc-lib/src/lib/theme/styles/variables'; + +.bar-wrapper { + height: calc(100% - 1rem); +} +.gantt-container { + position: relative; + border-top: 0.063rem solid map.get($color, gantt-lines); + height: 100%; + transition: height 2s ease-in-out; + .gantt-loading { + display: none; + width: 100%; + background-color: map.get($color, transparent-light); + z-index: 10; + height: 3.125rem; + transition: transform 2s ease-in-out; + padding: 0.938rem; + transform: translateY(3.125rem); + align-items: center; + justify-content: center; + } + &.loading { + .gantt-chart { + height: calc(100% - 3.125rem); + } + .gantt-loading { + display: flex; + transform: translateY(0); + } + } +} + +.gantt-menu { + background-color: map.get($color, light); + box-shadow: 0px 0.938rem 1.25rem 0px #00000029; + width: 11.75rem; +} + +nb-list-item { + border: none; + cursor: pointer; + align-items: center; + height: 1em; + &:hover { + background-color: map.get($color, background-basic); + } + + .title { + position: relative; + padding-left: 1em; + bottom: 0.15em; + } +} + +.dot-falling { + position: relative; + left: -9999px; + width: 0.625rem; + height: 0.625rem; + border-radius: 0.313rem; + background-color: map.get($font, primary); + color: map.get($font, primary); + box-shadow: 9999px 0 0 0 map.get($font, primary); + animation: dot-falling 1s infinite linear; + animation-delay: 0.1s; +} +.dot-falling::before, +.dot-falling::after { + content: ''; + display: inline-block; + position: absolute; + top: 0; +} +.dot-falling::before { + width: 0.625rem; + height: 0.625rem; + border-radius: 0.313rem; + background-color: map.get($font, primary); + color: map.get($font, primary); + animation: dot-falling-before 1s infinite linear; + animation-delay: 0s; +} +.dot-falling::after { + width: 0.625rem; + height: 0.625rem; + border-radius: 0.313rem; + background-color: map.get($font, primary); + color: map.get($font, primary); + animation: dot-falling-after 1s infinite linear; + animation-delay: 0.2s; +} + +@keyframes dot-falling { + 0% { + box-shadow: 624.938rem -0.938rem 0 0 map.get($color, falling-loader-shadow); + } + 25%, + 50%, + 75% { + box-shadow: 624.938rem 0 0 0 map.get($font, primary); + } + 100% { + box-shadow: 624.938rem 0.938rem 0 0 map.get($color, falling-loader-shadow); + } +} +@keyframes dot-falling-before { + 0% { + box-shadow: 624rem -0.938rem 0 0 map.get($color, falling-loader-shadow); + } + 25%, + 50%, + 75% { + box-shadow: 624rem 0 0 0 map.get($font, primary); + } + 100% { + box-shadow: 624rem 0.938rem 0 0 map.get($color, falling-loader-shadow); + } +} +@keyframes dot-falling-after { + 0% { + box-shadow: 625.875rem -0.938rem 0 0 map.get($color, falling-loader-shadow); + } + 25%, + 50%, + 75% { + box-shadow: 625.875rem 0 0 0 map.get($font, primary); + } + 100% { + box-shadow: 625.875rem 0.938rem 0 0 map.get($color, falling-loader-shadow); + } +} +.default-color { + fill: map.get($font, 'primary'); +} + +.header-icon-wrapper{ + margin: -12px; +} \ No newline at end of file diff --git a/projects/arc/src/app/components/gantt-demo/gantt-demo.component.spec.ts b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.spec.ts new file mode 100644 index 00000000..b4a78f70 --- /dev/null +++ b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.spec.ts @@ -0,0 +1,22 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {GanttDemoComponent} from './gantt-demo.component'; + +describe('GanttDemoComponent', () => { + let component: GanttDemoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GanttDemoComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(GanttDemoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 00000000..3cbb5644 --- /dev/null +++ b/projects/arc/src/app/components/gantt-demo/gantt-demo.component.ts @@ -0,0 +1,172 @@ +import { + AfterViewInit, + Component, + ElementRef, + TemplateRef, + Type, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import {NbMenuItem, NbSidebarService} from '@nebular/theme'; +import { + GanttProviders, + GanttAdapter, + CustomGanttAdapter, + GanttService, + GanttRenderOptions, + ContextItemFilter, + GanttRowConfig, + GanttTaskValue, + IBarComponent, + IColumnComponent, + KebabListItem, + Timelines, +} from '@project-lib/components/gantt'; +import {GanttScaleUnits} from '@project-lib/components/gantt/enum'; +import {Item, empData} from '@project-lib/components/gantt/model/item.model'; +import {AnyObject} from '@project-lib/core/api'; +import {gantt} from 'dhtmlx-gantt'; +import {takeUntil, switchMap, tap, map} from 'rxjs'; + +@Component({ + selector: 'arc-gantt-demo', + templateUrl: './gantt-demo.component.html', + styleUrls: ['./gantt-demo.component.scss'], + providers: [ + GanttProviders, + { + provide: GanttAdapter, + useClass: CustomGanttAdapter, + }, + ], +}) +export class GanttDemoComponent + implements GanttRenderOptions +{ + // data for tooltip component + showTooltip = false; + selectedItem: Item; + + constructor(public readonly viewContainerRef: ViewContainerRef) {} + contextItems: NbMenuItem[]; + contextTemplate?: TemplateRef; + columnName?: string; + showKebab?: boolean; + showParentInitials: boolean; + showChildInitials: boolean; + columnComponent: Type>; + barComponent: Type>; + columnWidth: number; + resizer: boolean; + sorting: boolean; + moveToToday: boolean; + highlightRange?: [Date, Date]; + showOverallocatedIcon: boolean; + showNonBillableIcon: boolean; + contextItemFilter?: ContextItemFilter; + defaultScale: Timelines; + sidebarItems: empData[] = []; // Load or set your sidebar items here + // allocationTypes: any; + // allocationBase: number; + markToday: boolean; + showBillingRate?: boolean; + groupings?: string[]; + childIndent: boolean; + tooltipOffset?: number; + infiniteScroll: boolean; + batchSize?: number; + searchPlaceholder?: string; + showSearch: boolean; + ganttStartDate?: Date; + kebabOption: (task: GanttTaskValue) => KebabListItem[]; + ganttRowConfig: GanttRowConfig; + 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 ', + 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, + }, + ]; + + allocationTypes = { + PlaceholderResource: 'PlaceholderResource', + }; + + allocationBase = 40; + + item: any = { + type: 'ActualResource', + allocation: 32, + payload: {dealStage: 'closedwon', billingRate: 100}, + classes: ['example-class'], + subAllocations: [ + {percent: 50, allocation: 20, allocatedHours: 20, classes: ['class1']}, + {percent: 50, allocation: 15, allocatedHours: 15, classes: ['class2']}, + ], + }; + + // Data for GanttHeaderComponent + headerDesc = true; + headerName = 'Dynamic Project Gantt'; + headerSearchPlaceholder = 'Search your tasks'; + headerShowSearch = true; + + onSidebarItemSelected(item: Item): void { + this.selectedItem = this.convertToItem(item); + console.log(item); + } + + // Helper function to convert empData to Item if needed + convertToItem(empItem: Item): Item { + return { + allocatedHours: 40, + billingRate: 100, + startDate: new Date(), + endDate: new Date(), + }; + } +} 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 00000000..abae5c61 --- /dev/null +++ b/projects/arc/src/app/components/gantt.component.html @@ -0,0 +1 @@ +
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 00000000..aa3c7fea --- /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'; + + +.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 00000000..efe241f1 --- /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 00000000..2a5e85c3 --- /dev/null +++ b/projects/arc/src/app/components/gantt.component.ts @@ -0,0 +1,203 @@ +import { + AfterViewInit, + Component, + ElementRef, + TemplateRef, + Type, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import {NbMenuItem} from '@nebular/theme'; +import { + GanttProviders, + GanttAdapter, + CustomGanttAdapter, + GanttService, + GanttRenderOptions, + ContextItemFilter, + GanttRowConfig, + GanttTaskValue, + IBarComponent, + IColumnComponent, + KebabListItem, + Timelines, +} from '@project-lib/components/gantt'; +import {Item, empData} from '@project-lib/components/gantt/model/item.model'; +import {AnyObject} from '@project-lib/core/api'; +import {gantt} from 'dhtmlx-gantt'; +import {BehaviorSubject, takeUntil} from 'rxjs'; + +@Component({ + selector: 'arc-gantt', + templateUrl: './gantt.component.html', + styleUrls: ['./gantt.component.scss'], + providers: [ + GanttProviders, + { + provide: GanttAdapter, + useClass: CustomGanttAdapter, + }, + ], +}) +export class GanttComponent + implements GanttRenderOptions +{ + private _data: BehaviorSubject = new BehaviorSubject([]); + @ViewChild('gantt', {static: true}) ganttContainer!: ElementRef; + // data for tooltip component + showTooltip = false; + selectedItem: Item; + infiniteScroll = false; + + constructor( + private readonly ganttSvc: GanttService, + public readonly viewContainerRef: ViewContainerRef, + ) {} + showParentInitials: boolean; + showChildInitials: boolean; + showOverallocatedIcon: boolean; + columnComponent: Type>; + barComponent: Type>; + contextItems: NbMenuItem[]; + contextTemplate?: TemplateRef; + columnName?: string; + showKebab?: boolean; + columnWidth: number; + resizer: boolean; + sorting: boolean; + moveToToday: boolean; + highlightRange?: [Date, Date]; + showNonBillableIcon: boolean; + contextItemFilter?: ContextItemFilter; + defaultScale: Timelines; + markToday: boolean; + showBillingRate?: boolean; + groupings?: string[]; + childIndent: boolean; + tooltipOffset?: number; + batchSize?: number; + searchPlaceholder?: string; + showSearch: boolean; + ganttStartDate?: Date; + kebabOption: (task: GanttTaskValue) => KebabListItem[]; + ganttRowConfig: GanttRowConfig; + + private initializeGantt(): void { + gantt.config.scale_unit = 'year'; + gantt.config.date_scale = '%Y'; + gantt.config.subscales = [ + {unit: 'month', step: 1, date: '%F'}, + {unit: 'week', step: 1, date: 'Week %W'}, + ]; + gantt.config.start_date = new Date(); + gantt.config.end_date = new Date( + gantt.config.start_date.getFullYear() + 1, + 0, + 1, + ); + + gantt.init(this.ganttContainer.nativeElement); + + gantt.parse({ + data: [ + { + id: 1, + text: 'Project #1', + start_date: '01-04-2023', + duration: 365, + progress: 0.6, + }, + { + id: 2, + text: 'Task #1', + start_date: '01-04-2023', + duration: 30, + progress: 0.6, + parent: 1, + }, + { + id: 3, + text: 'Task #2', + start_date: '01-05-2023', + duration: 60, + progress: 0.6, + parent: 1, + }, + ], + }); + } + + // 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, + }, + ]; + + 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']}, + ], + }; + headerDesc = true; + headerName = 'Dynamic Project Gantt'; + headerSearchPlaceholder = 'Search your tasks'; + headerShowSearch = true; +}