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-dynamic": "^19.0.0",
|
||||
"@angular/router": "^19.0.0",
|
||||
"luxon": "^3.6.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
@ -27,6 +28,7 @@
|
||||
"@angular/cli": "^19.0.2",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/luxon": "^3.6.2",
|
||||
"jasmine-core": "~5.4.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
@ -5054,6 +5056,13 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
@ -9845,6 +9854,15 @@
|
||||
"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": {
|
||||
"version": "0.30.12",
|
||||
"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-dynamic": "^19.0.0",
|
||||
"@angular/router": "^19.0.0",
|
||||
"luxon": "^3.6.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
@ -29,6 +30,7 @@
|
||||
"@angular/cli": "^19.0.2",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/luxon": "^3.6.2",
|
||||
"jasmine-core": "~5.4.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
|
@ -48,4 +48,26 @@
|
||||
</mat-select>
|
||||
</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 />
|
@ -12,12 +12,18 @@ import { MatChipsModule } from '@angular/material/chips';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
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({
|
||||
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',
|
||||
styleUrl: './app.component.scss'
|
||||
})
|
||||
@ -45,4 +51,48 @@ export class AppComponent {
|
||||
unselectAll(): void {
|
||||
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