Skip to content

Commit 7116067

Browse files
author
Andreas Müller
committed
hacked report/invoice based on graphql
which allows to get the notes to spent entries
1 parent 800b016 commit 7116067

File tree

8 files changed

+564
-14
lines changed

8 files changed

+564
-14
lines changed

src/gtt-report.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const Output = {
1515
pdf: require('./output/pdf'),
1616
markdown: require('./output/markdown'),
1717
invoice: require('./output/invoice'),
18+
invoice2: require('./output/invoice2'),
1819
dump: require('./output/dump'),
1920
xlsx: require('./output/xlsx')
2021
};
@@ -72,7 +73,8 @@ program
7273
.option('--invoiceCurrencyPerHour <number>', 'hourly wage rate on invoice')
7374
.option('--invoiceVAT <number>', 'vat decimal (20% = 0.2)')
7475
.option('--invoiceDate <number>', 'date string')
75-
.option('--invoiceCurrencyMaxUnit <number>', 'rouning invoice total, e.g. 0.01, 0.05 or 1')
76+
.option('--invoiceTimeMaxUnit <number>', 'rounds up invoice times, e.g. 60 rounds every issue per day to 1 minute')
77+
.option('--invoiceCurrencyMaxUnit <number>', 'rounding invoice total, e.g. 0.01, 0.05 or 1')
7678
.option('--invoicePositionText <text>', 'invoice position text')
7779
.option('--invoicePositionExtraText <text>', 'extra invoice position: text')
7880
.option('--invoicePositionExtraValue <number>', 'extra invoice position: value')
@@ -142,6 +144,7 @@ config
142144
.set('invoiceCurrencyPerHour', program.opts().invoiceCurrencyPerHour)
143145
.set('invoiceVAT', program.opts().invoiceVAT)
144146
.set('invoiceDate', program.opts().invoiceDate)
147+
.set('invoiceTimeMaxUnit', program.opts().invoiceTimeMaxUnit)
145148
.set('invoiceCurrencyMaxUnit', program.opts().invoiceCurrencyMaxUnit)
146149
.set('invoicePositionText', program.opts().invoicePositionText)
147150
.set('invoicePositionExtraText', program.opts().invoicePositionExtraText)
@@ -312,6 +315,21 @@ new Promise(resolve => {
312315
.then(() => resolve());
313316
}))
314317

318+
// get timelogs
319+
.then(() => new Promise(resolve => {
320+
Cli.list(`${Cli.fetch} Loading timelogs`);
321+
322+
reports
323+
.forEach((report, done) => {
324+
report.getTimelogs()
325+
.catch(error => done(error))
326+
.then(() => done());
327+
})
328+
.catch(error => Cli.x(`could not load timelogs.`, error))
329+
.then(() => Cli.mark())
330+
.then(() => resolve());
331+
}))
332+
315333
// merge reports
316334
.then(() => new Promise(resolve => {
317335
Cli.list(`${Cli.merge} Merging reports`);

src/models/base.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,39 @@ class base {
5353
}));
5454
}
5555

56+
57+
/**
58+
* query the given path
59+
* @param path
60+
* @param data
61+
* @returns {*}
62+
*/
63+
graphQL(data) {
64+
// remove v4/ from url, add graphql
65+
const path = this.url.substr(0, this.url.length-3) + 'graphql';
66+
67+
let key = base.createDumpKey(path, data);
68+
if (this.config.dump) return this.getDump(key);
69+
70+
return new Promise((resolve, reject) => throttle(() => {
71+
request.post(`${path}`, {
72+
json: true,
73+
body: data,
74+
insecure: this._insecure,
75+
proxy: this._proxy,
76+
resolveWithFullResponse: true,
77+
headers: {
78+
'Authorization': 'Bearer '+this.token,
79+
'Content-Type': 'application/json'
80+
}
81+
}).then(response => {
82+
if (this.config.get('_createDump')) this.setDump(response, key);
83+
resolve(response);
84+
}).catch(e => reject(e));
85+
}));
86+
}
87+
88+
5689
/**
5790
* query the given path
5891
* @param path

src/models/dayReport.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
2+
/**
3+
* day model of one item
4+
*/
5+
class dayReport {
6+
constructor(iid, title, spentAt, chargeRatio) {
7+
this.iid = iid;
8+
this.title = title;
9+
this.spentAt = spentAt;
10+
this.chargeRatio = chargeRatio;
11+
12+
this.spent = 0;
13+
this.notes = [];
14+
}
15+
16+
getIid() {
17+
return this.iid;
18+
}
19+
20+
getTitle() {
21+
return this.title;
22+
}
23+
getSpent(invoiceTimeMaxUnit) {
24+
if(!invoiceTimeMaxUnit) {
25+
invoiceTimeMaxUnit = 1.0;
26+
}
27+
return Math.ceil(this.spent / invoiceTimeMaxUnit) * invoiceTimeMaxUnit;
28+
}
29+
30+
getDate() {
31+
return this.spentAt;
32+
}
33+
34+
getNotes() {
35+
return this.notes;
36+
}
37+
38+
addSpent(seconds) {
39+
this.spent += seconds;
40+
}
41+
addNote(note) {
42+
this.notes.push(note);
43+
}
44+
getChargeRatio() {
45+
return this.chargeRatio;
46+
}
47+
48+
49+
}
50+
51+
module.exports = dayReport;

src/models/hasTimes.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const moment = require('moment');
33

44
const Base = require('./base');
55
const Time = require('./time');
6+
const DayReport = require('./dayReport');
67

78
const regex = /added (.*) of time spent(?: at (.*))?/i;
89
const subRegex = /subtracted (.*) of time spent(?: at (.*))?/i;
@@ -17,6 +18,7 @@ class hasTimes extends Base {
1718
super(config);
1819
this.times = [];
1920
this.timesWarnings = [];
21+
this.days = {};
2022
}
2123

2224
/**
@@ -60,6 +62,50 @@ class hasTimes extends Base {
6062
return promise;
6163
}
6264

65+
recordTimelogs(timelogs){
66+
67+
let spentFreeLabels = this.config.get('freeLabels');
68+
if(undefined === spentFreeLabels) {
69+
spentFreeLabels = [];
70+
}
71+
let spentHalfPriceLabels = this.config.get('halfPriceLabels');
72+
if(undefined === spentHalfPriceLabels) {
73+
spentHalfPriceLabels = [];
74+
}
75+
76+
let free = false;
77+
let halfPrice = false;
78+
this.labels.forEach(label => {
79+
spentFreeLabels.forEach(freeLabel => {
80+
free |= (freeLabel == label);
81+
});
82+
});
83+
this.labels.forEach(label => {
84+
spentHalfPriceLabels.forEach(halfPriceLabel => {
85+
halfPrice |= (halfPriceLabel == label);
86+
});
87+
});
88+
89+
90+
let chargeRatio = free? 0.0: (halfPrice? 0.5: 1.0);
91+
92+
93+
94+
timelogs.forEach(
95+
(timelog) => {
96+
let spentAt = moment(timelog.spentAt);
97+
let dateGrp = spentAt.format(this.config.get('dateFormatGroupReport'));
98+
if(!this.days[dateGrp])
99+
{
100+
this.days[dateGrp] = new DayReport(this.iid, this.title, spentAt, chargeRatio);
101+
}
102+
if(timelog.note && timelog.note.body) {
103+
this.days[dateGrp].addNote(timelog.note.body);
104+
}
105+
this.days[dateGrp].addSpent(timelog.timeSpent);
106+
});
107+
}
108+
63109
/**
64110
* set times (call set notes first)
65111
* @returns {Promise}

src/models/mergeRequest.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const _ = require('underscore');
12
const hasTimes = require('./hasTimes');
23

34
/**

src/models/report.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class report extends Base {
2323

2424
this.issues = [];
2525
this.mergeRequests = [];
26+
27+
this.timelogs = null;
2628
}
2729

2830
/**
@@ -110,6 +112,117 @@ class report extends Base {
110112
return issues.filter(issue => this.config.get('showWithoutTimes') || (issue.times && issue.times.length > 0));
111113
}
112114

115+
116+
117+
118+
/**
119+
* starts loading the timelogs data page after the cursor into the array timelogs and recurse to the next page as soon as results are received
120+
* sets this.timelogs when the last page is received.
121+
* @param {String} cursor
122+
* @param {Array} timelogs
123+
*/
124+
getTimelogPage(cursor, timelogs) {
125+
if(!timelogs) {
126+
timelogs = [];
127+
}
128+
129+
const query = `
130+
query ($project: ID!, $after: String, $entryPerPage: Int,
131+
$startTime:Time, $endTime:Time){
132+
project(fullPath: $project) {
133+
name
134+
timelogs(startTime: $startTime, endTime: $endTime,
135+
first:$entryPerPage, after: $after) {
136+
pageInfo {
137+
hasNextPage
138+
endCursor
139+
}
140+
nodes {
141+
user {
142+
username
143+
}
144+
spentAt
145+
timeSpent
146+
summary
147+
note {
148+
body
149+
url
150+
}
151+
mergeRequests:mergeRequest {
152+
iid
153+
projectId
154+
}
155+
issues:issue {
156+
iid
157+
projectId
158+
}
159+
}
160+
}
161+
}
162+
}
163+
`
164+
165+
let request = {
166+
"query": query,
167+
"variables": {
168+
"project": this.project.data.path_with_namespace,
169+
"after": (cursor===undefined)?'':cursor,
170+
"entryPerPage": 30,
171+
"startTime": this.config.get('from'),
172+
"endTime": this.config.get('to')
173+
}
174+
};
175+
176+
let promise = this.graphQL(request);
177+
promise.then(response => {
178+
if (response.body.errors) {
179+
this.timelogs = [];
180+
} else {
181+
if (response.body.data.project.timelogs.nodes) {
182+
// add timelogs
183+
timelogs.push(response.body.data.project.timelogs.nodes);
184+
if (response.body.data.project.timelogs.pageInfo.hasNextPage) {
185+
// get next page
186+
this.getTimelogPage(response.body.data.project.timelogs.pageInfo.endCursor, timelogs);
187+
}
188+
else {
189+
// all pages loaded. combine chunks into single array.
190+
let timelogsAggr = [];
191+
timelogs.forEach((timelogchunk) => {
192+
timelogchunk.forEach((timelog) => {
193+
timelogsAggr.push(timelog);
194+
});
195+
});
196+
this.timelogs = timelogsAggr;
197+
}
198+
}
199+
else {
200+
this.timelogs = [];
201+
}
202+
}
203+
}
204+
);
205+
}
206+
207+
208+
waitForTimelogs(resolve) {
209+
if (this.timelogs == null) {
210+
setTimeout(this.waitForTimelogs.bind(this), 50, resolve);
211+
} else {
212+
resolve();
213+
}
214+
215+
}
216+
217+
218+
getTimelogs() {
219+
this.getTimelogPage();
220+
let prm = new Promise((resolve, reject) => {
221+
this.waitForTimelogs(resolve);
222+
});
223+
return prm;
224+
}
225+
113226
/**
114227
* process the given input
115228
* @param input
@@ -125,6 +238,11 @@ class report extends Base {
125238
let item = new model(this.config, data);
126239
item.project_namespace = this.projects[item.project_id];
127240

241+
item.recordTimelogs(this.timelogs.filter(
242+
timelog => timelog[input] &&
243+
timelog[input].iid == data.iid &&
244+
timelog[input].projectId == data.project_id));
245+
128246
item.getNotes()
129247
.then(() => item.getTimes())
130248
.catch(error => done(error))
@@ -159,6 +277,8 @@ class report extends Base {
159277
if (!this.members) this.members = [];
160278
this.members = this.members.concat(report.members ? report.members : []);
161279
this.projects = Object.assign(this.projects, report.projects);
280+
if (!this.timelogs) this.timelogs = [];
281+
this.timelogs = this.timelogs.concat(report.timelogs);
162282
}
163283

164284
/**

0 commit comments

Comments
 (0)