diff --git a/package-lock.json b/package-lock.json index 8e2cae310..9d7ea630e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1123,6 +1123,15 @@ "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.4.2.tgz", "integrity": "sha512-cDB930/7MbzaGF6U3IwSQp6XBru8xWajF5PV2YZZeV8DyiliTuld11afVztGI9+yJZ29il5E+NpGA6ooV/Cjkg==" }, + "@types/moment-duration-format": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/moment-duration-format/-/moment-duration-format-2.2.2.tgz", + "integrity": "sha512-CuYswsMI3y5uR5sD9i/VUqIbZrsYN2eaCs7nH3qpDl2CZlNI48mjMf4ve2RpQ/65irprtnQVetfnea9my+jqcg==", + "dev": true, + "requires": { + "moment": ">=2.14.0" + } + }, "@types/node": { "version": "8.9.5", "resolved": "http://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", diff --git a/package.json b/package.json index 44e788768..34b3a655a 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@ngrx/schematics": "^6.1.0", "@types/jasmine": "~2.8.6", "@types/jasminewd2": "~2.0.3", + "@types/moment-duration-format": "^2.2.2", "@types/node": "~8.9.4", "codelyzer": "~4.2.1", "dbus-native": "^0.2.5", diff --git a/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.html b/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.html index 8aed488a1..4652311c8 100644 --- a/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.html +++ b/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.html @@ -1,196 +1,207 @@ -

Time Sheet Export

+
+

Time Sheet Export

-
-
- -
-

This view allows you to export your worked time to a google sheet. You need to allow for your Google Spreadsheets to be accessed by Super Productivity. You also need to create a spreadsheet with a headings in the first row nad specify it's ID in the input field (Ho to find the id of a spreadsheet?). -

-

After successfully loading your spreadsheet a table will show up with 4 rows. The first row shows the heading you specified in the spreadsheet itself.

-

The second row is for informational purposes and shows the last row from the spreadsheet.

-

The forth row is a list of values you can directly enter save to the spreadsheet.

-

The third row is there to automatically define some values for the forth row. There are several special strings you can enter into the cells:

-
- -
-
{{ '{' }}startTime}
-
The time when you first used this app today. It's possible to round this via the options.
- -
{{ '{' }}currentTime}
-
The current time. Could be used for the for the end time of todays working day It's possible to round this via the options.
- -
{{ '{' }}date}
-
Todays date in standard format (mm/dd/yyyy)
- -
{{ '{' }}date:DD/MM/YYYY} (example)
-
Date with a custom date format string.
- -
{{ '{' }}taskTitles}
-
Comma separated (parent) task titles
- -
{{ '{' }}subTaskTitles}
-
Comma separated sub task titles
- -
{{ '{' }}totalTime}
-
The total time you spend working on your todays tasks.
-
- -

In addition to this there are several options you can use to modify the calculation of those values.

-
- - - Auto-login and load data next time - - Always round work time up - - - - - don't round - - {{roundOption.title}} - - - - - - - - don't round - - {{roundOption.title}} - - - - - - - - don't round - - {{roundOption.title}} - - - - - -
- - - - Revoke permissions - - Edit spreadsheet - -
- - -
- -
- -
- - - - - -
- -
- - - - - - - - - - - - - - - - - - -
Last saved row{{col}}
Default - -
Actual - -
-
+
+
-
- - -
- +
+
+ +
+

This view allows you to export your worked time to a google sheet. You need to allow for your Google Spreadsheets to be accessed by Super Productivity. You also need to create a spreadsheet with a headings in the first row nad specify it's ID in the input field (Ho to find the id of a spreadsheet?). +

+

After successfully loading your spreadsheet a table will show up with 4 rows. The first row shows the heading you specified in the spreadsheet itself.

+

The second row is for informational purposes and shows the last row from the spreadsheet.

+

The forth row is a list of values you can directly enter save to the spreadsheet.

+

The third row is there to automatically define some values for the forth row. There are several special strings you can enter into the cells:

+
+ +
+
{{ '{' }}startTime}
+
The time when you first used this app today. It's possible to round this via the options.
+ +
{{ '{' }}currentTime}
+
The current time. Could be used for the for the end time of todays working day It's possible to round this via the options.
+ +
{{ '{' }}date}
+
Todays date in standard format (mm/dd/yyyy)
+ +
{{ '{' }}date:DD/MM/YYYY} (example)
+
Date with a custom date format string.
+ +
{{ '{' }}taskTitles}
+
Comma separated (parent) task titles
+ +
{{ '{' }}subTaskTitles}
+
Comma separated sub task titles
+ +
{{ '{' }}totalTime}
+
The total time you spend working on your todays tasks.
+
+ +

In addition to this there are several options you can use to modify the calculation of those values.

+
+ + + Auto-login and load data next time + + + Always round work time up + + + + + don't round + + {{roundOption.title}} + + + + + + + + don't round + + {{roundOption.title}} + + + + + + + + don't round + + {{roundOption.title}} + + + + + +
+ + + + Revoke permissions + + Edit spreadsheet + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + +
Last saved row{{col}}
Default + +
Actual + +
+
+
+ + +
+ + +
+
+
diff --git a/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.scss b/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.scss index e69de29bb..2cf575ecd 100644 --- a/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.scss +++ b/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.scss @@ -0,0 +1,52 @@ +.help-collapsible { + ::ng-deep.collapsible-title { + font-size: 20px; + } +} + +.options-collapsible { + ::ng-deep .collapsible-title { + font-size: 20px; + margin-top: 0; + } +} + +.export-input-table { + th { + font-weight: bold; + } + + input { + width: 100%; + min-width: 40px; + } +} + +.possible-properties { + dt { + margin-bottom: 4px; + } + dd { + margin-bottom: 10px; + } +} + +.loading-spinner { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: rgba(0, 0, 0, 0.3); + + mat-progress-spinner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%, 0); + } +} + +mat-input-container { + min-width: 150px; +} diff --git a/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.ts b/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.ts index ad850e36a..5cc6e20f1 100644 --- a/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.ts +++ b/src/app/core/google/dialog-google-export-time/dialog-google-export-time.component.ts @@ -1,4 +1,8 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import * as moment from 'moment'; +import { Duration, Moment } from 'moment'; +import { GoogleApiService } from '../google-api.service'; +import { SnackService } from '../../snack/snack.service'; @Component({ selector: 'dialog-google-export-time', @@ -7,11 +11,275 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; changeDetection: ChangeDetectionStrategy.OnPush }) export class DialogGoogleExportTimeComponent implements OnInit { + opts: any = { + spreadsheetId: undefined, + isAutoLogin: false, + isAutoFocusEmpty: false, + isRoundWorkTimeUp: undefined, + roundStartTimeTo: undefined, + roundEndTimeTo: undefined, + roundWorkTimeTo: undefined, + defaultValues: [ + '' + ] + }; + // $rootScope.r.uiHelper.timeSheetExportSettings; + actualValues = []; + isLoading = false; + isLoggedIn = false; + headings: string[] = []; + lastRow: string[] = []; + MISSING = { + startedTimeToday: 'MISSING startedTimeToday', + getTimeWorkedToday: 'MISSING getTimeWorkedToday', + getToday: [] + }; - constructor() { + roundTimeOptions = [ + {id: 'QUARTER', title: 'full quarters'}, + {id: 'HALF', title: 'full half hours'}, + {id: 'HOUR', title: 'full hours'}, + ]; + + + constructor( + public googleApiService: GoogleApiService, + private _snackService: SnackService, + private _cd: ChangeDetectorRef + ) { } ngOnInit() { + if (this.opts.isAutoLogin) { + this.login() + .then(() => { + if (this.opts.spreadsheetId) { + this.readSpreadsheet(); + } + }) + .then(() => { + this.updateDefaults(); + }); + } + } + + cancel() { + // $mdDialog.hide(); + } + + login() { + this.isLoading = true; + return this.googleApiService.login() + .then(() => { + this.isLoading = false; + this.isLoggedIn = true; + this._cd.detectChanges(); + }); + } + + readSpreadsheet() { + this.isLoading = true; + this.headings = undefined; + return this.googleApiService.getSpreadsheetHeadingsAndLastRow(this.opts.spreadsheetId) + .then((data: any) => { + this.headings = data.headings; + this.lastRow = data.lastRow; + this.updateDefaults(); + this.isLoading = false; + this._cd.detectChanges(); + }); + } + + logout() { + this.isLoading = false; + return this.googleApiService.logout() + .then(() => { + this.isLoading = false; + this.isLoggedIn = false; + this._cd.detectChanges(); + }); + } + + save() { + this.isLoading = true; + const arraysEqual = (arr1, arr2) => { + if (arr1.length !== arr2.length) { + return false; + } + for (let i = arr1.length; i--;) { + if (arr1[i] !== arr2[i]) { + return false; + } + } + return true; + }; + + if (arraysEqual(this.actualValues, this.lastRow)) { + this._snackService.open('Current values and the last saved row have equal values, that is probably not what you want.'); + } else { + + this.googleApiService.appendRow(this.opts.spreadsheetId, this.actualValues) + .then(() => { + this._snackService.open({ + message: 'Row successfully appended', + type: 'SUCCESS' + }); + + // $mdDialog.hide(); + // $rootScope.r.currentSession.isTimeSheetExported = true; + this.isLoading = false; + }); + } + } + + updateDefaults() { + this.opts.defaultValues.forEach((val, index) => { + this.actualValues[index] = this.replaceVals(val); + }); + } + + replaceVals(defaultVal: string): string { + if (!defaultVal) { + return; + } + + const dVal = defaultVal.trim(); + + if (dVal.match(/\{date:/)) { + return this.getCustomDate(dVal); + } + + switch (dVal) { + case '{startTime}': + return this.getStartTime(); + case '{currentTime}': + return this.getCurrentTime(); + case '{date}': + return moment().format('MM/DD/YYYY'); + case '{taskTitles}': + return this.getTaskTitles(); + case '{subTaskTitles}': + return this.getSubTaskTitles(); + case '{totalTime}': + return this.getTotalTimeWorked(); + default: + return dVal; + } + } + + private roundDuration(value: Duration, roundTo, isRoundUp): Duration { + let rounded; + + switch (roundTo) { + case 'QUARTER': + rounded = Math.round(value.asMinutes() / 15) * 15; + if (isRoundUp) { + rounded = Math.ceil(value.asMinutes() / 15) * 15; + } + return moment.duration({minutes: rounded}); + + case 'HALF': + rounded = Math.round(value.asMinutes() / 30) * 30; + if (isRoundUp) { + rounded = Math.ceil(value.asMinutes() / 30) * 30; + } + return moment.duration({minutes: rounded}); + + case 'HOUR': + rounded = Math.round(value.asMinutes() / 60) * 60; + if (isRoundUp) { + rounded = Math.ceil(value.asMinutes() / 60) * 60; + } + return moment.duration({minutes: rounded}); + + default: + return value; + } + } + + private roundTime(value: Moment, roundTo, isRoundUp = false): Moment { + let rounded; + + switch (roundTo) { + case 'QUARTER': + rounded = Math.round(value.minute() / 15) * 15; + if (isRoundUp) { + rounded = Math.ceil(value.minute() / 15) * 15; + } + return value.minute(rounded).second(0); + + case 'HALF': + rounded = Math.round(value.minute() / 30) * 30; + if (isRoundUp) { + rounded = Math.ceil(value.minute() / 30) * 30; + } + return value.minute(rounded).second(0); + + case 'HOUR': + rounded = Math.round(value.minute() / 60) * 60; + if (isRoundUp) { + rounded = Math.ceil(value.minute() / 60) * 60; + } + return value.minute(rounded).second(0); + + default: + return value; + } + } + + private getCustomDate(dVal: string): string { + const dateFormatStr = dVal + .replace('{date:', '') + .replace('}', '') + .trim(); + return moment().format(dateFormatStr); + } + + private getStartTime() { + const val = moment(this.MISSING.startedTimeToday); + const roundTo = this.opts.roundStartTimeTo; + return this.roundTime(val, roundTo) + .format('HH:mm'); + } + + private getCurrentTime(): string { + const val = moment(); + const roundTo = this.opts.roundEndTimeTo; + + return this.roundTime(val, roundTo) + .format('HH:mm'); + } + + private getTotalTimeWorked(): string { + const val = moment.duration(this.MISSING.getTimeWorkedToday); + + const roundTo = this.opts.roundWorkTimeTo; + const dur = this.roundDuration(val, roundTo, this.opts.isRoundWorkTimeUp) as any; + return dur.format('HH:mm'); + } + + private getTaskTitles(): string { + const tasks = this.MISSING.getToday; + let titleStr = ''; + tasks.forEach((task) => { + titleStr += task.title + ', '; + }); + return titleStr.substring(0, titleStr.length - 2); + } + + private getSubTaskTitles(): string { + const tasks = this.MISSING.getToday; + let titleStr = ''; + tasks.forEach((task) => { + if (task.subTasks) { + task.subTasks.forEach((subTask) => { + titleStr += subTask.title + ', '; + }); + } else { + titleStr += task.title + ', '; + } + }); + return titleStr.substring(0, titleStr.length - 2); } } diff --git a/src/app/core/google/google-api.service.ts b/src/app/core/google/google-api.service.ts index 6181ad29a..10d95f194 100644 --- a/src/app/core/google/google-api.service.ts +++ b/src/app/core/google/google-api.service.ts @@ -3,7 +3,7 @@ import { GOOGLE_DEFAULT_FIELDS_FOR_DRIVE, GOOGLE_DISCOVERY_DOCS, GOOGLE_SCOPES, import * as moment from 'moment'; import { IS_ELECTRON } from '../../app.constants'; import { MultiPartBuilder } from './util/multi-part-builder'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpRequest } from '@angular/common/http'; import { SnackService } from '../snack/snack.service'; import { SnackType } from '../snack/snack.model'; @@ -138,9 +138,9 @@ export class GoogleApiService { return this._requestWrapper(new Promise((resolve, reject) => { this.getSpreadsheetData(spreadsheetId, 'A1:Z99') .then((response: any) => { - const range = response.result || response.data; + const range = response.result || response.data || response; - if (range.values && range.values[0]) { + if (range && range.values && range.values[0]) { resolve({ headings: range.values[0], lastRow: range.values[range.values.length - 1], @@ -364,8 +364,8 @@ export class GoogleApiService { }); } - private _mapHttp(params: any): Promise { - return this._http.request(params).toPromise(); + private _mapHttp(p: HttpRequest | any): Promise { + return this._http[p.method.toLowerCase()](p.url, p).toPromise(); } diff --git a/src/app/core/snack/snack.service.ts b/src/app/core/snack/snack.service.ts index d24d952fe..a3d3ea778 100644 --- a/src/app/core/snack/snack.service.ts +++ b/src/app/core/snack/snack.service.ts @@ -10,7 +10,10 @@ export class SnackService { constructor(private _store$: Store) { } - open(params: SnackParams) { + open(params: SnackParams | string) { + if (typeof params === 'string') { + params = {message: params}; + } this._store$.dispatch(new SnackOpen(params)); } diff --git a/tsconfig.json b/tsconfig.json index 916247e4c..069b86138 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,9 @@ "typeRoots": [ "node_modules/@types" ], + "types": [ + "@types/moment-duration-format" + ], "lib": [ "es2017", "dom"