changed graph appearance + QOL improvements
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "WorkTime",
|
"name": "WorkTime",
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"author": "Ionic Framework",
|
"author": "Ionic Framework",
|
||||||
"homepage": "https://ionicframework.com/",
|
"homepage": "https://ionicframework.com/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -27,7 +27,10 @@
|
|||||||
<ion-card>
|
<ion-card>
|
||||||
<ion-card-header>
|
<ion-card-header>
|
||||||
<ion-card-title>Noch zu arbeiten</ion-card-title>
|
<ion-card-title>Noch zu arbeiten</ion-card-title>
|
||||||
<ion-card-subtitle *ngIf="combinedWorkTime > (settings.maxWorkTime + settings.desiredOverTime)">Tagessoll erreicht!</ion-card-subtitle>
|
<ion-card-subtitle *ngIf="combinedWorkTime < settings.maxWorkTime">Noch {{formatTime(settings.maxWorkTime - combinedWorkTime)}} Stunden</ion-card-subtitle>
|
||||||
|
<ion-card-subtitle *ngIf="combinedWorkTime >= settings.maxWorkTime && combinedWorkTime < (settings.maxWorkTime + settings.desiredOverTime)">Arbeitszeit erreicht!</ion-card-subtitle>
|
||||||
|
<ion-card-subtitle *ngIf="combinedWorkTime >= (settings.maxWorkTime + settings.desiredOverTime) && combinedWorkTime <= (settings.maxWorkTime + settings.maxOverTime)">Tagessoll erreicht!</ion-card-subtitle>
|
||||||
|
<ion-card-subtitle *ngIf="combinedWorkTime > settings.maxWorkTime + settings.maxOverTime">Tageslimit erreicht!</ion-card-subtitle>
|
||||||
</ion-card-header>
|
</ion-card-header>
|
||||||
|
|
||||||
<ion-card-content>
|
<ion-card-content>
|
||||||
@@ -63,7 +66,7 @@
|
|||||||
{{formatTime(Math.max(combinedWorkTime - settings.maxWorkTime, 0))}} von {{formatTime(settings.desiredOverTime)}} ({{formatTime(settings.maxOverTime)}} maximal)
|
{{formatTime(Math.max(combinedWorkTime - settings.maxWorkTime, 0))}} von {{formatTime(settings.desiredOverTime)}} ({{formatTime(settings.maxOverTime)}} maximal)
|
||||||
<ion-progress-bar
|
<ion-progress-bar
|
||||||
*ngIf="combinedWorkTime - settings.maxWorkTime <= settings.maxOverTime"
|
*ngIf="combinedWorkTime - settings.maxWorkTime <= settings.maxOverTime"
|
||||||
class="work-progress" id="overtime"
|
class="work-progress" id="overtime" [ngClass]="{'progress-warn': combinedWorkTime > settings.maxWorkTime + settings.desiredOverTime}"
|
||||||
[value]="(combinedWorkTime - settings.maxWorkTime) / settings.maxOverTime"
|
[value]="(combinedWorkTime - settings.maxWorkTime) / settings.maxOverTime"
|
||||||
[buffer]="settings.desiredOverTime / settings.maxOverTime"
|
[buffer]="settings.desiredOverTime / settings.maxOverTime"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
background-image: radial-gradient(ellipse at center, var(--background) 0%, var(--background) 30%, var(--background) 30%);
|
background-image: radial-gradient(ellipse at center, var(--background) 0%, var(--background) 30%, var(--background) 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::part(track) {
|
&::part(track), &.progress-warn::part(progress) {
|
||||||
background-color: var(--ion-color-warning);
|
background-color: var(--ion-color-warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {TimeService} from "../../services/time.service";
|
|||||||
import {Chart} from "chart.js/auto";
|
import {Chart} from "chart.js/auto";
|
||||||
import {addIcons} from "ionicons";
|
import {addIcons} from "ionicons";
|
||||||
import {briefcase, card, pizza} from "ionicons/icons";
|
import {briefcase, card, pizza} from "ionicons/icons";
|
||||||
import {NgIf} from "@angular/common";
|
import {NgClass, NgIf} from "@angular/common";
|
||||||
import {SettingsService} from "../../services/settings.service";
|
import {SettingsService} from "../../services/settings.service";
|
||||||
import {Settings} from "../../models/settings";
|
import {Settings} from "../../models/settings";
|
||||||
import {AppComponent} from "../app.component";
|
import {AppComponent} from "../app.component";
|
||||||
@@ -31,7 +31,7 @@ import {AppComponent} from "../app.component";
|
|||||||
templateUrl: 'analysis.page.html',
|
templateUrl: 'analysis.page.html',
|
||||||
styleUrls: ['analysis.page.scss'],
|
styleUrls: ['analysis.page.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonItem, IonLabel, IonDatetimeButton, IonModal, IonDatetime, FormsModule, IonCard, IonCardHeader, IonCardTitle, IonCardContent, IonCardSubtitle, IonList, IonIcon, IonProgressBar, NgIf]
|
imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonItem, IonLabel, IonDatetimeButton, IonModal, IonDatetime, FormsModule, IonCard, IonCardHeader, IonCardTitle, IonCardContent, IonCardSubtitle, IonList, IonIcon, IonProgressBar, NgIf, NgClass]
|
||||||
})
|
})
|
||||||
export class AnalysisPage {
|
export class AnalysisPage {
|
||||||
public currentDate: any;
|
public currentDate: any;
|
||||||
@@ -51,7 +51,7 @@ export class AnalysisPage {
|
|||||||
|
|
||||||
constructor(private time: TimeService, private settingsProvider: SettingsService) {
|
constructor(private time: TimeService, private settingsProvider: SettingsService) {
|
||||||
this.settings = this.settingsProvider.loadSettings();
|
this.settings = this.settingsProvider.loadSettings();
|
||||||
addIcons({briefcase, pizza, card})
|
addIcons({briefcase, pizza, card});
|
||||||
}
|
}
|
||||||
|
|
||||||
ionViewDidEnter() {
|
ionViewDidEnter() {
|
||||||
@@ -72,11 +72,6 @@ export class AnalysisPage {
|
|||||||
this.driveTime = 0;
|
this.driveTime = 0;
|
||||||
this.combinedWorkTime = 0;
|
this.combinedWorkTime = 0;
|
||||||
|
|
||||||
if (this.timeData.length == 0) {
|
|
||||||
this.showEmptyChart();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.timeData.length >= 1 && this.time.isToday(this.currentDate)) {
|
if (this.timeData.length >= 1 && this.time.isToday(this.currentDate)) {
|
||||||
const lastEntry = this.timeData[this.timeData.length - 1];
|
const lastEntry = this.timeData[this.timeData.length - 1];
|
||||||
const diff = this.time.calculateTimespanInMinutes(lastEntry, {
|
const diff = this.time.calculateTimespanInMinutes(lastEntry, {
|
||||||
@@ -98,7 +93,7 @@ export class AnalysisPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.timeData.length > 2) {
|
if (this.timeData.length >= 2) {
|
||||||
for (let i = 1; i < this.timeData.length; i++) {
|
for (let i = 1; i < this.timeData.length; i++) {
|
||||||
const start = this.timeData[i - 1];
|
const start = this.timeData[i - 1];
|
||||||
const end = this.timeData[i];
|
const end = this.timeData[i];
|
||||||
@@ -135,22 +130,34 @@ export class AnalysisPage {
|
|||||||
const style = getComputedStyle(document.body);
|
const style = getComputedStyle(document.body);
|
||||||
const textColor = style.getPropertyValue('--ion-text-color');
|
const textColor = style.getPropertyValue('--ion-text-color');
|
||||||
const workColor = style.getPropertyValue('--ion-color-primary');
|
const workColor = style.getPropertyValue('--ion-color-primary');
|
||||||
const driveColor = style.getPropertyValue('--ion-color-secondary');
|
|
||||||
const remainColor = style.getPropertyValue('--ion-card-background');
|
const remainColor = style.getPropertyValue('--ion-card-background');
|
||||||
const overColor = style.getPropertyValue('--ion-color-tertiary');
|
const overColor = style.getPropertyValue('--ion-color-tertiary');
|
||||||
const overColorWarn = style.getPropertyValue('--ion-color-warning');
|
const overColorWarn = style.getPropertyValue('--ion-color-warning');
|
||||||
|
const overColorDanger = style.getPropertyValue('--ion-color-danger');
|
||||||
|
|
||||||
let overData: number[] = [];
|
let chartData: number[] = [];
|
||||||
let overLabels: string[] = [];
|
let chartLabels: string[] = [];
|
||||||
let overColors: string[] = [];
|
let chartColors: string[] = [];
|
||||||
|
|
||||||
if (this.combinedWorkTime > this.settings.maxWorkTime) {
|
if (this.combinedWorkTime > (this.settings.maxWorkTime + this.settings.maxOverTime)) {
|
||||||
|
chartData.push(1);
|
||||||
|
chartColors.push(overColorDanger);
|
||||||
|
}
|
||||||
|
else if (this.combinedWorkTime > this.settings.maxWorkTime) {
|
||||||
const overTime = this.combinedWorkTime - this.settings.maxWorkTime;
|
const overTime = this.combinedWorkTime - this.settings.maxWorkTime;
|
||||||
const overPercentage = overTime / this.settings.desiredOverTime;
|
const overPercentage = (overTime / this.settings.desiredOverTime) * 0.5;
|
||||||
|
|
||||||
overData.push(this.combinedWorkTime * overPercentage);
|
chartData.push(overPercentage, 1 - overPercentage);
|
||||||
overLabels.push('Überstunden');
|
chartLabels.push('Überstunden', 'Arbeitszeit');
|
||||||
overColors.push(overPercentage > 1 ? overColorWarn : overColor);
|
chartColors.push(overPercentage > 0.5 ? overColorWarn : overColor, workColor);
|
||||||
|
}
|
||||||
|
else if (this.combinedWorkTime == 0) {
|
||||||
|
chartData.push(1);
|
||||||
|
chartColors.push(remainColor);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
chartData.push(this.workTime, Math.max(this.settings.maxWorkTime - this.combinedWorkTime, 0));
|
||||||
|
chartColors.push(workColor, remainColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chart?.destroy();
|
this.chart?.destroy();
|
||||||
@@ -158,55 +165,17 @@ export class AnalysisPage {
|
|||||||
this.chart = new Chart(this.chartRef.nativeElement, {
|
this.chart = new Chart(this.chartRef.nativeElement, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: {
|
data: {
|
||||||
labels: [
|
labels: chartLabels,
|
||||||
...overLabels,
|
|
||||||
'Arbeitszeit',
|
|
||||||
'Dienstreise'
|
|
||||||
],
|
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Zeit',
|
data: chartData,
|
||||||
data: [...overData, this.workTime, this.driveTime, Math.max(this.settings.maxWorkTime - this.combinedWorkTime, 0)],
|
backgroundColor: chartColors
|
||||||
backgroundColor: [
|
|
||||||
...overColors,
|
|
||||||
workColor,
|
|
||||||
driveColor,
|
|
||||||
remainColor
|
|
||||||
],
|
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
events: [],
|
events: [],
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: this.driveTime > 0 || this.combinedWorkTime > this.settings.maxWorkTime
|
display: false
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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.settings.maxWorkTime
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,62 +13,69 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-list>
|
<ion-card>
|
||||||
<!--<ion-item>
|
<ion-card-content>
|
||||||
<ion-toggle [(ngModel)]="input.notifications">Benachrichtigungen</ion-toggle>
|
<ion-list>
|
||||||
</ion-item>
|
<!--<ion-item>
|
||||||
<ion-item-divider/>-->
|
<ion-toggle [(ngModel)]="input.notifications">Benachrichtigungen</ion-toggle>
|
||||||
<ion-item>
|
</ion-item>
|
||||||
<ion-label><h1>Zeiten</h1></ion-label>
|
<ion-item-divider/>-->
|
||||||
</ion-item>
|
<ion-item>
|
||||||
<ion-item>
|
<ion-icon aria-hidden="true" name="briefcase"></ion-icon>
|
||||||
<ion-label slot="start">Arbeitszeit</ion-label>
|
<ion-label><h1>Zeiten</h1></ion-label>
|
||||||
<ion-input type="number" fill="solid" [placeholder]="settings.maxWorkTime / 60" [(ngModel)]="input.maxWorkTime" />
|
</ion-item>
|
||||||
<ion-label slot="end">Stunden</ion-label>
|
<ion-item>
|
||||||
</ion-item>
|
<ion-label slot="start">Arbeitszeit</ion-label>
|
||||||
<ion-item-divider/>
|
<ion-input type="number" fill="solid" [placeholder]="settings.maxWorkTime / 60" [(ngModel)]="input.maxWorkTime" />
|
||||||
<ion-item>
|
<ion-label slot="end">Stunden</ion-label>
|
||||||
<ion-label><h1>Pause</h1></ion-label>
|
</ion-item>
|
||||||
</ion-item>
|
<ion-item-divider/>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label slot="start">Nach 0 Stunden</ion-label>
|
<ion-icon aria-hidden="true" name="pizza"></ion-icon>
|
||||||
<ion-input type="number" fill="solid" [placeholder]="settings.defaultPauseTime" [(ngModel)]="input.defaultPauseTime" />
|
<ion-label><h1>Pause</h1></ion-label>
|
||||||
<ion-label slot="end">Minuten</ion-label>
|
</ion-item>
|
||||||
</ion-item>
|
<ion-item>
|
||||||
<ion-item>
|
<ion-label slot="start">Nach 0 Stunden</ion-label>
|
||||||
<ion-label slot="start">Nach 6 Stunden</ion-label>
|
<ion-input type="number" fill="solid" [placeholder]="settings.defaultPauseTime" [(ngModel)]="input.defaultPauseTime" />
|
||||||
<ion-input type="number" fill="solid" [placeholder]="settings.pauseAfter6" [(ngModel)]="input.pauseAfter6" />
|
<ion-label slot="end">Minuten</ion-label>
|
||||||
<ion-label slot="end">Minuten</ion-label>
|
</ion-item>
|
||||||
</ion-item>
|
<ion-item>
|
||||||
<ion-item>
|
<ion-label slot="start">Nach 6 Stunden</ion-label>
|
||||||
<ion-label slot="start">Nach 9 Stunden</ion-label>
|
<ion-input type="number" fill="solid" [placeholder]="settings.pauseAfter6" [(ngModel)]="input.pauseAfter6" />
|
||||||
<ion-input type="number" fill="solid" [placeholder]="settings.pauseAfter9" [(ngModel)]="input.pauseAfter9" />
|
<ion-label slot="end">Minuten</ion-label>
|
||||||
<ion-label slot="end">Minuten</ion-label>
|
</ion-item>
|
||||||
</ion-item>
|
<ion-item>
|
||||||
<ion-item>
|
<ion-label slot="start">Nach 9 Stunden</ion-label>
|
||||||
<ion-label slot="start">Nicht tracken nach</ion-label>
|
<ion-input type="number" fill="solid" [placeholder]="settings.pauseAfter9" [(ngModel)]="input.pauseAfter9" />
|
||||||
<ion-input type="number" fill="solid" [placeholder]="settings.dontTrackPauseAfter" [(ngModel)]="input.dontTrackPauseAfter" />
|
<ion-label slot="end">Minuten</ion-label>
|
||||||
<ion-label slot="end">Uhr</ion-label>
|
</ion-item>
|
||||||
</ion-item>
|
<ion-item>
|
||||||
<ion-item-divider/>
|
<ion-label slot="start">Nicht tracken nach</ion-label>
|
||||||
<ion-item>
|
<ion-input type="number" fill="solid" [placeholder]="settings.dontTrackPauseAfter" [(ngModel)]="input.dontTrackPauseAfter" />
|
||||||
<ion-label><h1>Überstunden</h1></ion-label>
|
<ion-label slot="end">Uhr</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item-divider/>
|
||||||
<ion-label slot="start">Optimal</ion-label>
|
<ion-item>
|
||||||
<ion-input type="number" fill="solid" [placeholder]="settings.desiredOverTime" [(ngModel)]="input.desiredOverTime" />
|
<ion-icon aria-hidden="true" name="card"></ion-icon>
|
||||||
<ion-label slot="end">Minuten</ion-label>
|
<ion-label><h1>Überstunden</h1></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label slot="start">Maximal</ion-label>
|
<ion-label slot="start">Optimal</ion-label>
|
||||||
<ion-input type="number" fill="solid" [placeholder]="settings.maxOverTime" [(ngModel)]="input.maxOverTime" />
|
<ion-input type="number" fill="solid" [placeholder]="settings.desiredOverTime" [(ngModel)]="input.desiredOverTime" />
|
||||||
<ion-label slot="end">Minuten</ion-label>
|
<ion-label slot="end">Minuten</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item-divider/>
|
<ion-item>
|
||||||
<ion-item>
|
<ion-label slot="start">Maximal</ion-label>
|
||||||
<ion-col size="12" class="ion-text-center">
|
<ion-input type="number" fill="solid" [placeholder]="settings.maxOverTime" [(ngModel)]="input.maxOverTime" />
|
||||||
<ion-button size="normal" (click)="save()">Speichern</ion-button>
|
<ion-label slot="end">Minuten</ion-label>
|
||||||
</ion-col>
|
</ion-item>
|
||||||
</ion-item>
|
<ion-item-divider/>
|
||||||
</ion-list>
|
<ion-item>
|
||||||
|
<ion-col size="12" class="ion-text-center">
|
||||||
|
<ion-button size="normal" (click)="save()">Speichern</ion-button>
|
||||||
|
</ion-col>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-card-content>
|
||||||
|
</ion-card>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
ion-card-content {
|
||||||
|
ion-item, ion-item-divider {
|
||||||
|
--background: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ion-item-divider {
|
ion-item-divider {
|
||||||
--background: transparent;
|
--background: transparent;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@@ -10,3 +16,13 @@ ion-item {
|
|||||||
ion-label {
|
ion-label {
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
transform: translateY(-0.15rem);
|
||||||
|
margin-right: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,20 +7,30 @@ import {
|
|||||||
IonList,
|
IonList,
|
||||||
IonItem,
|
IonItem,
|
||||||
IonInput,
|
IonInput,
|
||||||
IonToggle, IonButton, IonLabel, IonItemDivider, IonCol, ToastController
|
IonToggle,
|
||||||
|
IonButton,
|
||||||
|
IonLabel,
|
||||||
|
IonItemDivider,
|
||||||
|
IonCol,
|
||||||
|
ToastController,
|
||||||
|
IonIcon,
|
||||||
|
IonCard,
|
||||||
|
IonCardHeader,
|
||||||
|
IonCardTitle,
|
||||||
|
IonCardContent
|
||||||
} from '@ionic/angular/standalone';
|
} from '@ionic/angular/standalone';
|
||||||
import {Settings} from "../../models/settings";
|
import {Settings} from "../../models/settings";
|
||||||
import {SettingsService} from "../../services/settings.service";
|
import {SettingsService} from "../../services/settings.service";
|
||||||
import {FormsModule} from "@angular/forms";
|
import {FormsModule} from "@angular/forms";
|
||||||
import {addIcons} from "ionicons";
|
import {addIcons} from "ionicons";
|
||||||
import { save } from 'ionicons/icons';
|
import {briefcase, card, pizza, save} from 'ionicons/icons';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tab3',
|
selector: 'app-tab3',
|
||||||
templateUrl: 'settings.page.html',
|
templateUrl: 'settings.page.html',
|
||||||
styleUrls: ['settings.page.scss'],
|
styleUrls: ['settings.page.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonInput, FormsModule, IonToggle, IonButton, IonLabel, IonItemDivider, IonCol],
|
imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonInput, FormsModule, IonToggle, IonButton, IonLabel, IonItemDivider, IonCol, IonIcon, IonCard, IonCardHeader, IonCardTitle, IonCardContent],
|
||||||
})
|
})
|
||||||
export class SettingsPage {
|
export class SettingsPage {
|
||||||
public settings: Settings;
|
public settings: Settings;
|
||||||
@@ -30,7 +40,7 @@ export class SettingsPage {
|
|||||||
this.settings = settingsProvider.loadSettings();
|
this.settings = settingsProvider.loadSettings();
|
||||||
this.input.notifications = this.settings.notifications;
|
this.input.notifications = this.settings.notifications;
|
||||||
|
|
||||||
addIcons({save});
|
addIcons({save, briefcase, pizza, card});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async save() {
|
public async save() {
|
||||||
|
|||||||
Reference in New Issue
Block a user