parent
5c2d03272f
commit
9cf16ab3df
@ -1,4 +1,51 @@
|
|||||||
<div>
|
<div *ngIf="latestEntry && timeTrackerService.isActive" class="d-flex flex-column">
|
||||||
Spent: {{currentSpent()}}
|
<div class="zone"><b>{{resolveZone(latestEntry.zone)}}</b></div>
|
||||||
Compare spent: {{compareSpent()}}
|
<div class="d-flex flex-row">
|
||||||
|
Spent:
|
||||||
|
<app-wrap-value [value]="{current: currentSpent(), compare: compareSpent()}">
|
||||||
|
<ng-template let-v>
|
||||||
|
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current | hms}}</span>
|
||||||
|
<span>{{v.compare | hms}}</span>
|
||||||
|
</ng-template>
|
||||||
|
</app-wrap-value>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
First Entry:
|
||||||
|
<app-wrap-value [value]="{current: currentFirst(), compare: compareFirst()}">
|
||||||
|
<ng-template let-v>
|
||||||
|
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current | hms}}</span>
|
||||||
|
<span>{{v.compare | hms}}</span>
|
||||||
|
</ng-template>
|
||||||
|
</app-wrap-value>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
Last Entry:
|
||||||
|
<app-wrap-value [value]="{current: currentLast(), compare: compareLast()}">
|
||||||
|
<ng-template let-v>
|
||||||
|
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current | hms}}</span>
|
||||||
|
<span>{{v.compare | hms}}</span>
|
||||||
|
</ng-template>
|
||||||
|
</app-wrap-value>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
Number of Entries:
|
||||||
|
<app-wrap-value [value]="{current: currentEntries(), compare: compareEntries()}">
|
||||||
|
<ng-template let-v>
|
||||||
|
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current}}</span>
|
||||||
|
<span>{{v.compare}}</span>
|
||||||
|
</ng-template>
|
||||||
|
</app-wrap-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ng-container *ngIf="!latestEntry">
|
||||||
|
Awaiting zone entry to compare data
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="latestEntry && !timeTrackerService.isActive">
|
||||||
|
Awaiting stopwatch to start
|
||||||
|
</ng-container>
|
@ -0,0 +1,9 @@
|
|||||||
|
.indeterminate {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
.good-diff {
|
||||||
|
color: rgb(46, 179, 46);
|
||||||
|
}
|
||||||
|
.bad-diff {
|
||||||
|
color: rgb(199, 13, 13);
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { hmsTimestamp } from '../_services/time-tracker.service';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'hms',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class HmsPipe implements PipeTransform {
|
||||||
|
transform(value?: number): string {
|
||||||
|
return hmsTimestamp(value);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<div class="d-flex justify-content-evenly align-items-center mb-2 mt-2">
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<button class="" mat-raised-button color="accent" (click)="openDialog()">Browse
|
||||||
|
Plans
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<button class="" mat-raised-button color="accent" (click)="loadBasePlan()">
|
||||||
|
Load base plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<button class="" mat-raised-button color="accent" (click)="loadFromUrl()">
|
||||||
|
Import from url
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="enumerated d-flex flex-column">
|
||||||
|
<mat-list role="list" class="d-flex flex-column h-100">
|
||||||
|
<cdk-virtual-scroll-viewport itemSize="10" class="h-100">
|
||||||
|
<mat-list-item class="d-flex flex-column" role="listitem" *cdkVirtualFor="let plan of plans">
|
||||||
|
<span>
|
||||||
|
<button [disabled]="disablePlans" (click)="loadPrevious(plan.stored_path!)">
|
||||||
|
<img *ngIf="plan.update_url" src="assets/public.svg">{{plan.name}}
|
||||||
|
</button><button *ngIf="plan.update_url" class="planActionButton" [disabled]="disablePlans"
|
||||||
|
(click)="checkForPlanUpdate(plan)" [title]="recentPlanTitle(plan)">
|
||||||
|
<svg-icon *ngIf="!hasUpdate(plan)" src="assets/material-sync.svg" />
|
||||||
|
<svg-icon *ngIf="hasUpdate(plan) && !isErr(plan)" src="assets/material-check.svg"
|
||||||
|
class="nice" />
|
||||||
|
<svg-icon *ngIf="hasUpdate(plan) && isErr(plan)" src="assets/material-warning.svg"
|
||||||
|
class="notnice" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</mat-list-item>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
</mat-list>
|
||||||
|
</div>
|
@ -0,0 +1,34 @@
|
|||||||
|
.planChooser {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enumerated {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: rgba(200, 200, 220, 0.15);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
|
||||||
|
mat-list {
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice {
|
||||||
|
svg {
|
||||||
|
fill: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notnice {
|
||||||
|
svg {
|
||||||
|
fill: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.planActionButton {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PlanSelectionComponent } from './plan-selection.component';
|
||||||
|
|
||||||
|
describe('PlanSelectionComponent', () => {
|
||||||
|
let component: PlanSelectionComponent;
|
||||||
|
let fixture: ComponentFixture<PlanSelectionComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [PlanSelectionComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(PlanSelectionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,143 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Plan, PlanMetadata } from 'src/app/_models/plan';
|
||||||
|
import { PlanService, UrlError } from 'src/app/_services/plan.service';
|
||||||
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
|
import { from } from 'rxjs';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||||
|
import { MatListModule } from '@angular/material/list';
|
||||||
|
import { AngularSvgIconModule } from 'angular-svg-icon';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-plan-selection',
|
||||||
|
templateUrl: './plan-selection.component.html',
|
||||||
|
styleUrls: ['./plan-selection.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MatButtonModule, ScrollingModule, MatListModule, AngularSvgIconModule]
|
||||||
|
})
|
||||||
|
export class PlanSelectionComponent {
|
||||||
|
plans: PlanMetadata[] = [];
|
||||||
|
checkingPlanUpdate: boolean = false;
|
||||||
|
|
||||||
|
recentUpdateAttempts: Map<string, Date | UrlError> = new Map<string, Date | UrlError>();
|
||||||
|
constructor(
|
||||||
|
private planService: PlanService,
|
||||||
|
) {
|
||||||
|
this.planService.getStoredPlans().subscribe(plans => {
|
||||||
|
this.plans = plans;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get disablePlans(): boolean {
|
||||||
|
return this.checkingPlanUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
openDialog() {
|
||||||
|
from(open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "JSON (.json)",
|
||||||
|
extensions: ['json']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})).subscribe(file => {
|
||||||
|
if (file) {
|
||||||
|
this.planService.loadPlanFromPath(file as string).subscribe(plan => {
|
||||||
|
if (plan) {
|
||||||
|
this.planService.setCurrentPlan(plan);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBasePlan() {
|
||||||
|
this.planService.getBasePlan().subscribe(plan => {
|
||||||
|
plan.current = 0;
|
||||||
|
this.planService.setCurrentPlan(plan);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFromUrl() {
|
||||||
|
this.planService.loadFromUrl().subscribe(plan => {
|
||||||
|
if (plan) {
|
||||||
|
this.planService.savePlanAtStore(plan.name!, plan).subscribe((path) => {
|
||||||
|
if (path) {
|
||||||
|
plan.setPath(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.planService.setCurrentPlan(plan);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPrevious(path: string) {
|
||||||
|
this.planService.loadPlanFromPath(path, false).subscribe(plan => {
|
||||||
|
this.planService.setCurrentPlan(plan);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForPlanUpdate(plan: PlanMetadata) {
|
||||||
|
this.checkingPlanUpdate = true;
|
||||||
|
|
||||||
|
this.planService.checkForPlanUpdate(plan).subscribe({
|
||||||
|
next: updatedPlan => {
|
||||||
|
console.log("check update: ", updatedPlan);
|
||||||
|
this.planService.loadPlanFromPath(plan.stored_path!, false).subscribe(oldPlan => {
|
||||||
|
console.log("oldPlan", oldPlan);
|
||||||
|
const newCurrent = this.commonUUIDToNewCurrent(oldPlan, updatedPlan);
|
||||||
|
updatedPlan.current = newCurrent;
|
||||||
|
//TODO: Interface to determine if we want to save the plan or not (allow not doing it in case user regrets... Also needs position error fix).
|
||||||
|
this.planService.savePlanAtStore(oldPlan.name!, updatedPlan, true).subscribe();
|
||||||
|
})
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.checkingPlanUpdate = false
|
||||||
|
this.recentUpdateAttempts.set(plan.update_url!, new Date());
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.recentUpdateAttempts.set(plan.update_url!, err);
|
||||||
|
if (err instanceof UrlError) {
|
||||||
|
alert("Error retrieving plan update (" + err.status + "): " + err.message);
|
||||||
|
} else if (err instanceof Error) {
|
||||||
|
alert("Unexpected error: " + err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkingPlanUpdate = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
recentPlanTitle(plan: PlanMetadata) {
|
||||||
|
if (!this.hasUpdate(plan)) return "Check for update";
|
||||||
|
if (this.hasUpdate(plan) && !this.isErr(plan)) return "Up to date";
|
||||||
|
if (this.hasUpdate(plan) && this.isErr(plan)) return "Error updating";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
isErr(plan: PlanMetadata) {
|
||||||
|
return this.recentUpdateAttempts.get(plan.update_url!) instanceof UrlError;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUpdate(plan: PlanMetadata): any {
|
||||||
|
return this.recentUpdateAttempts.has(plan.update_url!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private commonUUIDToNewCurrent(oldPlan: Plan, updatedPlan: Plan): number {
|
||||||
|
let bestOldCurrent = oldPlan.current;
|
||||||
|
let newIndex = 0;
|
||||||
|
//Attempt current plan element's UUID first, otherwise step backwards until we find a match
|
||||||
|
while (bestOldCurrent >= 0) {
|
||||||
|
const tempNewIndex = updatedPlan.plan.findIndex(e => e.uuid === oldPlan.plan[bestOldCurrent].uuid);
|
||||||
|
if (tempNewIndex !== -1) {
|
||||||
|
newIndex = tempNewIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bestOldCurrent--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newIndex;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
<button mat-raised-button color="warn" *ngIf="timeTrackerService.isActive"
|
||||||
|
(click)="timeTrackerService.stop()">Force stop stopwatch</button>
|
||||||
|
<button mat-raised-button color="warn"
|
||||||
|
*ngIf="!timeTrackerService.isActive && timeTrackerService.hasRunLoaded" (click)="timeTrackerService.startLoaded()">Start last
|
||||||
|
loaded</button>
|
@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { StopwatchControlsComponent } from './stopwatch-controls.component';
|
||||||
|
|
||||||
|
describe('StopwatchControlsComponent', () => {
|
||||||
|
let component: StopwatchControlsComponent;
|
||||||
|
let fixture: ComponentFixture<StopwatchControlsComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [StopwatchControlsComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(StopwatchControlsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,15 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { TimeTrackerService } from 'src/app/_services/time-tracker.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-stopwatch-controls',
|
||||||
|
templateUrl: './stopwatch-controls.component.html',
|
||||||
|
styleUrls: ['./stopwatch-controls.component.scss'],
|
||||||
|
imports: [CommonModule, MatButtonModule],
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class StopwatchControlsComponent {
|
||||||
|
constructor(public timeTrackerService: TimeTrackerService) { }
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
<ng-container *ngTemplateOutlet="template!; context: { $implicit: value }">
|
||||||
|
</ng-container>
|
@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { WrapValueComponent } from './wrap-value.component';
|
||||||
|
|
||||||
|
describe('WrapValueComponent', () => {
|
||||||
|
let component: WrapValueComponent;
|
||||||
|
let fixture: ComponentFixture<WrapValueComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [WrapValueComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(WrapValueComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,14 @@
|
|||||||
|
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-wrap-value',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './wrap-value.component.html',
|
||||||
|
styleUrls: ['./wrap-value.component.scss']
|
||||||
|
})
|
||||||
|
export class WrapValueComponent<T> {
|
||||||
|
@Input() value?: T;
|
||||||
|
@ContentChild(TemplateRef) template?: TemplateRef<any>;
|
||||||
|
}
|
Loading…
Reference in new issue