finished time recording

This commit is contained in:
2024-08-30 21:09:12 +02:00
parent 1e4b267259
commit 53eb287b0a
24 changed files with 352 additions and 154 deletions

View File

@@ -4,12 +4,12 @@ import { ExploreContainerComponent } from '../explore-container/explore-containe
@Component({
selector: 'app-tab2',
templateUrl: 'tab2.page.html',
styleUrls: ['tab2.page.scss'],
templateUrl: 'analysis.page.html',
styleUrls: ['analysis.page.scss'],
standalone: true,
imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent]
})
export class Tab2Page {
export class AnalysisPage {
constructor() {}

View File

@@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
it('should create the app', async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
providers: [provideRouter([])]
}).compileComponents();
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

View File

@@ -4,11 +4,11 @@ import { ExploreContainerComponent } from '../explore-container/explore-containe
@Component({
selector: 'app-tab3',
templateUrl: 'tab3.page.html',
styleUrls: ['tab3.page.scss'],
templateUrl: 'settings.page.html',
styleUrls: ['settings.page.scss'],
standalone: true,
imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent],
})
export class Tab3Page {
export class SettingsPage {
constructor() {}
}

View File

@@ -1,17 +0,0 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Tab 1
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tab 1</ion-title>
</ion-toolbar>
</ion-header>
<app-explore-container name="Tab 1 page"></app-explore-container>
</ion-content>

View File

@@ -1,18 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Tab1Page } from './tab1.page';
describe('Tab1Page', () => {
let component: Tab1Page;
let fixture: ComponentFixture<Tab1Page>;
beforeEach(async () => {
fixture = TestBed.createComponent(Tab1Page);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,14 +0,0 @@
import { Component } from '@angular/core';
import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular/standalone';
import { ExploreContainerComponent } from '../explore-container/explore-container.component';
@Component({
selector: 'app-tab1',
templateUrl: 'tab1.page.html',
styleUrls: ['tab1.page.scss'],
standalone: true,
imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent],
})
export class Tab1Page {
constructor() {}
}

View File

@@ -1,18 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Tab2Page } from './tab2.page';
describe('Tab2Page', () => {
let component: Tab2Page;
let fixture: ComponentFixture<Tab2Page>;
beforeEach(async () => {
fixture = TestBed.createComponent(Tab2Page);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,18 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Tab3Page } from './tab3.page';
describe('Tab3Page', () => {
let component: Tab3Page;
let fixture: ComponentFixture<Tab3Page>;
beforeEach(async () => {
fixture = TestBed.createComponent(Tab3Page);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,18 +1,18 @@
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab1" href="/tabs/tab1">
<ion-icon aria-hidden="true" name="triangle"></ion-icon>
<ion-label>Tab 1</ion-label>
<ion-tab-button tab="time" href="/time">
<ion-icon aria-hidden="true" name="time"></ion-icon>
<ion-label>Erfassen</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab2" href="/tabs/tab2">
<ion-icon aria-hidden="true" name="ellipse"></ion-icon>
<ion-label>Tab 2</ion-label>
<ion-tab-button tab="analysis" href="/analysis">
<ion-icon aria-hidden="true" name="pie-chart"></ion-icon>
<ion-label>Analyse</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab3" href="/tabs/tab3">
<ion-icon aria-hidden="true" name="square"></ion-icon>
<ion-label>Tab 3</ion-label>
<ion-tab-button tab="settings" href="/settings">
<ion-icon aria-hidden="true" name="settings"></ion-icon>
<ion-label>Einstellungen</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>

View File

@@ -1,26 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { TabsPage } from './tabs.page';
describe('TabsPage', () => {
let component: TabsPage;
let fixture: ComponentFixture<TabsPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TabsPage],
providers: [provideRouter([])]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TabsPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,7 +1,7 @@
import { Component, EnvironmentInjector, inject } from '@angular/core';
import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { triangle, ellipse, square } from 'ionicons/icons';
import { time, pieChart, settings } from 'ionicons/icons';
@Component({
selector: 'app-tabs',
@@ -14,6 +14,6 @@ export class TabsPage {
public environmentInjector = inject(EnvironmentInjector);
constructor() {
addIcons({ triangle, ellipse, square });
addIcons({ time, pieChart, settings });
}
}

View File

@@ -7,19 +7,19 @@ export const routes: Routes = [
component: TabsPage,
children: [
{
path: 'tab1',
path: 'time',
loadComponent: () =>
import('../tab1/tab1.page').then((m) => m.Tab1Page),
import('../time/time.page').then((m) => m.TimePage),
},
{
path: 'tab2',
path: 'analysis',
loadComponent: () =>
import('../tab2/tab2.page').then((m) => m.Tab2Page),
import('../analysis/analysis.page').then((m) => m.AnalysisPage),
},
{
path: 'tab3',
path: 'settings',
loadComponent: () =>
import('../tab3/tab3.page').then((m) => m.Tab3Page),
import('../settings/settings.page').then((m) => m.SettingsPage),
},
{
path: '',
@@ -30,7 +30,7 @@ export const routes: Routes = [
},
{
path: '',
redirectTo: '/tabs/tab1',
redirectTo: '/tabs/time',
pathMatch: 'full',
},
];

View File

@@ -0,0 +1,78 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Zeiterfassung
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true" [scrollY]="false">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Zeiterfassung</ion-title>
</ion-toolbar>
</ion-header>
<ion-item>
<ion-label>Tag</ion-label>
<ion-datetime-button datetime="current-datetime"></ion-datetime-button>
<ion-modal [keepContentsMounted]="true">
<ng-template>
<ion-datetime id="current-datetime" presentation="date" [(ngModel)]="currentDate"></ion-datetime>
</ng-template>
</ion-modal>
</ion-item>
<section class="time-entries">
<ion-item class="entry" *ngFor="let entry of getEntriesOfToday(); let index = index" (click)="removeEntry(index)" [ngClass]="{'animate': shouldAnimate[index]}">
<div class="circle"></div>
<ion-label class="type">{{entry.type === 'login' ? "Eingestempelt" : "Ausgestempelt"}}</ion-label>
<span class="time">{{entry.registeredAt.toLocaleTimeString()}}</span>
<span *ngIf="index !== 0" class="between">
<span class="between-content">{{generateSeparatorText(data[index - 1], entry)}}</span>
</span>
</ion-item>
</section>
<div class="button-container">
<ion-button (click)="addEntry()">{{currentAction === 'login' ? "Einstempeln" : "Ausstempeln"}}</ion-button>
<ion-button shape="round" id="open-modal">
<ion-icon slot="icon-only" name="add"></ion-icon>
</ion-button>
</div>
<ion-modal trigger="open-modal" (willDismiss)="modal?.dismiss(null, 'cancel')" #createModal>
<ng-template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="modal?.dismiss(null, 'cancel')">Cancel</ion-button>
</ion-buttons>
<ion-title>Welcome</ion-title>
<ion-buttons slot="end">
<ion-button [strong]="true" (click)="addModalEntry()">Confirm</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label>Uhrzeit</ion-label>
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-modal [keepContentsMounted]="true">
<ng-template>
<ion-datetime id="datetime" presentation="time" [(ngModel)]="modalDate"></ion-datetime>
</ng-template>
</ion-modal>
</ion-item>
<ion-item>
<ion-select label="Stempeltyp" value="login" [(ngModel)]="modalMode">
<ion-select-option value="login">Einstempeln</ion-select-option>
<ion-select-option value="logout">Ausstempeln</ion-select-option>
</ion-select>
</ion-item>
</ion-content>
</ng-template>
</ion-modal>
</ion-content>

121
src/app/time/time.page.scss Normal file
View File

@@ -0,0 +1,121 @@
.button-container {
position: absolute;
bottom: 75px;
left: 0;
right: 0;
display: flex;
justify-content: center;
gap: 15px;
}
.time-entries {
display: flex;
flex-direction: column;
gap: 65px;
margin: 20px;
.entry {
--inner-border-width: 0 0 0 0;
position: relative;
overflow: visible;
line-height: 15px;
z-index: 0;
&.animate {
opacity: 0;
animation: fade-in 200ms ease-in-out forwards;
.between::before {
transform: scaleX(0);
animation: line-in-horizontal 200ms ease-in-out 1000ms forwards;
}
.between-content {
opacity: 0;
animation: fade-in 500ms ease-in-out 1200ms forwards;
}
.circle::after {
transform: scaleY(0);
animation: line-in 500ms ease-in-out 500ms forwards;
}
}
.between {
position: absolute;
overflow: visible;
left: 45px;
bottom: calc(100% + 25px);
&::before {
content: '';
position: absolute;
height: 2px;
width: 20px;
background-color: var(--color);
top: calc(50% - 1px);
left: -37px;
}
}
.type {
padding-left: 15px;
}
.circle {
background-color: var(--color);
border-radius: 50%;
width: 15px;
height: 15px;
position: relative;
&::after {
content: '';
position: absolute;
background-color: var(--color);
width: 2px;
height: 100px;
bottom: 100%;
left: calc(50% - 1px);
}
}
&:first-of-type .circle::after {
display: none;
}
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes line-in {
0% {
transform-origin: top;
transform: scaleY(0);
}
100% {
transform-origin: top;
transform: scaleY(1);
}
}
@keyframes line-in-horizontal {
0% {
transform-origin: left;
transform: scaleX(0);
}
100% {
transform-origin: left;
transform: scaleX(1);
}
}

118
src/app/time/time.page.ts Normal file
View File

@@ -0,0 +1,118 @@
import {Component, ViewChild} from '@angular/core';
import {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonButton,
IonList,
IonItem,
IonLabel, IonIcon, IonModal, IonButtons, IonInput, IonDatetime, IonDatetimeButton, IonSelect, IonSelectOption
} from '@ionic/angular/standalone';
import { ExploreContainerComponent } from '../explore-container/explore-container.component';
import {TimeEntry, TimeType} from "../../models/timeEntry";
import {NgClass, NgForOf, NgIf} from "@angular/common";
import {addIcons} from "ionicons";
import {add} from "ionicons/icons";
import {FormsModule} from "@angular/forms";
@Component({
selector: 'app-tab1',
templateUrl: 'time.page.html',
styleUrls: ['time.page.scss'],
standalone: true,
imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent, NgForOf, IonButton, IonList, IonItem, IonLabel, NgIf, NgClass, IonIcon, IonModal, IonButtons, IonInput, IonDatetime, IonDatetimeButton, IonSelect, IonSelectOption, FormsModule],
})
export class TimePage {
public data: TimeEntry[] = [];
public shouldAnimate: boolean[] = [];
public currentAction: TimeType = 'login';
@ViewChild('createModal') modal: IonModal | undefined;
public modalDate: any;
public modalMode: TimeType = 'login';
public currentDate: any;
constructor() {
const savedData = localStorage.getItem("time-data");
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();
}
addIcons({add});
}
public getEntriesOfToday(): TimeEntry[] {
const today = new Date(this.currentDate || Date.now()).getDay();
return this.data.filter(entry => entry.registeredAt.getDay() === today);
}
public generateSeparatorText(entry1: TimeEntry, entry2: TimeEntry): string {
const difference = +entry2.registeredAt.getTime() - +entry1.registeredAt.getTime() - 3600000;
const date = new Date(difference);
const text = entry1.type === 'login' ? "Arbeit " : "Pause ";
return text + `(${date.toLocaleTimeString()})`;
}
public addEntry(): void {
this.shouldAnimate.push(true)
setTimeout(() => this.shouldAnimate[this.shouldAnimate.length - 1] = false, 5000);
this.data.push({
registeredAt: new Date(Date.now()),
type: this.currentAction
});
this.saveData();
this.currentAction = this.currentAction === 'login' ? 'logout' : 'login';
}
public removeEntry(index: number): void {
this.shouldAnimate.splice(index, 1);
this.data.splice(index, 1);
this.saveData();
this.updateCurrentAction();
}
private updateCurrentAction(): void {
if (this.data.length == 0) {
this.currentAction = 'login';
}else {
this.currentAction = this.data[this.data.length - 1].type === 'login' ? 'logout' : 'login';
}
}
private saveData(): void {
localStorage.setItem("time-data", JSON.stringify(this.data));
}
public addModalEntry(): void {
const date = new Date(this.modalDate);
date.setSeconds(0);
this.data.push({
registeredAt: date,
type: this.modalMode
});
this.data.sort((a: TimeEntry, b: TimeEntry) => {
return a.registeredAt.getTime() - b.registeredAt.getTime();
});
this.shouldAnimate = [];
for (let i = 0; i < this.data.length; i++) {
this.shouldAnimate.push(false);
}
this.saveData();
this.modal?.dismiss(null, 'submit');
}
}

View File

@@ -1 +0,0 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>Ionic App</title>
<title>Zeiterfassung</title>
<base href="/" />

6
src/models/timeEntry.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface TimeEntry {
registeredAt: Date;
type: TimeType;
}
export type TimeType = 'login' | 'logout';