Gantt
This commit is contained in:
		
							parent
							
								
									7a0ecf281a
								
							
						
					
					
						commit
						6b76e5b905
					
				
							
								
								
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -18,6 +18,7 @@
 | 
				
			|||||||
        "@angular/platform-browser": "^19.0.0",
 | 
					        "@angular/platform-browser": "^19.0.0",
 | 
				
			||||||
        "@angular/platform-browser-dynamic": "^19.0.0",
 | 
					        "@angular/platform-browser-dynamic": "^19.0.0",
 | 
				
			||||||
        "@angular/router": "^19.0.0",
 | 
					        "@angular/router": "^19.0.0",
 | 
				
			||||||
 | 
					        "luxon": "^3.6.1",
 | 
				
			||||||
        "rxjs": "~7.8.0",
 | 
					        "rxjs": "~7.8.0",
 | 
				
			||||||
        "tslib": "^2.3.0",
 | 
					        "tslib": "^2.3.0",
 | 
				
			||||||
        "zone.js": "~0.15.0"
 | 
					        "zone.js": "~0.15.0"
 | 
				
			||||||
@ -27,6 +28,7 @@
 | 
				
			|||||||
        "@angular/cli": "^19.0.2",
 | 
					        "@angular/cli": "^19.0.2",
 | 
				
			||||||
        "@angular/compiler-cli": "^19.0.0",
 | 
					        "@angular/compiler-cli": "^19.0.0",
 | 
				
			||||||
        "@types/jasmine": "~5.1.0",
 | 
					        "@types/jasmine": "~5.1.0",
 | 
				
			||||||
 | 
					        "@types/luxon": "^3.6.2",
 | 
				
			||||||
        "jasmine-core": "~5.4.0",
 | 
					        "jasmine-core": "~5.4.0",
 | 
				
			||||||
        "karma": "~6.4.0",
 | 
					        "karma": "~6.4.0",
 | 
				
			||||||
        "karma-chrome-launcher": "~3.2.0",
 | 
					        "karma-chrome-launcher": "~3.2.0",
 | 
				
			||||||
@ -5054,6 +5056,13 @@
 | 
				
			|||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/luxon": {
 | 
				
			||||||
 | 
					      "version": "3.6.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/mime": {
 | 
					    "node_modules/@types/mime": {
 | 
				
			||||||
      "version": "1.3.5",
 | 
					      "version": "1.3.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
 | 
				
			||||||
@ -9845,6 +9854,15 @@
 | 
				
			|||||||
        "yallist": "^3.0.2"
 | 
					        "yallist": "^3.0.2"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/luxon": {
 | 
				
			||||||
 | 
					      "version": "3.6.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=12"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/magic-string": {
 | 
					    "node_modules/magic-string": {
 | 
				
			||||||
      "version": "0.30.12",
 | 
					      "version": "0.30.12",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,7 @@
 | 
				
			|||||||
    "@angular/platform-browser": "^19.0.0",
 | 
					    "@angular/platform-browser": "^19.0.0",
 | 
				
			||||||
    "@angular/platform-browser-dynamic": "^19.0.0",
 | 
					    "@angular/platform-browser-dynamic": "^19.0.0",
 | 
				
			||||||
    "@angular/router": "^19.0.0",
 | 
					    "@angular/router": "^19.0.0",
 | 
				
			||||||
 | 
					    "luxon": "^3.6.1",
 | 
				
			||||||
    "rxjs": "~7.8.0",
 | 
					    "rxjs": "~7.8.0",
 | 
				
			||||||
    "tslib": "^2.3.0",
 | 
					    "tslib": "^2.3.0",
 | 
				
			||||||
    "zone.js": "~0.15.0"
 | 
					    "zone.js": "~0.15.0"
 | 
				
			||||||
@ -29,6 +30,7 @@
 | 
				
			|||||||
    "@angular/cli": "^19.0.2",
 | 
					    "@angular/cli": "^19.0.2",
 | 
				
			||||||
    "@angular/compiler-cli": "^19.0.0",
 | 
					    "@angular/compiler-cli": "^19.0.0",
 | 
				
			||||||
    "@types/jasmine": "~5.1.0",
 | 
					    "@types/jasmine": "~5.1.0",
 | 
				
			||||||
 | 
					    "@types/luxon": "^3.6.2",
 | 
				
			||||||
    "jasmine-core": "~5.4.0",
 | 
					    "jasmine-core": "~5.4.0",
 | 
				
			||||||
    "karma": "~6.4.0",
 | 
					    "karma": "~6.4.0",
 | 
				
			||||||
    "karma-chrome-launcher": "~3.2.0",
 | 
					    "karma-chrome-launcher": "~3.2.0",
 | 
				
			||||||
 | 
				
			|||||||
@ -48,4 +48,26 @@
 | 
				
			|||||||
  </mat-select>
 | 
					  </mat-select>
 | 
				
			||||||
</mat-form-field>
 | 
					</mat-form-field>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- 
 | 
				
			||||||
 | 
					<mat-form-field appearance="fill">
 | 
				
			||||||
 | 
					  <mat-label>Choose an option</mat-label>
 | 
				
			||||||
 | 
					  <input matInput [grossNetCalculator]="calculator">
 | 
				
			||||||
 | 
					  <gross-net-calculator-toggle-button matIconPrefix></gross-net-calculator-toggle-button>
 | 
				
			||||||
 | 
					  <gross-net-calculator #calculator></gross-net-calculator>
 | 
				
			||||||
 | 
					</mat-form-field> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div style="margin: 1rem">
 | 
				
			||||||
 | 
					  <app-gantt [items]="ganttItems"></app-gantt>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- component.html -->
 | 
				
			||||||
 | 
					<!-- <ngx-gantt #gantt [items]="items">
 | 
				
			||||||
 | 
					  <ngx-gantt-table>
 | 
				
			||||||
 | 
					    <ngx-gantt-column name="test" width="300px">
 | 
				
			||||||
 | 
					      <ng-template #cell let-item="item"> {{ item.title }} </ng-template>
 | 
				
			||||||
 | 
					    </ngx-gantt-column>
 | 
				
			||||||
 | 
					  </ngx-gantt-table>
 | 
				
			||||||
 | 
					</ngx-gantt> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<router-outlet />
 | 
					<router-outlet />
 | 
				
			||||||
@ -12,12 +12,18 @@ import { MatChipsModule } from '@angular/material/chips';
 | 
				
			|||||||
import { AsyncPipe } from '@angular/common';
 | 
					import { AsyncPipe } from '@angular/common';
 | 
				
			||||||
import { MatSelectModule } from '@angular/material/select';
 | 
					import { MatSelectModule } from '@angular/material/select';
 | 
				
			||||||
import { MatToolbarModule } from '@angular/material/toolbar';
 | 
					import { MatToolbarModule } from '@angular/material/toolbar';
 | 
				
			||||||
 | 
					import { MatButtonModule } from '@angular/material/button';
 | 
				
			||||||
import { MatIconModule } from '@angular/material/icon';
 | 
					import { MatIconModule } from '@angular/material/icon';
 | 
				
			||||||
import { SelectFilterComponent } from "./select-filter/select-filter.component";
 | 
					import { SelectFilterComponent } from "./select-filter/select-filter.component";
 | 
				
			||||||
 | 
					import { GanttComponent, GanttItem } from "./gantt/gantt.component";
 | 
				
			||||||
 | 
					import { GrossNetCalculatorComponent } from "./gross-net-calculator/gross-net-calculator.component";
 | 
				
			||||||
 | 
					import { GrossNetCalculatorInputDirective } from './gross-net-calculator/gross-net-calculator-input.directive';
 | 
				
			||||||
 | 
					import { GrossNetCalculatorToggleButtonComponent } from "./gross-net-calculator/toggle-button/toggle-button.component";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-root',
 | 
					  selector: 'app-root',
 | 
				
			||||||
  imports: [RouterOutlet, MatToolbarModule, MatIconModule, CommentBoxComponent, MatFormFieldModule, MatInputModule, ReactiveFormsModule, ProgressStepperComponent, MatOptionModule, MatSelectModule, MatAutocompleteModule, MatChipsModule, AsyncPipe, FormsModule, SelectFilterComponent],
 | 
					  imports: [RouterOutlet, MatToolbarModule, GrossNetCalculatorInputDirective, MatIconModule, MatButtonModule, CommentBoxComponent, MatFormFieldModule, MatInputModule, ReactiveFormsModule, ProgressStepperComponent, MatOptionModule, MatSelectModule, MatAutocompleteModule, MatChipsModule, AsyncPipe, FormsModule, SelectFilterComponent, GanttComponent, GrossNetCalculatorComponent, GrossNetCalculatorToggleButtonComponent],
 | 
				
			||||||
 | 
					  providers: [GrossNetCalculatorInputDirective],
 | 
				
			||||||
  templateUrl: './app.component.html',
 | 
					  templateUrl: './app.component.html',
 | 
				
			||||||
  styleUrl: './app.component.scss'
 | 
					  styleUrl: './app.component.scss'
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
@ -45,4 +51,48 @@ export class AppComponent {
 | 
				
			|||||||
  unselectAll(): void {
 | 
					  unselectAll(): void {
 | 
				
			||||||
    this.selectedItemsList.setValue([]);
 | 
					    this.selectedItemsList.setValue([]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ganttItems: GanttItem[] = [{
 | 
				
			||||||
 | 
					    id: '000',
 | 
				
			||||||
 | 
					    title: 'Test 1',
 | 
				
			||||||
 | 
					    items: [{
 | 
				
			||||||
 | 
					      title: '124',
 | 
				
			||||||
 | 
					      start: 1743487875,
 | 
				
			||||||
 | 
					      duration_sec: 7200,
 | 
				
			||||||
 | 
					    }, {
 | 
				
			||||||
 | 
					      title: 'F242',
 | 
				
			||||||
 | 
					      start: 1743660675,
 | 
				
			||||||
 | 
					      duration_sec: 16 * 3600,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: '001',
 | 
				
			||||||
 | 
					    title: 'Test 2',
 | 
				
			||||||
 | 
					    items: [{
 | 
				
			||||||
 | 
					      title: '124',
 | 
				
			||||||
 | 
					      start: 1743487875,
 | 
				
			||||||
 | 
					      duration_sec: 7200,
 | 
				
			||||||
 | 
					    }, {
 | 
				
			||||||
 | 
					      title: 'F242',
 | 
				
			||||||
 | 
					      start: 1743660675,
 | 
				
			||||||
 | 
					      duration_sec: 72000,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: 'dfaf',
 | 
				
			||||||
 | 
					    title: 'WeekTest',
 | 
				
			||||||
 | 
					    items: [
 | 
				
			||||||
 | 
					      { title: '1', start: 1744005600, duration_sec: 72000 },
 | 
				
			||||||
 | 
					      { title: '2', start: 1744092000, duration_sec: 72000 },
 | 
				
			||||||
 | 
					      { title: '3', start: 1744178400, duration_sec: 72000 },
 | 
				
			||||||
 | 
					      { title: '4', start: 1744264800, duration_sec: 72000 },
 | 
				
			||||||
 | 
					      { title: '5', start: 1744351200, duration_sec: 72000 },
 | 
				
			||||||
 | 
					      { title: '6', start: 1744437600, duration_sec: 72000 },
 | 
				
			||||||
 | 
					      { title: '7', start: 1744524000, duration_sec: 72000 }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										214
									
								
								src/app/gantt/gantt.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/app/gantt/gantt.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,214 @@
 | 
				
			|||||||
 | 
					<div class="container">
 | 
				
			||||||
 | 
					    <div class="info-box">
 | 
				
			||||||
 | 
					        <div><button (click)="moveBack()">Back</button><button (click)="moveNext()">Next</button></div>
 | 
				
			||||||
 | 
					        <div>{{month}}</div>
 | 
				
			||||||
 | 
					        <div>KW {{week}}</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="table">
 | 
				
			||||||
 | 
					        <!-- <div class="header"></div> -->
 | 
				
			||||||
 | 
					        @for (item of items; track item.id) {
 | 
				
			||||||
 | 
					        <div class="table-row">{{item.title}}</div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        <!-- <div>fjadjfldjalfjldaf</div> -->
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="bars" #barContainer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @for(day of [0,1,2,3,4,5,6]; track day) {
 | 
				
			||||||
 | 
					        <div class="header">
 | 
				
			||||||
 | 
					            <div>{{getDate(day)}}</div>
 | 
				
			||||||
 | 
					            <div>{{getWeekday(day)}}</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @for (item of items; track item.id) {
 | 
				
			||||||
 | 
					        <div class="bar-row bars">
 | 
				
			||||||
 | 
					            <div class="bar-container">
 | 
				
			||||||
 | 
					                <!-- Draw open periods -->
 | 
				
			||||||
 | 
					                @for(day of [0,1,2,3,4,5,6]; track day) {
 | 
				
			||||||
 | 
					                @for (time of getDayWorkTime(day); track $index) {
 | 
				
			||||||
 | 
					                <div style="height: 100px; position: absolute; background-color: purple;"
 | 
				
			||||||
 | 
					                    [style.width]="durationToWidth(time.duration_sec)"
 | 
				
			||||||
 | 
					                    [style.left]="startToOffset(day * 24 * 3600 + time.start_day_sec + startDate.toSeconds())"> </div>
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					                <!-- Draw gantt bars -->
 | 
				
			||||||
 | 
					                @for (bar of item.items; track $index) {
 | 
				
			||||||
 | 
					                <div class="bar" [style.width]="durationToWidth(bar.duration_sec)"
 | 
				
			||||||
 | 
					                    [style.left]="startToOffset(bar.start, {dstCorrection: true})">
 | 
				
			||||||
 | 
					                    {{bar.title}}</div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            @for(day of [0,1,2,3,4,5,6]; track day) {
 | 
				
			||||||
 | 
					            <div class="grid">
 | 
				
			||||||
 | 
					                <!-- <div></div> -->
 | 
				
			||||||
 | 
					                @for (time of getDayWorkTime(day); track $index) {
 | 
				
			||||||
 | 
					                <div class="open" [style.width]="durationToWidth(time.duration_sec)"
 | 
				
			||||||
 | 
					                    [style.left]="durationToWidth(time.start_day_sec)"> </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- <div class="bar-row bars">
 | 
				
			||||||
 | 
					            <div class="bar-container">
 | 
				
			||||||
 | 
					                <div class="bar" [style.width]="durationToWidth(24*3600)" [style.left]="startToOffset(test)">fdaf</div>
 | 
				
			||||||
 | 
					                <div class="bar bar2" [style.width]="'400px'">fdaf</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            @for(day of [0,1,2,3,4,5,6]; track day) {
 | 
				
			||||||
 | 
					            <div class="grid" [style.height]="'100px'">
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        </div> -->
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="container new">
 | 
				
			||||||
 | 
					    <div class="info-box">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <button (click)="moveBack()">Back</button><button (click)="moveNext()">Next</button>
 | 
				
			||||||
 | 
					            <select>
 | 
				
			||||||
 | 
					                <option value="month">Month</option>
 | 
				
			||||||
 | 
					                <option value="week">week</option>
 | 
				
			||||||
 | 
					                <option value="day">Day</option>
 | 
				
			||||||
 | 
					            </select>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div>{{month}}</div>
 | 
				
			||||||
 | 
					        <div class="interval-descriptor">
 | 
				
			||||||
 | 
					            @for (week of intervalArray; track week.title) {
 | 
				
			||||||
 | 
					            <div [style.width]="durationToWidth(week.duration, { offsetCorrection: -1})">{{week.title}}</div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="table">
 | 
				
			||||||
 | 
					        @for (item of items; track item.id) {
 | 
				
			||||||
 | 
					        <div class="table-row">{{item.title}}</div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="bars" #barContainer>
 | 
				
			||||||
 | 
					        <div class="calender-head">
 | 
				
			||||||
 | 
					            @for(day of [0,1,2,3,4,5,6,7]; track day) {
 | 
				
			||||||
 | 
					            <div class="interval" [style.--first-interval-shift]="firstDayIntervalShift(day)"
 | 
				
			||||||
 | 
					                [style.width]="durationToWidth(getDayDuration(day))">
 | 
				
			||||||
 | 
					                <div class="content">
 | 
				
			||||||
 | 
					                    <span>{{getDate(day)}}</span>
 | 
				
			||||||
 | 
					                    <span>{{getWeekday(day)}}</span>
 | 
				
			||||||
 | 
					                    <span class="spacer"></span>
 | 
				
			||||||
 | 
					                    <div class="ticks">
 | 
				
			||||||
 | 
					                        @for (hour of hourArray; track hour) {
 | 
				
			||||||
 | 
					                        <span class="tick" [style.width]="durationToWidth(24 / hourArray.length * 3600)">{{hour}}</span>
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @for (item of items; track item.id) {
 | 
				
			||||||
 | 
					        <div class="bar-row">
 | 
				
			||||||
 | 
					            <!-- Draw open periods -->
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                @for(day of [0,1,2,3,4,5,6,7]; track day) {
 | 
				
			||||||
 | 
					                @for (time of getDayWorkTime(day); track $index) {
 | 
				
			||||||
 | 
					                <div class="open" [style.width]="durationToWidth(time.duration_sec)"
 | 
				
			||||||
 | 
					                    [style.left]="startToOffset(startDate.startOf('day').plus({days: day, seconds: time.start_day_sec}), {dstCorrection: true})">
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <!-- Draw gantt bars -->
 | 
				
			||||||
 | 
					            <div class="bar-container">
 | 
				
			||||||
 | 
					                @for (bar of item.items; track $index) {
 | 
				
			||||||
 | 
					                <div class="bar" [style.width]="durationToWidth(bar.duration_sec)"
 | 
				
			||||||
 | 
					                    [style.left]="startToOffset(bar.start, {dstCorrection: true})">
 | 
				
			||||||
 | 
					                    {{bar.title}}</div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="container new">
 | 
				
			||||||
 | 
					    <div class="info-box">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <button (click)="view.back()">Back</button><button (click)="view.next()">Next</button>
 | 
				
			||||||
 | 
					            <select #viewSelect value="week" (change)="changeViewType(viewSelect.value)">
 | 
				
			||||||
 | 
					                <option value="month">Month</option>
 | 
				
			||||||
 | 
					                <option value="week" selected="selected">week</option>
 | 
				
			||||||
 | 
					                <option value="day">Day</option>
 | 
				
			||||||
 | 
					            </select>
 | 
				
			||||||
 | 
					            <button (click)="view.today()">Today</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="month-display">{{view.month}}</div>
 | 
				
			||||||
 | 
					        <div class="interval-descriptor">
 | 
				
			||||||
 | 
					            @for (cell of view.intervalDescriptor; track cell.title) {
 | 
				
			||||||
 | 
					            <div [style.width]="cell.width">{{cell.title}}</div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="table">
 | 
				
			||||||
 | 
					        @for (item of items; track item.id) {
 | 
				
			||||||
 | 
					        <div class="table-row">{{item.title}}</div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="bars" #barContainer>
 | 
				
			||||||
 | 
					        <div class="calender-head">
 | 
				
			||||||
 | 
					            @for(item of view.cellDescriptors; track item.title) {
 | 
				
			||||||
 | 
					            <div class="cell" [class.weekend]="item.weekend" [class.sunday]="item.sunOrHoliday"
 | 
				
			||||||
 | 
					                [style.--first-interval-shift]="item.firstTickOffset" [style.width]="item.width">
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    <div class="content">
 | 
				
			||||||
 | 
					                        <span class="day-info">{{item.title}}</span>
 | 
				
			||||||
 | 
					                        <span class="day-info">{{item.subTitle}}</span>
 | 
				
			||||||
 | 
					                        <span class="spacer"></span>
 | 
				
			||||||
 | 
					                        <div class="ticks">
 | 
				
			||||||
 | 
					                            @for (item of view.tickDescriptors; track item.title) {
 | 
				
			||||||
 | 
					                            <span class="tick" [style.width]="item.width">{{item.title}}</span>
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @for (item of items; track item.id) {
 | 
				
			||||||
 | 
					        <div class="bar-row">
 | 
				
			||||||
 | 
					            <!-- Draw open periods -->
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                @for(day of view.cellDescriptors; track day.title) {
 | 
				
			||||||
 | 
					                @for (time of day.available; track $index) {
 | 
				
			||||||
 | 
					                <div class="open" [style.width]="time.duration" [style.left]="time.offset"> </div>
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <!-- Draw gantt bars -->
 | 
				
			||||||
 | 
					            <div class="bar-container">
 | 
				
			||||||
 | 
					                @for (bar of item.items; track $index) {
 | 
				
			||||||
 | 
					                <div class="bar" [style.width]="view.durationToWidth(bar.duration_sec)"
 | 
				
			||||||
 | 
					                    [style.left]="view.getOffset(bar.start)">
 | 
				
			||||||
 | 
					                    {{bar.title}}</div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										308
									
								
								src/app/gantt/gantt.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								src/app/gantt/gantt.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,308 @@
 | 
				
			|||||||
 | 
					.container {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    background-color: #ccc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    grid-template-columns: max-content 1fr;
 | 
				
			||||||
 | 
					    // column-gap: 1rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    --bar-margin: 5px;
 | 
				
			||||||
 | 
					    --row-height: 1.5rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    --header-height: 4rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header {
 | 
				
			||||||
 | 
					    height: var(--header-height);
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-flow: column;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    div {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        justify-content: center;
 | 
				
			||||||
 | 
					        padding: 0.25rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.info-box {
 | 
				
			||||||
 | 
					    grid-column-start: 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .interval-descriptor {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        border: solid thin #fff;
 | 
				
			||||||
 | 
					        border-collapse: collapse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        font-size: 0.8rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        div {
 | 
				
			||||||
 | 
					            background-color: aqua;
 | 
				
			||||||
 | 
					            border-right: solid thin #fff;
 | 
				
			||||||
 | 
					            padding: 0.05rem;
 | 
				
			||||||
 | 
					            box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            display: flex;
 | 
				
			||||||
 | 
					            justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &:nth-child(2n) {
 | 
				
			||||||
 | 
					                background-color: azure;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :last-child {
 | 
				
			||||||
 | 
					            flex-grow: 1;
 | 
				
			||||||
 | 
					            border-right: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.table {
 | 
				
			||||||
 | 
					    grid-row-start: 2;
 | 
				
			||||||
 | 
					    margin-top: var(--header-height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .table-row {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        height: calc(var(--row-height) + 2 * var(--bar-margin));
 | 
				
			||||||
 | 
					        border-top: solid thin #fff;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        padding-right: 1rem;
 | 
				
			||||||
 | 
					        padding-left: 0.5rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bars {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    grid-template-columns: repeat(7, 1fr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .header {
 | 
				
			||||||
 | 
					        border-left: solid thin #fff;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .grid {
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        border-left: solid thin #fff;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        height: calc(var(--row-height) + 2 * var(--bar-margin));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .open {
 | 
				
			||||||
 | 
					            position: absolute;
 | 
				
			||||||
 | 
					            height: 100%;
 | 
				
			||||||
 | 
					            background-color: orange;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .bar-row {
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        grid-column: span 7;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        border-top: solid thin #fff;
 | 
				
			||||||
 | 
					        font-size: 0.75rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .bar-container {
 | 
				
			||||||
 | 
					            position: absolute;
 | 
				
			||||||
 | 
					            top: var(--bar-margin);
 | 
				
			||||||
 | 
					            left: 0;
 | 
				
			||||||
 | 
					            z-index: 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .bar {
 | 
				
			||||||
 | 
					                position: absolute;
 | 
				
			||||||
 | 
					                top: 0;
 | 
				
			||||||
 | 
					                left: 0;
 | 
				
			||||||
 | 
					                height: var(--row-height);
 | 
				
			||||||
 | 
					                background-color: blue;
 | 
				
			||||||
 | 
					                color: #fff;
 | 
				
			||||||
 | 
					                box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                border-radius: 0.2rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                display: flex;
 | 
				
			||||||
 | 
					                align-items: center;
 | 
				
			||||||
 | 
					                justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                overflow: hidden;
 | 
				
			||||||
 | 
					                text-overflow: ellipsis;
 | 
				
			||||||
 | 
					                white-space: nowrap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                &:hover {
 | 
				
			||||||
 | 
					                    border: solid thin #fff;
 | 
				
			||||||
 | 
					                    z-index: 99;
 | 
				
			||||||
 | 
					                    cursor: default;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.new {
 | 
				
			||||||
 | 
					    --bg-color: rgb(247, 247, 247); //#fafafa;
 | 
				
			||||||
 | 
					    --separator-color: rgb(238, 238, 238);
 | 
				
			||||||
 | 
					    --bg-weekend-color: #e7dada;
 | 
				
			||||||
 | 
					    --bg-open-color: #fff;
 | 
				
			||||||
 | 
					    --bg-closed-color: rgb(208, 208, 208);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    --bg-week-color-even: #98afc7;
 | 
				
			||||||
 | 
					    --bg-week-color-odd: #8391a1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    background-color: var(--bg-color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    border: solid thin black;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .interval-descriptor {
 | 
				
			||||||
 | 
					        div {
 | 
				
			||||||
 | 
					            background-color: var(--bg-week-color-even);
 | 
				
			||||||
 | 
					            color: #fff;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &:nth-child(2n) {
 | 
				
			||||||
 | 
					                background-color: var(--bg-week-color-odd);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .bars {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        padding-top: var(--header-height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .open {
 | 
				
			||||||
 | 
					            height: calc(var(--row-height) + 2 * var(--bar-margin));
 | 
				
			||||||
 | 
					            position: absolute;
 | 
				
			||||||
 | 
					            background-color: var(--bg-open-color);
 | 
				
			||||||
 | 
					            top: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .bar-row {
 | 
				
			||||||
 | 
					            height: calc(var(--row-height) + 2 * var(--bar-margin));
 | 
				
			||||||
 | 
					            border-color: var(--separator-color);
 | 
				
			||||||
 | 
					            background-color: var(--bg-closed-color);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .calender-head {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        height: var(--header-height);
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        top: 0;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .spacer {
 | 
				
			||||||
 | 
					            flex-grow: 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .cell {
 | 
				
			||||||
 | 
					            position: relative;
 | 
				
			||||||
 | 
					            height: 100%;
 | 
				
			||||||
 | 
					            box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            > div {
 | 
				
			||||||
 | 
					                position: absolute;
 | 
				
			||||||
 | 
					                height: 100%;
 | 
				
			||||||
 | 
					                width: 100%;
 | 
				
			||||||
 | 
					                border-right: solid thin var(--separator-color);
 | 
				
			||||||
 | 
					                z-index: 4;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &.weekend {
 | 
				
			||||||
 | 
					                background-color: var(--bg-weekend-color);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &.sunday {
 | 
				
			||||||
 | 
					                background-color: var(--bg-weekend-color);
 | 
				
			||||||
 | 
					                color: red;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &:first-child {
 | 
				
			||||||
 | 
					                border-left: solid thin var(--separator-color);
 | 
				
			||||||
 | 
					                // shift first element if necessary
 | 
				
			||||||
 | 
					                .ticks {
 | 
				
			||||||
 | 
					                    transform: translate(var(--first-interval-shift));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &:last-child {
 | 
				
			||||||
 | 
					                .tick {
 | 
				
			||||||
 | 
					                    color: red;
 | 
				
			||||||
 | 
					                    &:last-child {
 | 
				
			||||||
 | 
					                        border-right: none;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &:not(:first-child) {
 | 
				
			||||||
 | 
					                .content {
 | 
				
			||||||
 | 
					                    border-left: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    .ticks {
 | 
				
			||||||
 | 
					                        margin-left: 1px;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .day-info {
 | 
				
			||||||
 | 
					                &:first-child {
 | 
				
			||||||
 | 
					                    padding-top: 0.4rem;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                padding-top: 0.1rem;
 | 
				
			||||||
 | 
					                padding-left: 0.25rem;
 | 
				
			||||||
 | 
					                font-size: 0.9rem;
 | 
				
			||||||
 | 
					                font-weight: 500;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .content {
 | 
				
			||||||
 | 
					                height: var(--header-height);
 | 
				
			||||||
 | 
					                display: flex;
 | 
				
			||||||
 | 
					                flex-direction: column;
 | 
				
			||||||
 | 
					                justify-content: flex-start;
 | 
				
			||||||
 | 
					                align-items: flex-start;
 | 
				
			||||||
 | 
					                z-index: 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // border: solid thin #000;
 | 
				
			||||||
 | 
					                // border-top: none;
 | 
				
			||||||
 | 
					                // box-sizing: border-box;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .ticks {
 | 
				
			||||||
 | 
					                display: flex;
 | 
				
			||||||
 | 
					                color: #000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                :first-child {
 | 
				
			||||||
 | 
					                    border-left: none;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                .tick {
 | 
				
			||||||
 | 
					                    padding-left: 0.2rem;
 | 
				
			||||||
 | 
					                    box-sizing: border-box;
 | 
				
			||||||
 | 
					                    font-size: 0.7rem;
 | 
				
			||||||
 | 
					                    border-right: solid thin #fff;
 | 
				
			||||||
 | 
					                    border-color: #000;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .table {
 | 
				
			||||||
 | 
					        margin-top: var(--header-height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .table-row {
 | 
				
			||||||
 | 
					            border-color: var(--separator-color);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .month-display {
 | 
				
			||||||
 | 
					        padding: 0.2rem;
 | 
				
			||||||
 | 
					        font-weight: 500;
 | 
				
			||||||
 | 
					        font-size: 0.95rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/app/gantt/gantt.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/app/gantt/gantt.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import { ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { GanttComponent } from './gantt.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('GanttComponent', () => {
 | 
				
			||||||
 | 
					  let component: GanttComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<GanttComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    await TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      imports: [GanttComponent]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(GanttComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										203
									
								
								src/app/gantt/gantt.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/app/gantt/gantt.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,203 @@
 | 
				
			|||||||
 | 
					import { ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core';
 | 
				
			||||||
 | 
					import { Observable } from 'rxjs';
 | 
				
			||||||
 | 
					import { DateTime } from 'luxon';
 | 
				
			||||||
 | 
					import { GanttView } from './views/view';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface GanttItem {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  items: GanttBarElement[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface GanttBarElement {
 | 
				
			||||||
 | 
					  title: string,
 | 
				
			||||||
 | 
					  start: number,
 | 
				
			||||||
 | 
					  duration_sec: number,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface WorkTime {
 | 
				
			||||||
 | 
					  start_day_sec: number,
 | 
				
			||||||
 | 
					  duration_sec: number,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IntervalDescriptor {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  duration: number,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-gantt',
 | 
				
			||||||
 | 
					  imports: [],
 | 
				
			||||||
 | 
					  templateUrl: './gantt.component.html',
 | 
				
			||||||
 | 
					  styleUrl: './gantt.component.scss'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class GanttComponent {
 | 
				
			||||||
 | 
					  @ViewChild('barContainer') _barContainer!: ElementRef;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input() items: GanttItem[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  view: GanttView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  globalBaseFontSize: number = 16;
 | 
				
			||||||
 | 
					  barContainerWidth: number = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  startDate: DateTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  weekOpenTime: WorkTime[][] = [
 | 
				
			||||||
 | 
					    [{ start_day_sec: 8 * 3600, duration_sec: 8 * 3600 }],
 | 
				
			||||||
 | 
					    [{ start_day_sec: 8 * 3600, duration_sec: 8 * 3600 }],
 | 
				
			||||||
 | 
					    [{ start_day_sec: 8 * 3600, duration_sec: 8 * 3600 }],
 | 
				
			||||||
 | 
					    [{ start_day_sec: 8 * 3600, duration_sec: 8 * 3600 }],
 | 
				
			||||||
 | 
					    [{ start_day_sec: 8 * 3600, duration_sec: 8 * 3600 }],
 | 
				
			||||||
 | 
					    [{ start_day_sec: 8 * 3600, duration_sec: 8 * 3600 }],
 | 
				
			||||||
 | 
					    [{ start_day_sec: 8 * 3600, duration_sec: 8 * 3600 }],
 | 
				
			||||||
 | 
					    [{ start_day_sec: 8 * 3600, duration_sec: 8 * 3600 }],
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(private changeDetectorRef: ChangeDetectorRef) {
 | 
				
			||||||
 | 
					    this.startDate = DateTime.now().minus({ weeks: 1 }).startOf('week').startOf('day').setLocale('de');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.view = new GanttView();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let htmlRoot: HTMLElement = <HTMLElement>document.getElementsByTagName("html")[0];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngAfterViewInit(): void {
 | 
				
			||||||
 | 
					    // get base font size
 | 
				
			||||||
 | 
					    let fontSize = window.getComputedStyle(this._barContainer.nativeElement).getPropertyValue('font-size');
 | 
				
			||||||
 | 
					    this.globalBaseFontSize = parseInt(fontSize.slice(0, fontSize.length - 2));
 | 
				
			||||||
 | 
					    this.view.globalBaseFontSize = parseInt(fontSize.slice(0, fontSize.length - 2));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // observe resizing of the bar container
 | 
				
			||||||
 | 
					    new Observable((observer) => {
 | 
				
			||||||
 | 
					      const resizeObserver = new ResizeObserver(() => {
 | 
				
			||||||
 | 
					        observer.next();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      resizeObserver.observe(this._barContainer.nativeElement);
 | 
				
			||||||
 | 
					    }).subscribe((change) => {
 | 
				
			||||||
 | 
					      this.barContainerWidth = this._barContainer.nativeElement.offsetWidth;
 | 
				
			||||||
 | 
					      this.view.width = this._barContainer.nativeElement.offsetWidth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.changeDetectorRef.detectChanges();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  changeViewType(value: string): void {
 | 
				
			||||||
 | 
					    this.view.changeView(value)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getDate(day: number): string {
 | 
				
			||||||
 | 
					    return this.startDate.plus({ days: day }).toFormat('dd.MM');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getWeekday(day: number): string {
 | 
				
			||||||
 | 
					    return this.startDate.plus({ days: day }).toFormat('EEE');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getDayDuration(day: number): number {
 | 
				
			||||||
 | 
					    if (day === 0) {
 | 
				
			||||||
 | 
					      return Math.abs(this.startDate.diff(this.startDate.endOf('day')).as('seconds'));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return 24 * 3600;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getDayWorkTime(day: number): WorkTime[] {
 | 
				
			||||||
 | 
					    return this.weekOpenTime[day];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  durationToWidth(seconds: number, opts?: { offsetCorrection?: number }): string {
 | 
				
			||||||
 | 
					    let correction = opts?.offsetCorrection ?? 0;
 | 
				
			||||||
 | 
					    return `${this.pixelPerSec * seconds + correction}px`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  startToOffset(date: DateTime | number, opts?: { dstCorrection?: boolean, offsetCorrection?: number }): string {
 | 
				
			||||||
 | 
					    // generate date from timestamp if necessary
 | 
				
			||||||
 | 
					    if (typeof date === 'number')
 | 
				
			||||||
 | 
					      date = DateTime.fromSeconds(date);
 | 
				
			||||||
 | 
					    // calculate offset 
 | 
				
			||||||
 | 
					    let offset = this.dateDiff(date, this.startDate, opts) * this.pixelPerSec;
 | 
				
			||||||
 | 
					    let correction = opts?.offsetCorrection ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return `${offset + correction}px`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dateDiff(date1: DateTime, date2: DateTime, opts?: { dstCorrection?: boolean }): number {
 | 
				
			||||||
 | 
					    // calculate offset 
 | 
				
			||||||
 | 
					    let diff = date1.diff(date2, 'seconds').as('seconds');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //modify if DST (Daylight Saving Time) has changed // TODO Check if DST Switch is problematic
 | 
				
			||||||
 | 
					    if (opts?.dstCorrection) {
 | 
				
			||||||
 | 
					      if (date1.isInDST && !date2.isInDST) {
 | 
				
			||||||
 | 
					        diff += 3600;
 | 
				
			||||||
 | 
					      } else if (date1.isInDST && !date2.isInDST) {
 | 
				
			||||||
 | 
					        diff -= 3600;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return diff;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  firstDayIntervalShift(day: number): string {
 | 
				
			||||||
 | 
					    return `${this.getDayDuration(day) / this.segmentBaseSeconds * 100 - 100}%`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  moveBack(): void {
 | 
				
			||||||
 | 
					    this.startDate = this.startDate.minus({ days: 3.5 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  moveNext(): void {
 | 
				
			||||||
 | 
					    this.startDate = this.startDate.plus({ days: 3.5 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get segmentBaseSeconds(): number {
 | 
				
			||||||
 | 
					    return 3600 * 24;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get pixelPerSec(): number {
 | 
				
			||||||
 | 
					    const SecondsPerWeek = 3600 * 24 * 7;
 | 
				
			||||||
 | 
					    return this.barContainerWidth / SecondsPerWeek;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get month(): string {
 | 
				
			||||||
 | 
					    return this.startDate.toFormat('MMMM yyyy')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get week(): string {
 | 
				
			||||||
 | 
					    return this.startDate.toFormat('nn')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO do not recalculate for every element but only once when window is resized or forward or backword is pressed
 | 
				
			||||||
 | 
					  get hourArray(): number[] {
 | 
				
			||||||
 | 
					    let base = 3600 * 24;
 | 
				
			||||||
 | 
					    let intervals = base * this.pixelPerSec / (2 * 0.8 * this.globalBaseFontSize);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO make calculation more sane!!!
 | 
				
			||||||
 | 
					    let test = Math.floor(24 / Math.ceil((2 * 0.8 * this.globalBaseFontSize) / (base * this.pixelPerSec / 24)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Array.from({ length: test }, (_, i) => i * 24 / test)
 | 
				
			||||||
 | 
					    // return Array.from({ length: 23 }, (_, i) => i + 1)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get intervalArray(): IntervalDescriptor[] {
 | 
				
			||||||
 | 
					    let start = this.startDate.get('weekNumber');
 | 
				
			||||||
 | 
					    let end = this.startDate.plus({ week: 1 }).minus({ seconds: 10 }).get('weekNumber');
 | 
				
			||||||
 | 
					    let arr: IntervalDescriptor[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = start; i <= end; i++) {
 | 
				
			||||||
 | 
					      if (i === start && start !== end) {
 | 
				
			||||||
 | 
					        arr.push({
 | 
				
			||||||
 | 
					          title: `KW ${i}`,
 | 
				
			||||||
 | 
					          duration: this.dateDiff(this.startDate.endOf('week'), this.startDate, { dstCorrection: true }) //this.startDate.endOf('week').diff(this.startDate).as('seconds')
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      } else if (i === end) {
 | 
				
			||||||
 | 
					        arr.push({
 | 
				
			||||||
 | 
					          title: `KW ${i}`,
 | 
				
			||||||
 | 
					          duration: 0 // last element grows automatically
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      } else { }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return arr;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										70
									
								
								src/app/gantt/views/base.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/app/gantt/views/base.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					import { DateTime, Settings } from "luxon";
 | 
				
			||||||
 | 
					import { DescriptorSet } from "./descriptors";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const defaultAvailable = [{ start_day_sec: 8 * 3600, duration_sec: 8 * 3600, onSaturday: false, onSunOrHoliday: false }];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export abstract class GanttViewType {
 | 
				
			||||||
 | 
					    start: DateTime;        // start date of this gantt view
 | 
				
			||||||
 | 
					    viewDuration: number;   // duration in seconds between start and end of this gantt view
 | 
				
			||||||
 | 
					    cellWidth: number;      // width of a single cell (e.g. a day in the week view)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pixelPerSecond: number = 0;     // width in pixel of one second of the gantt view
 | 
				
			||||||
 | 
					    globalBaseFontSize: number = 16;    // font size that is used to calculate tick display
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        // defining time settings
 | 
				
			||||||
 | 
					        Settings.defaultZone = 'utc';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // default view is week
 | 
				
			||||||
 | 
					        let date: DateTime = DateTime.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.start = date.startOf('week').startOf('day');
 | 
				
			||||||
 | 
					        this.viewDuration = this.start.endOf('week').endOf('day').diff(this.start).as('seconds');
 | 
				
			||||||
 | 
					        this.cellWidth = this.viewDuration / 7;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update view (e.g. resizing, interval change)
 | 
				
			||||||
 | 
					    update(opts?: { width?: number, action?: 'next' | 'back', today?: boolean }): DescriptorSet {
 | 
				
			||||||
 | 
					        // was panel resized?
 | 
				
			||||||
 | 
					        if (opts?.width)
 | 
				
			||||||
 | 
					            this.pixelPerSecond = opts.width / this.viewDuration;
 | 
				
			||||||
 | 
					        // did the interval change?
 | 
				
			||||||
 | 
					        if (opts?.action)
 | 
				
			||||||
 | 
					            this.calculateIntervalShift(opts.action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // recreate descriptors
 | 
				
			||||||
 | 
					        return this.createDescriptorSet();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // convert duration to width in pixels
 | 
				
			||||||
 | 
					    durationToWidth(seconds: number, opts?: { offsetCorrection?: number }): string {
 | 
				
			||||||
 | 
					        let correction = opts?.offsetCorrection ?? 0;
 | 
				
			||||||
 | 
					        return `${this.pixelPerSecond * seconds + correction}px`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // get offset in pixels
 | 
				
			||||||
 | 
					    getOffset(date: DateTime | number, opts?: { dstCorrection?: boolean, offsetCorrection?: number }): string {
 | 
				
			||||||
 | 
					        // generate date from timestamp if necessary
 | 
				
			||||||
 | 
					        if (typeof date === 'number')
 | 
				
			||||||
 | 
					            date = DateTime.fromSeconds(date);
 | 
				
			||||||
 | 
					        // calculate offset 
 | 
				
			||||||
 | 
					        let offset = date.diff(this.start).as('seconds') * this.pixelPerSecond;
 | 
				
			||||||
 | 
					        let correction = opts?.offsetCorrection ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return `${offset + correction}px`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isDaySunOrHoliday(date: DateTime): boolean {
 | 
				
			||||||
 | 
					        return date.weekday === 7
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    isDayOnWeekend(date: DateTime): boolean {
 | 
				
			||||||
 | 
					        return date.weekday > 5;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    createDescriptorSet(): DescriptorSet { return { interval: [], cell: [], tick: [] } };
 | 
				
			||||||
 | 
					    calculateIntervalShift(action: 'next' | 'back'): void { }
 | 
				
			||||||
 | 
					    setToday(): void { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										85
									
								
								src/app/gantt/views/day.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/app/gantt/views/day.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
 | 
					import { defaultAvailable, GanttViewType } from "./base";
 | 
				
			||||||
 | 
					import { CellDescriptor, DescriptorSet, IntervalDescriptor, TickDescriptor } from "./descriptors";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class GanttViewDay extends GanttViewType {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(date: DateTime) {
 | 
				
			||||||
 | 
					        super()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.start = date.startOf('day');
 | 
				
			||||||
 | 
					        this.viewDuration = this.start.endOf('day').diff(this.start).as('seconds');
 | 
				
			||||||
 | 
					        this.cellWidth = this.viewDuration / 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override setToday(): void {
 | 
				
			||||||
 | 
					        this.start = DateTime.now().startOf('day');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override calculateIntervalShift(action: "next" | "back"): void {
 | 
				
			||||||
 | 
					        if (action === 'next')
 | 
				
			||||||
 | 
					            this.start = this.start.plus({ days: 0.5 });
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					            this.start = this.start.minus({ days: 0.5 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override createDescriptorSet(): DescriptorSet { return { interval: this.createIntervals(), cell: this.createCells(), tick: this.createTicks() } };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // get duration of a day in seconds
 | 
				
			||||||
 | 
					    dayDuration(day?: number): number {
 | 
				
			||||||
 | 
					        // check if first day is only a fraction of a day
 | 
				
			||||||
 | 
					        if (day === 0) {
 | 
				
			||||||
 | 
					            return Math.abs(this.start.diff(this.start.endOf('day')).as('seconds'));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return 24 * 3600;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private createIntervals(): IntervalDescriptor[] {
 | 
				
			||||||
 | 
					        let weeks = [this.start.get('weekNumber')];
 | 
				
			||||||
 | 
					        let end2 = this.start.plus({ days: 1 }).minus({ seconds: 10 });
 | 
				
			||||||
 | 
					        if (!this.start.hasSame(end2, 'week'))
 | 
				
			||||||
 | 
					            weeks.push(end2.weekNumber)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return weeks.map((item, index) => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                title: `KW ${item}`,
 | 
				
			||||||
 | 
					                width: index === weeks.length - 1 ? '0px' : this.durationToWidth(this.start.endOf('week').diff(this.start).as('seconds'))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private createCells(): CellDescriptor[] {
 | 
				
			||||||
 | 
					        return Array.from({ length: 2 }, (_, day) => {
 | 
				
			||||||
 | 
					            let date = this.start.plus({ days: day });
 | 
				
			||||||
 | 
					            const duration = this.dayDuration(day);
 | 
				
			||||||
 | 
					            const sunOrHoliday = this.isDaySunOrHoliday(date);
 | 
				
			||||||
 | 
					            const weekend = this.isDayOnWeekend(date);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                title: date.toFormat('dd.MM'),
 | 
				
			||||||
 | 
					                subTitle: date.toFormat('EEE'),
 | 
				
			||||||
 | 
					                width: this.durationToWidth(duration),
 | 
				
			||||||
 | 
					                firstTickOffset: `${duration / this.cellWidth * 100 - 100}%`,
 | 
				
			||||||
 | 
					                sunOrHoliday,
 | 
				
			||||||
 | 
					                weekend,
 | 
				
			||||||
 | 
					                available: defaultAvailable.filter(item => ((!weekend && !sunOrHoliday) || (weekend && !sunOrHoliday) && item.onSaturday) || (sunOrHoliday && item.onSunOrHoliday)).map(item => {
 | 
				
			||||||
 | 
					                    return { offset: this.getOffset(date.startOf('day').plus({ seconds: item.start_day_sec })), duration: this.durationToWidth(item.duration_sec) }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private createTicks(): TickDescriptor[] {
 | 
				
			||||||
 | 
					        // TODO make calculation more sane!!!
 | 
				
			||||||
 | 
					        let test = Math.floor(24 / Math.ceil((2 * 0.8 * this.globalBaseFontSize) / (this.dayDuration() * this.pixelPerSecond / 24)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Array.from({ length: test }, (_, i) => i * 24 / test).map(item => {
 | 
				
			||||||
 | 
					            return <TickDescriptor>{
 | 
				
			||||||
 | 
					                title: item.toString(),
 | 
				
			||||||
 | 
					                width: this.durationToWidth(24 / test * 3600)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/app/gantt/views/descriptors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/app/gantt/views/descriptors.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					export interface DescriptorSet {
 | 
				
			||||||
 | 
					    interval: IntervalDescriptor[],
 | 
				
			||||||
 | 
					    cell: CellDescriptor[],
 | 
				
			||||||
 | 
					    tick: TickDescriptor[],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BasicDescriptor {
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    width: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AvailableTime {
 | 
				
			||||||
 | 
					    offset: string,
 | 
				
			||||||
 | 
					    duration: string,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IntervalDescriptor extends BasicDescriptor { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface CellDescriptor extends BasicDescriptor {
 | 
				
			||||||
 | 
					    subTitle: string;
 | 
				
			||||||
 | 
					    firstTickOffset: string,
 | 
				
			||||||
 | 
					    sunOrHoliday: boolean;
 | 
				
			||||||
 | 
					    weekend: boolean;
 | 
				
			||||||
 | 
					    available: AvailableTime[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface TickDescriptor extends BasicDescriptor { }
 | 
				
			||||||
							
								
								
									
										75
									
								
								src/app/gantt/views/month.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/app/gantt/views/month.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
 | 
					import { defaultAvailable, GanttViewType } from "./base";
 | 
				
			||||||
 | 
					import { CellDescriptor, DescriptorSet, IntervalDescriptor, TickDescriptor } from "./descriptors";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class GanttViewMonth extends GanttViewType {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(date: DateTime) {
 | 
				
			||||||
 | 
					        super()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.start = date.startOf('week').startOf('day');
 | 
				
			||||||
 | 
					        this.viewDuration = this.start.plus({ days: 4 * 7 - 1 }).endOf('day').diff(this.start).as('seconds');
 | 
				
			||||||
 | 
					        this.cellWidth = this.viewDuration / (4 * 7);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override setToday(): void {
 | 
				
			||||||
 | 
					        this.start = DateTime.now().startOf('week').startOf('day');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override calculateIntervalShift(action: "next" | "back"): void {
 | 
				
			||||||
 | 
					        if (action === 'next')
 | 
				
			||||||
 | 
					            this.start = this.start.plus({ weeks: 2 });
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					            this.start = this.start.minus({ weeks: 2 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override createDescriptorSet(): DescriptorSet { return { interval: this.createIntervals(), cell: this.createCells(), tick: this.createTicks() } };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // get duration of a day in seconds
 | 
				
			||||||
 | 
					    dayDuration(): number {
 | 
				
			||||||
 | 
					        return 24 * 3600;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private createIntervals(): IntervalDescriptor[] {
 | 
				
			||||||
 | 
					        return Array.from({ length: 4 }, (_, i) => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                title: `KW ${this.start.plus({ weeks: i }).get('weekNumber')}`,
 | 
				
			||||||
 | 
					                width: i === 3 ? '0px' : this.durationToWidth(this.start.endOf('week').diff(this.start).as('seconds'))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private createCells(): CellDescriptor[] {
 | 
				
			||||||
 | 
					        return Array.from({ length: 4 * 7 }, (_, day) => {
 | 
				
			||||||
 | 
					            let date = this.start.plus({ days: day });
 | 
				
			||||||
 | 
					            const duration = this.dayDuration();
 | 
				
			||||||
 | 
					            const sunOrHoliday = this.isDaySunOrHoliday(date);
 | 
				
			||||||
 | 
					            const weekend = this.isDayOnWeekend(date);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                title: date.toFormat('dd.MM'),
 | 
				
			||||||
 | 
					                subTitle: date.toFormat('EEE'),
 | 
				
			||||||
 | 
					                width: this.durationToWidth(duration),
 | 
				
			||||||
 | 
					                firstTickOffset: '0px',
 | 
				
			||||||
 | 
					                sunOrHoliday,
 | 
				
			||||||
 | 
					                weekend,
 | 
				
			||||||
 | 
					                available: defaultAvailable.filter(item => ((!weekend && !sunOrHoliday) || (weekend && !sunOrHoliday) && item.onSaturday) || (sunOrHoliday && item.onSunOrHoliday)).map(item => {
 | 
				
			||||||
 | 
					                    return { offset: this.getOffset(date.startOf('day').plus({ seconds: item.start_day_sec })), duration: this.durationToWidth(item.duration_sec) }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private createTicks(): TickDescriptor[] {
 | 
				
			||||||
 | 
					        // TODO make calculation more sane!!!
 | 
				
			||||||
 | 
					        let test = Math.floor(24 / Math.ceil((1.2 * 0.8 * this.globalBaseFontSize) / (this.dayDuration() * this.pixelPerSecond / 24)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Array.from({ length: test }, (_, i) => i * 24 / test).map(item => {
 | 
				
			||||||
 | 
					            return <TickDescriptor>{
 | 
				
			||||||
 | 
					                title: item.toString(),
 | 
				
			||||||
 | 
					                width: this.durationToWidth(24 / test * 3600)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										84
									
								
								src/app/gantt/views/view.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/app/gantt/views/view.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					import { DateTime, Settings } from "luxon";
 | 
				
			||||||
 | 
					import { GanttViewType } from "./base";
 | 
				
			||||||
 | 
					import { GanttViewWeek } from "./week";
 | 
				
			||||||
 | 
					import { GanttViewDay } from "./day";
 | 
				
			||||||
 | 
					import { GanttViewMonth } from "./month";
 | 
				
			||||||
 | 
					import { CellDescriptor, DescriptorSet, IntervalDescriptor, TickDescriptor } from "./descriptors";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// export interface DescriptorSet {
 | 
				
			||||||
 | 
					//     interval: IntervalDescriptor[],
 | 
				
			||||||
 | 
					//     cell: IntervalDescriptor[],
 | 
				
			||||||
 | 
					//     tick: IntervalDescriptor[],
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// export interface IntervalDescriptor {
 | 
				
			||||||
 | 
					//     title: string;
 | 
				
			||||||
 | 
					//     subTitle?: string;
 | 
				
			||||||
 | 
					//     width: string,
 | 
				
			||||||
 | 
					//     firstTickOffset?: string,
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class GanttView {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    viewType: GanttViewType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private _width: number = 0;     // width of this gantt view in pixel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    descriptorSet: DescriptorSet = {
 | 
				
			||||||
 | 
					        interval: [],
 | 
				
			||||||
 | 
					        cell: [],
 | 
				
			||||||
 | 
					        tick: [],
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        // defining time settings
 | 
				
			||||||
 | 
					        Settings.defaultZone = 'utc';
 | 
				
			||||||
 | 
					        Settings.defaultLocale = "de";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // initialize view dates (as week view)
 | 
				
			||||||
 | 
					        this.viewType = new GanttViewWeek(DateTime.now());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    set width(width: number) {
 | 
				
			||||||
 | 
					        this._width = width;
 | 
				
			||||||
 | 
					        this.descriptorSet = this.viewType.update({ width: width });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    set globalBaseFontSize(size: number) {
 | 
				
			||||||
 | 
					        this.viewType.globalBaseFontSize = size;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get month(): string { return this.viewType.start.toFormat('MMMM yyyy') }
 | 
				
			||||||
 | 
					    get week(): string { return this.viewType.start.toFormat('nn') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get intervalDescriptor(): IntervalDescriptor[] { return this.descriptorSet.interval };
 | 
				
			||||||
 | 
					    get cellDescriptors(): CellDescriptor[] { return this.descriptorSet.cell };
 | 
				
			||||||
 | 
					    get tickDescriptors(): TickDescriptor[] { return this.descriptorSet.tick };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    durationToWidth(seconds: number, opts?: { offsetCorrection?: number }): string {
 | 
				
			||||||
 | 
					        return this.viewType.durationToWidth(seconds, opts);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getOffset(date: DateTime | number, opts?: { dstCorrection?: boolean, offsetCorrection?: number }): string {
 | 
				
			||||||
 | 
					        return this.viewType.getOffset(date, opts);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    changeView(view: string): void {
 | 
				
			||||||
 | 
					        switch (view) {
 | 
				
			||||||
 | 
					            case 'day': this.viewType = new GanttViewDay(this.viewType.start); break;
 | 
				
			||||||
 | 
					            case 'week': this.viewType = new GanttViewWeek(this.viewType.start); break;
 | 
				
			||||||
 | 
					            case 'month': this.viewType = new GanttViewMonth(this.viewType.start); break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.descriptorSet = this.viewType.update({ width: this._width });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    back(): void {
 | 
				
			||||||
 | 
					        this.descriptorSet = this.viewType.update({ action: 'back' })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    next(): void {
 | 
				
			||||||
 | 
					        this.descriptorSet = this.viewType.update({ action: 'next' })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    today(): void {
 | 
				
			||||||
 | 
					        this.viewType.setToday()
 | 
				
			||||||
 | 
					        this.descriptorSet = this.viewType.update({ width: this._width });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										85
									
								
								src/app/gantt/views/week.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/app/gantt/views/week.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
 | 
					import { defaultAvailable, GanttViewType } from "./base";
 | 
				
			||||||
 | 
					import { CellDescriptor, DescriptorSet, IntervalDescriptor, TickDescriptor } from "./descriptors";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class GanttViewWeek extends GanttViewType {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(date: DateTime) {
 | 
				
			||||||
 | 
					        super()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.start = date.startOf('week').startOf('day');
 | 
				
			||||||
 | 
					        this.viewDuration = this.start.endOf('week').endOf('day').diff(this.start).as('seconds');
 | 
				
			||||||
 | 
					        this.cellWidth = this.viewDuration / 7;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override setToday(): void {
 | 
				
			||||||
 | 
					        this.start = DateTime.now().startOf('week').startOf('day');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override calculateIntervalShift(action: "next" | "back"): void {
 | 
				
			||||||
 | 
					        if (action === 'next')
 | 
				
			||||||
 | 
					            this.start = this.start.plus({ days: 3.5 });
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					            this.start = this.start.minus({ days: 3.5 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override createDescriptorSet(): DescriptorSet { return { interval: this.createIntervals(), cell: this.createCells(), tick: this.createTicks() } };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // get duration of a day in seconds
 | 
				
			||||||
 | 
					    dayDuration(day?: number): number {
 | 
				
			||||||
 | 
					        // check if first day is only a fraction of a day
 | 
				
			||||||
 | 
					        if (day === 0) {
 | 
				
			||||||
 | 
					            return Math.abs(this.start.diff(this.start.endOf('day')).as('seconds'));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return 24 * 3600;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private createIntervals(): IntervalDescriptor[] {
 | 
				
			||||||
 | 
					        let weeks = [this.start.get('weekNumber')];
 | 
				
			||||||
 | 
					        let end = this.start.plus({ week: 1 }).minus({ seconds: 10 });
 | 
				
			||||||
 | 
					        if (!this.start.hasSame(end, 'week'))
 | 
				
			||||||
 | 
					            weeks.push(end.weekNumber)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return weeks.map((item, index) => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                title: `KW ${item}`,
 | 
				
			||||||
 | 
					                width: index === weeks.length - 1 ? '0px' : this.durationToWidth(this.start.endOf('week').diff(this.start).as('seconds'))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private createCells(): CellDescriptor[] {
 | 
				
			||||||
 | 
					        return Array.from({ length: 8 }, (_, day) => {
 | 
				
			||||||
 | 
					            let date = this.start.plus({ days: day });
 | 
				
			||||||
 | 
					            const duration = this.dayDuration(day);
 | 
				
			||||||
 | 
					            const sunOrHoliday = this.isDaySunOrHoliday(date);
 | 
				
			||||||
 | 
					            const weekend = this.isDayOnWeekend(date);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                title: date.toFormat('dd.MM'),
 | 
				
			||||||
 | 
					                subTitle: date.toFormat('EEE'),
 | 
				
			||||||
 | 
					                width: this.durationToWidth(duration),
 | 
				
			||||||
 | 
					                firstTickOffset: `${duration / this.cellWidth * 100 - 100}%`,
 | 
				
			||||||
 | 
					                sunOrHoliday,
 | 
				
			||||||
 | 
					                weekend,
 | 
				
			||||||
 | 
					                available: defaultAvailable.filter(item => ((!weekend && !sunOrHoliday) || (weekend && !sunOrHoliday) && item.onSaturday) || (sunOrHoliday && item.onSunOrHoliday)).map(item => {
 | 
				
			||||||
 | 
					                    return { offset: this.getOffset(date.startOf('day').plus({ seconds: item.start_day_sec })), duration: this.durationToWidth(item.duration_sec) }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private createTicks(): TickDescriptor[] {
 | 
				
			||||||
 | 
					        // TODO make calculation more sane!!!
 | 
				
			||||||
 | 
					        let test = Math.floor(24 / Math.ceil((2 * 0.8 * this.globalBaseFontSize) / (this.dayDuration() * this.pixelPerSecond / 24)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Array.from({ length: test }, (_, i) => i * 24 / test).map(item => {
 | 
				
			||||||
 | 
					            return <TickDescriptor>{
 | 
				
			||||||
 | 
					                title: item.toString(),
 | 
				
			||||||
 | 
					                width: this.durationToWidth(24 / test * 3600)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user