diff --git a/package-lock.json b/package-lock.json
index daee4bd..60ff0b0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "WorkTime",
- "version": "0.0.1",
+ "version": "0.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "WorkTime",
- "version": "0.0.1",
+ "version": "0.1.3",
"dependencies": {
"@angular/animations": "^18.0.0",
"@angular/common": "^18.0.0",
@@ -23,6 +23,7 @@
"@capacitor/keyboard": "6.0.2",
"@capacitor/status-bar": "6.0.1",
"@ionic/angular": "^8.0.0",
+ "chart.js": "^4.4.6",
"ionicons": "^7.2.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@@ -4285,6 +4286,12 @@
"tslib": "2"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
+ "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==",
+ "license": "MIT"
+ },
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@@ -6839,6 +6846,18 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
+ "node_modules/chart.js": {
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz",
+ "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
diff --git a/package.json b/package.json
index 23652f4..690f164 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "WorkTime",
- "version": "0.1.3",
+ "version": "0.2.1",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
@@ -28,6 +28,7 @@
"@capacitor/keyboard": "6.0.2",
"@capacitor/status-bar": "6.0.1",
"@ionic/angular": "^8.0.0",
+ "chart.js": "^4.4.6",
"ionicons": "^7.2.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
diff --git a/src/app/analysis/analysis.page.html b/src/app/analysis/analysis.page.html
index 38b153e..6b24924 100644
--- a/src/app/analysis/analysis.page.html
+++ b/src/app/analysis/analysis.page.html
@@ -1,7 +1,7 @@
- Tab 2
+ Analyse
@@ -9,9 +9,63 @@
- Tab 2
+ Analyse
-
+
+ Tag
+
+
+
+
+
+
+
+
+
+
+
+ Noch zu arbeiten
+ (maxWorkTime + desOverTime)">Tagessoll erreicht!
+
+
+
+
+
+
+
+
+
+ Auswertung
+
+
+
+
+
+
+
+ {{formatTime(workTime)}} von {{formatTime(maxWorkTime)}} 0">(+{{formatTime(driveTime)}} Reisezeit)
+
+
+
+
+
+
+ {{formatTime(pauseTime)}} von {{formatTime(maxPauseTime)}}
+
+ maxPauseTime" value="1" color="warning" />
+
+
+
+
+
+ {{formatTime(Math.max(combinedWorkTime - maxWorkTime, 0))}} von {{formatTime(desOverTime)}} ({{formatTime(maxOverTime)}} maximal)
+
+ maxOverTime" value="1" color="danger" />
+
+
+
+
+
diff --git a/src/app/analysis/analysis.page.scss b/src/app/analysis/analysis.page.scss
index e69de29..3043f96 100644
--- a/src/app/analysis/analysis.page.scss
+++ b/src/app/analysis/analysis.page.scss
@@ -0,0 +1,19 @@
+.work-progress {
+ &::part(stream) {
+ animation: none;
+ -webkit-animation: none;
+ background-image: radial-gradient(ellipse at center, var(--background) 0%, var(--background) 30%, var(--background) 30%);
+ }
+
+ &::part(track) {
+ background-color: var(--ion-color-warning);
+ }
+}
+
+#overtime::part(track) {
+ background-color: var(--ion-color-tertiary-shade);
+}
+
+ion-card-content ion-item {
+ --background: unset;
+}
diff --git a/src/app/analysis/analysis.page.ts b/src/app/analysis/analysis.page.ts
index 6d8e213..2ea46e3 100644
--- a/src/app/analysis/analysis.page.ts
+++ b/src/app/analysis/analysis.page.ts
@@ -1,16 +1,194 @@
-import { Component } from '@angular/core';
-import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular/standalone';
+import {Component, ElementRef, ViewChild} from '@angular/core';
+import {
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent,
+ IonItem,
+ IonLabel,
+ IonDatetimeButton,
+ IonModal,
+ IonDatetime,
+ IonCard,
+ IonCardHeader,
+ IonCardTitle,
+ IonCardContent,
+ IonCardSubtitle, IonList, IonIcon, IonProgressBar
+} from '@ionic/angular/standalone';
import { ExploreContainerComponent } from '../explore-container/explore-container.component';
+import {FormsModule} from "@angular/forms";
+import {TimeEntry} from "../../models/timeEntry";
+import {TimeService} from "../../services/time.service";
+import {Chart} from "chart.js/auto";
+import {addIcons} from "ionicons";
+import {briefcase, card, pizza} from "ionicons/icons";
+import {NgIf} from "@angular/common";
@Component({
selector: 'app-tab2',
templateUrl: 'analysis.page.html',
styleUrls: ['analysis.page.scss'],
standalone: true,
- imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent]
+ imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent, IonItem, IonLabel, IonDatetimeButton, IonModal, IonDatetime, FormsModule, IonCard, IonCardHeader, IonCardTitle, IonCardContent, IonCardSubtitle, IonList, IonIcon, IonProgressBar, NgIf]
})
export class AnalysisPage {
+ public currentDate: any;
+ public timeData: TimeEntry[] = [];
- constructor() {}
+ public workTime: number = 0;
+ public pauseTime: number = 0;
+ public driveTime: number = 0;
+ public combinedWorkTime: number = 0;
+ public maxWorkTime: number = 420;
+ public maxPauseTime: number = 30;
+ public maxOverTime: number = 60;
+ public desOverTime: number = 30;
+
+ // @ts-ignore
+ @ViewChild('chart') chartRef: ElementRef;
+ private chart: any;
+
+ constructor(private time: TimeService) {
+ addIcons({briefcase, pizza, card})
+ }
+
+ ionViewDidEnter() {
+ this.updateCurrentData();
+ }
+
+ public updateCurrentData() {
+ this.timeData = this.time.getEntries(this.currentDate);
+ this.workTime = 0;
+ this.pauseTime = 0;
+ this.driveTime = 0;
+ this.combinedWorkTime = 0;
+
+ if (this.timeData.length < 2) {
+ this.showEmptyChart();
+ return;
+ }
+
+ for (let i = 1; i < this.timeData.length; i++) {
+ const start = this.timeData[i - 1];
+ const end = this.timeData[i];
+ const diff = this.time.calculateTimespanInMinutes(start, end);
+
+ if (start.type == 'start-drive' && end.type == 'end-drive') {
+ this.driveTime += diff;
+ }
+ else if (start.type === 'login') {
+ this.workTime += diff;
+ }
+ else {
+ this.pauseTime += diff;
+ }
+ }
+ this.combinedWorkTime = this.workTime + this.driveTime;
+
+ if (this.combinedWorkTime < 360) {
+ this.maxPauseTime = 0;
+ }
+ if (this.combinedWorkTime >= 360) { // 6h
+ this.maxPauseTime = 30;
+ }
+ if (this.combinedWorkTime >= 540) { // 9h
+ this.maxPauseTime = 45;
+ }
+
+ this.updateChart();
+ }
+
+ public updateChart() {
+ const style = getComputedStyle(document.body);
+ const textColor = style.getPropertyValue('--ion-text-color');
+ const workColor = style.getPropertyValue('--ion-color-primary');
+ const driveColor = style.getPropertyValue('--ion-color-secondary');
+ const remainColor = style.getPropertyValue('--ion-card-background');
+ const overColor = style.getPropertyValue('--ion-color-tertiary');
+ const overColorWarn = style.getPropertyValue('--ion-color-warning');
+
+ let overData: number[] = [];
+ let overLabels: string[] = [];
+ let overColors: string[] = [];
+
+ if (this.combinedWorkTime > this.maxWorkTime) {
+ const overTime = this.combinedWorkTime - this.maxWorkTime;
+ const overPercentage = overTime / this.desOverTime;
+
+ overData.push(this.combinedWorkTime * overPercentage);
+ overLabels.push('Überstunden');
+ overColors.push(overPercentage > 1 ? overColorWarn : overColor);
+ }
+
+ this.chart?.destroy();
+ Chart.defaults.color = textColor;
+ this.chart = new Chart(this.chartRef.nativeElement, {
+ type: 'doughnut',
+ data: {
+ labels: [
+ ...overLabels,
+ 'Arbeitszeit',
+ 'Dienstreise'
+ ],
+ datasets: [{
+ label: 'Zeit',
+ data: [...overData, this.workTime, this.driveTime, Math.max(this.maxWorkTime - this.combinedWorkTime, 0)],
+ backgroundColor: [
+ ...overColors,
+ workColor,
+ driveColor,
+ remainColor
+ ],
+ }]
+ },
+ options: {
+ events: [],
+ plugins: {
+ legend: {
+ display: this.driveTime > 0 || this.combinedWorkTime > this.maxWorkTime
+ }
+ }
+ }
+ });
+ }
+
+ public showEmptyChart() {
+ const style = getComputedStyle(document.body);
+ const remainColor = style.getPropertyValue('--ion-card-background');
+
+ this.chart?.destroy();
+ this.chart = new Chart(this.chartRef.nativeElement, {
+ type: 'doughnut',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'Zeit',
+ data: [100],
+ backgroundColor: [
+ remainColor
+ ],
+ }]
+ },
+ options: {
+ events: [],
+ plugins: {
+ legend: {
+ display: this.driveTime > 0 || this.combinedWorkTime > this.maxWorkTime
+ }
+ }
+ }
+ });
+ }
+
+ public formatTime(time: number): string {
+ const hours = Math.floor(time / 60);
+ const minutes = time % 60;
+
+ let result = hours < 10 ? "0" + hours + ":" : hours.toString() + ":";
+ result += minutes < 10 ? "0" + minutes : minutes.toString();
+ return result;
+ }
+
+ protected readonly Math = Math;
}
diff --git a/src/app/tabs/tabs.page.html b/src/app/tabs/tabs.page.html
index 595eaf1..4fa7ca3 100644
--- a/src/app/tabs/tabs.page.html
+++ b/src/app/tabs/tabs.page.html
@@ -5,7 +5,7 @@
Erfassen
-
+
Analyse
diff --git a/src/app/time/time.page.ts b/src/app/time/time.page.ts
index b1f883f..75ffdf0 100644
--- a/src/app/time/time.page.ts
+++ b/src/app/time/time.page.ts
@@ -15,6 +15,7 @@ import {NgClass, NgForOf, NgIf} from "@angular/common";
import {addIcons} from "ionicons";
import {add} from "ionicons/icons";
import {FormsModule} from "@angular/forms";
+import {TimeService} from "../../services/time.service";
@Component({
selector: 'app-tab1',
@@ -33,22 +34,15 @@ export class TimePage {
public modalDate: any;
public currentDate: any;
- constructor() {
- const savedData = localStorage.getItem("time-data");
+ constructor(private timeService: TimeService) {
+ this.data = timeService.loadEntries();
- if (savedData != null) {
- this.data = JSON.parse(savedData as string);
-
- for (let entry of this.data) {
- entry.registeredAt = new Date(entry.registeredAt);
- entry.registeredAt.toLocaleTimeString();
-
- this.shouldAnimate.push(false);
- }
-
- this.updateCurrentAction();
+ for (let i = 0; i < this.data.length; i++) {
+ this.shouldAnimate.push(false);
}
+ this.updateCurrentAction();
+
addIcons({add});
}
@@ -80,18 +74,15 @@ export class TimePage {
text = "Dienstreise ";
}
- return text + `(${this.calculateTimespan(entry1.registeredAt, entry2.registeredAt)})`;
+ return text + `(${this.calculateTimespan(entry1, entry2)})`;
}
- private calculateTimespan(start: Date, end: Date): string {
- const startSeconds: number = (start.getHours() * 3600) + (start.getMinutes() * 60) + start.getSeconds();
- const endSeconds: number = (end.getHours() * 3600) + (end.getMinutes() * 60) + end.getSeconds();
+ private calculateTimespan(start: TimeEntry, end: TimeEntry): string {
+ const time = this.timeService.calculateTimespanInMinutes(start, end);
+ const hours = Math.floor(time / 60);
+ const minutes = time % 60;
- const difference = endSeconds - startSeconds;
- const diffHours = Math.floor(difference / 3600.00);
- const diffMinutes = Math.floor((difference % 3600) / 60.00);
-
- return this.formatEntry(diffHours, diffMinutes);
+ return this.formatEntry(hours, minutes);
}
public formatEntry(hours: number, minutes: number): string {
@@ -165,7 +156,7 @@ export class TimePage {
}
private saveData(): void {
- localStorage.setItem("time-data", JSON.stringify(this.data));
+ this.timeService.saveEntries(this.data);
}
public openModal(): void {
diff --git a/src/services/time.service.ts b/src/services/time.service.ts
new file mode 100644
index 0000000..5b24684
--- /dev/null
+++ b/src/services/time.service.ts
@@ -0,0 +1,46 @@
+import { Injectable } from '@angular/core';
+import {TimeEntry} from "../models/timeEntry";
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TimeService {
+
+ constructor() { }
+
+ public calculateTimespanInMinutes(start: TimeEntry, end: TimeEntry): number {
+ const startSeconds: number = (start.registeredAt.getHours() * 3600) + (start.registeredAt.getMinutes() * 60) + start.registeredAt.getSeconds();
+ const endSeconds: number = (end.registeredAt.getHours() * 3600) + (end.registeredAt.getMinutes() * 60) + end.registeredAt.getSeconds();
+
+ const difference = endSeconds - startSeconds;
+ const diffHours = Math.floor(difference / 3600.00);
+ const diffMinutes = Math.floor((difference % 3600) / 60.00);
+
+ return diffMinutes + (diffHours * 60);
+ }
+
+ public getEntries(day: any): TimeEntry[] {
+ const today = new Date(day || Date.now()).toLocaleDateString();
+ return this.loadEntries().filter(entry => entry.registeredAt.toLocaleDateString() === today);
+ }
+
+ public loadEntries(): TimeEntry[] {
+ const savedData = localStorage.getItem("time-data");
+
+ if (savedData != null) {
+ const data = JSON.parse(savedData as string) as TimeEntry[];
+
+ for (let entry of data) {
+ entry.registeredAt = new Date(entry.registeredAt);
+ }
+
+ return data;
+ }
+
+ return [];
+ }
+
+ public saveEntries(entries: TimeEntry[]): void {
+ localStorage.setItem("time-data", JSON.stringify(entries));
+ }
+}