Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
hacked report/invoice based on graphql
which allows to get the notes to spent entries
  • Loading branch information
Andreas Müller committed Nov 29, 2022
commit 711606778b8bab5f2ff38e525876277f5e10b377
20 changes: 19 additions & 1 deletion src/gtt-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const Output = {
pdf: require('./output/pdf'),
markdown: require('./output/markdown'),
invoice: require('./output/invoice'),
invoice2: require('./output/invoice2'),
dump: require('./output/dump'),
xlsx: require('./output/xlsx')
};
Expand Down Expand Up @@ -72,7 +73,8 @@ program
.option('--invoiceCurrencyPerHour <number>', 'hourly wage rate on invoice')
.option('--invoiceVAT <number>', 'vat decimal (20% = 0.2)')
.option('--invoiceDate <number>', 'date string')
.option('--invoiceCurrencyMaxUnit <number>', 'rouning invoice total, e.g. 0.01, 0.05 or 1')
.option('--invoiceTimeMaxUnit <number>', 'rounds up invoice times, e.g. 60 rounds every issue per day to 1 minute')
.option('--invoiceCurrencyMaxUnit <number>', 'rounding invoice total, e.g. 0.01, 0.05 or 1')
.option('--invoicePositionText <text>', 'invoice position text')
.option('--invoicePositionExtraText <text>', 'extra invoice position: text')
.option('--invoicePositionExtraValue <number>', 'extra invoice position: value')
Expand Down Expand Up @@ -142,6 +144,7 @@ config
.set('invoiceCurrencyPerHour', program.opts().invoiceCurrencyPerHour)
.set('invoiceVAT', program.opts().invoiceVAT)
.set('invoiceDate', program.opts().invoiceDate)
.set('invoiceTimeMaxUnit', program.opts().invoiceTimeMaxUnit)
.set('invoiceCurrencyMaxUnit', program.opts().invoiceCurrencyMaxUnit)
.set('invoicePositionText', program.opts().invoicePositionText)
.set('invoicePositionExtraText', program.opts().invoicePositionExtraText)
Expand Down Expand Up @@ -312,6 +315,21 @@ new Promise(resolve => {
.then(() => resolve());
}))

// get timelogs
.then(() => new Promise(resolve => {
Cli.list(`${Cli.fetch} Loading timelogs`);

reports
.forEach((report, done) => {
report.getTimelogs()
.catch(error => done(error))
.then(() => done());
})
.catch(error => Cli.x(`could not load timelogs.`, error))
.then(() => Cli.mark())
.then(() => resolve());
}))

// merge reports
.then(() => new Promise(resolve => {
Cli.list(`${Cli.merge} Merging reports`);
Expand Down
33 changes: 33 additions & 0 deletions src/models/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,39 @@ class base {
}));
}


/**
* query the given path
* @param path
* @param data
* @returns {*}
*/
graphQL(data) {
// remove v4/ from url, add graphql
const path = this.url.substr(0, this.url.length-3) + 'graphql';

let key = base.createDumpKey(path, data);
if (this.config.dump) return this.getDump(key);

return new Promise((resolve, reject) => throttle(() => {
request.post(`${path}`, {
json: true,
body: data,
insecure: this._insecure,
proxy: this._proxy,
resolveWithFullResponse: true,
headers: {
'Authorization': 'Bearer '+this.token,
'Content-Type': 'application/json'
}
}).then(response => {
if (this.config.get('_createDump')) this.setDump(response, key);
resolve(response);
}).catch(e => reject(e));
}));
}


/**
* query the given path
* @param path
Expand Down
51 changes: 51 additions & 0 deletions src/models/dayReport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@

/**
* day model of one item
*/
class dayReport {
constructor(iid, title, spentAt, chargeRatio) {
this.iid = iid;
this.title = title;
this.spentAt = spentAt;
this.chargeRatio = chargeRatio;

this.spent = 0;
this.notes = [];
}

getIid() {
return this.iid;
}

getTitle() {
return this.title;
}
getSpent(invoiceTimeMaxUnit) {
if(!invoiceTimeMaxUnit) {
invoiceTimeMaxUnit = 1.0;
}
return Math.ceil(this.spent / invoiceTimeMaxUnit) * invoiceTimeMaxUnit;
}

getDate() {
return this.spentAt;
}

getNotes() {
return this.notes;
}

addSpent(seconds) {
this.spent += seconds;
}
addNote(note) {
this.notes.push(note);
}
getChargeRatio() {
return this.chargeRatio;
}


}

module.exports = dayReport;
46 changes: 46 additions & 0 deletions src/models/hasTimes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const moment = require('moment');

const Base = require('./base');
const Time = require('./time');
const DayReport = require('./dayReport');

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

/**
Expand Down Expand Up @@ -60,6 +62,50 @@ class hasTimes extends Base {
return promise;
}

recordTimelogs(timelogs){

let spentFreeLabels = this.config.get('freeLabels');
if(undefined === spentFreeLabels) {
spentFreeLabels = [];
}
let spentHalfPriceLabels = this.config.get('halfPriceLabels');
if(undefined === spentHalfPriceLabels) {
spentHalfPriceLabels = [];
}

let free = false;
let halfPrice = false;
this.labels.forEach(label => {
spentFreeLabels.forEach(freeLabel => {
free |= (freeLabel == label);
});
});
this.labels.forEach(label => {
spentHalfPriceLabels.forEach(halfPriceLabel => {
halfPrice |= (halfPriceLabel == label);
});
});


let chargeRatio = free? 0.0: (halfPrice? 0.5: 1.0);



timelogs.forEach(
(timelog) => {
let spentAt = moment(timelog.spentAt);
let dateGrp = spentAt.format(this.config.get('dateFormatGroupReport'));
if(!this.days[dateGrp])
{
this.days[dateGrp] = new DayReport(this.iid, this.title, spentAt, chargeRatio);
}
if(timelog.note && timelog.note.body) {
this.days[dateGrp].addNote(timelog.note.body);
}
this.days[dateGrp].addSpent(timelog.timeSpent);
});
}

/**
* set times (call set notes first)
* @returns {Promise}
Expand Down
1 change: 1 addition & 0 deletions src/models/mergeRequest.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const _ = require('underscore');
const hasTimes = require('./hasTimes');

/**
Expand Down
120 changes: 120 additions & 0 deletions src/models/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class report extends Base {

this.issues = [];
this.mergeRequests = [];

this.timelogs = null;
}

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




/**
* 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
* sets this.timelogs when the last page is received.
* @param {String} cursor
* @param {Array} timelogs
*/
getTimelogPage(cursor, timelogs) {
if(!timelogs) {
timelogs = [];
}

const query = `
query ($project: ID!, $after: String, $entryPerPage: Int,
$startTime:Time, $endTime:Time){
project(fullPath: $project) {
name
timelogs(startTime: $startTime, endTime: $endTime,
first:$entryPerPage, after: $after) {
pageInfo {
hasNextPage
endCursor
}
nodes {
user {
username
}
spentAt
timeSpent
summary
note {
body
url
}
mergeRequests:mergeRequest {
iid
projectId
}
issues:issue {
iid
projectId
}
}
}
}
}
`

let request = {
"query": query,
"variables": {
"project": this.project.data.path_with_namespace,
"after": (cursor===undefined)?'':cursor,
"entryPerPage": 30,
"startTime": this.config.get('from'),
"endTime": this.config.get('to')
}
};

let promise = this.graphQL(request);
promise.then(response => {
if (response.body.errors) {
this.timelogs = [];
} else {
if (response.body.data.project.timelogs.nodes) {
// add timelogs
timelogs.push(response.body.data.project.timelogs.nodes);
if (response.body.data.project.timelogs.pageInfo.hasNextPage) {
// get next page
this.getTimelogPage(response.body.data.project.timelogs.pageInfo.endCursor, timelogs);
}
else {
// all pages loaded. combine chunks into single array.
let timelogsAggr = [];
timelogs.forEach((timelogchunk) => {
timelogchunk.forEach((timelog) => {
timelogsAggr.push(timelog);
});
});
this.timelogs = timelogsAggr;
}
}
else {
this.timelogs = [];
}
}
}
);
}


waitForTimelogs(resolve) {
if (this.timelogs == null) {
setTimeout(this.waitForTimelogs.bind(this), 50, resolve);
} else {
resolve();
}

}


getTimelogs() {
this.getTimelogPage();
let prm = new Promise((resolve, reject) => {
this.waitForTimelogs(resolve);
});
return prm;
}

/**
* process the given input
* @param input
Expand All @@ -125,6 +238,11 @@ class report extends Base {
let item = new model(this.config, data);
item.project_namespace = this.projects[item.project_id];

item.recordTimelogs(this.timelogs.filter(
timelog => timelog[input] &&
timelog[input].iid == data.iid &&
timelog[input].projectId == data.project_id));

item.getNotes()
.then(() => item.getTimes())
.catch(error => done(error))
Expand Down Expand Up @@ -159,6 +277,8 @@ class report extends Base {
if (!this.members) this.members = [];
this.members = this.members.concat(report.members ? report.members : []);
this.projects = Object.assign(this.projects, report.projects);
if (!this.timelogs) this.timelogs = [];
this.timelogs = this.timelogs.concat(report.timelogs);
}

/**
Expand Down
Loading