Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command to get available reviewers and submit timesheets for approval #61

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
86 changes: 86 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ export type GetUserScheduleRequest = {
toDate: string;
}

export type SubmitTimesheetRequest = {
comment: string;
reviewerAccountId: string;
from: Date;
to: Date;
}

export type GetUserScheduleResponse = {
results: ScheduleEntity[];
}
Expand Down Expand Up @@ -56,6 +63,59 @@ export type IssueEntity = {
key: string;
}

export type TimesheetResponse = {
actions: Actions;
period: Period;
requiredSeconds: number;
reviewer: UserEntity;
self: string;
status: StatusEntity;
timeSpentSeconds: number;
user: UserEntity;
worklogs: SelfEntity;
}

export interface Actions {
approve: SelfEntity;
reject: SelfEntity;
reopen: SelfEntity;
submit: SelfEntity;
}

export interface Period {
from: string;
to: string;
}

export interface StatusEntity {
actor: UserEntity;
comment: string;
key: string;
requiredSecondsAtSubmit: number;
timeSpentSecondsAtSubmit: number;
updatedAt: string;
}

export interface UserEntity {
accountId: string;
self: string;
displayName: string;
}

export interface ReviewersMetadataEntity {
count: number;
}

export interface ReviewersResponse {
metadata: ReviewersMetadataEntity;
results: UserEntity[];
self: string;
}

export interface SelfEntity {
self: string;
}

export default {

async addWorklog(request: AddWorklogRequest): Promise<WorklogEntity> {
Expand Down Expand Up @@ -105,6 +165,32 @@ export default {
results: response.data.results
}
})
},

async submitTimesheet(request: SubmitTimesheetRequest): Promise<TimesheetResponse> {
const credentials = await authenticator.getCredentials()
const fromDate = request.from.toISOString().split('T')[0]
const toDate = request.to.toISOString().split('T')[0]
const url = `/timesheet-approvals/user/${credentials.accountId}/submit?from=${fromDate}&to=${toDate}`
const body = {
comment: request.comment,
reviewerAccountId: request.reviewerAccountId
}

return execute(async () => {
const response = await tempoAxios.post(url, body)
debugLog(response)
return response.data
})
},

async getReviewers(): Promise<ReviewersResponse> {
const credentials = await authenticator.getCredentials()
return execute(async () => {
const response = await tempoAxios.get(`/timesheet-approvals/user/${credentials.accountId}/reviewers`)
debugLog(response)
return response.data
})
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/commands/reviewers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Command, flags } from '@oclif/command'
import { appName } from '../appName'
import tempo from '../tempo'
import globalFlags from '../globalFlags'

export default class Reviewers extends Command {
static description = 'get the list of reviewers for the current user'

static examples = [
`${appName} reviewers`
]

static aliases = []

static flags = {
help: flags.help({ char: 'h' }),
debug: flags.boolean()
}

static args = [
]

async run() {
const { flags } = this.parse(Reviewers)
globalFlags.debug = flags.debug
await tempo.getReviewers()
}
}
56 changes: 56 additions & 0 deletions src/commands/submit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Command, flags } from '@oclif/command'
import { appName } from '../appName'
import tempo from '../tempo'
import globalFlags from '../globalFlags'

export default class Submit extends Command {
static description = '[or s], submit a timesheet for approval'

static examples = [
`${appName} submit 123456`,
`${appName} s 123456`,
`${appName} submit 123456 "please approve"`,
`${appName} submit 123456 "please approve" "2022-01-01" "2022-01-07"`
]

static aliases = ['s']

static flags = {
help: flags.help({ char: 'h' }),
debug: flags.boolean()
}

static args = [
{
name: 'reviewer',
description: 'accountId of the reviewer that should approve the timesheet',
required: true
},
{
name: 'comment',
description: 'comment to add to the timesheet submission',
required: false
},
{
name: 'from',
description: 'start date of the timesheet to submit',
required: false
},
{
name: 'to',
description: 'end date of the timesheet to submit',
required: false
}
]

async run() {
const { args, flags } = this.parse(Submit)
globalFlags.debug = flags.debug
await tempo.submitTimesheet({
reviewerAccountId: args.reviewer,
from: args.from === undefined ? null : new Date(args.from),
to: args.to === undefined ? null : new Date(args.to),
comment: args.comment
})
}
}
21 changes: 21 additions & 0 deletions src/tempo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import authenticator from './config/authenticator'
import worklogs, { AddWorklogInput } from './worklogs/worklogs'
import timesheets, { SubmitTimesheetInput } from './timesheets/timesheets'
import prompts from './config/prompts'
import * as worklogsTable from './worklogs/worklogsTable'
import * as reviewersTable from './timesheets/reviewersTable'
import chalk from 'chalk'
import { appName } from './appName'
import { trimIndent } from './trimIndent'
Expand Down Expand Up @@ -190,6 +192,25 @@ export default {
console.log(table.toString())
}
})
},

async submitTimesheet(input: SubmitTimesheetInput): Promise<boolean> {
return execute(async () => {
cli.action.start('Submitting timesheet')
const timesheet = await timesheets.submitTimesheet(input)
cli.action.stop('Done.')
console.log(chalk.greenBright(`Successfully submitted timesheet to ${timesheet.reviewer.displayName} for approval.`))
})
},

async getReviewers(): Promise<boolean> {
return execute(async () => {
cli.action.start('Getting reviewers')
const reviewers = await timesheets.getReviewers()
cli.action.stop('Done.')
const table = await reviewersTable.render(reviewers)
console.log(table.toString())
})
}
}

Expand Down
55 changes: 55 additions & 0 deletions src/timesheets/reviewersTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Table, { HorizontalTable, Cell } from 'cli-table3'
import chalk from 'chalk'
import { UserEntity } from '../api/api'

export async function render(reviewers: UserEntity[]) {
const { reviewerHeaders, columnsNumber } = generateReviewerHeaders()
const infoHeaders = generateInfoHeaders(columnsNumber)
const content = await generateContent(reviewers, columnsNumber)
const table = new Table() as HorizontalTable
table.push(
...infoHeaders,
reviewerHeaders,
...content
)
return table
}

function generateReviewerHeaders() {
const headers = [
{ content: chalk.bold.greenBright('Display Name'), hAlign: 'left' },
{ content: chalk.bold.greenBright('AccountId'), hAlign: 'right' }
]
return {
reviewerHeaders: headers.map((r) => r as Cell),
columnsNumber: headers.length
}
}

function generateInfoHeaders(colSpan: number) {
return [
[{ colSpan: colSpan, hAlign: 'center', content: chalk.bold('Reviewers for the current user:') }]
].map((r) => r as Cell[])
}

async function generateContent(reviewers: UserEntity[], colSpan: number) {
let content = await generateUserContent(reviewers)
if (content.length === 0) {
content = [
[{ colSpan: colSpan, content: chalk.redBright('No reviewers'), hAlign: 'center' }]
]
}
return content.map((r) => r as Cell[])
}

async function generateUserContent(reviewers: UserEntity[]) {
return Promise.all(
reviewers.map(async (reviewer) => {
const tableContent = {
displayName: { colSpan: 1, content: chalk.yellow(reviewer.displayName), hAlign: 'left' },
accountId: { colSpan: 1, content: reviewer.accountId, hAlign: 'left' }
}
return Object.values(tableContent)
})
)
}
58 changes: 58 additions & 0 deletions src/timesheets/timesheets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import api, { TimesheetResponse, UserEntity } from '../api/api'
import time from '../time'
import authenticator from '../config/authenticator'

export type SubmitTimesheetInput = {
reviewerAccountId: string
comment: string
from: Date | null;
to: Date | null;
}

export default {

async submitTimesheet(input: SubmitTimesheetInput): Promise<TimesheetResponse> {
await checkToken()

if (input.to === null) {
input.to = getNextDayOfWeek(time.now(), 0)// Next Sunday
}

if (input.from === null) {
const newFrom = new Date(input.to.getTime())
newFrom.setDate(newFrom.getDate() - 7)
input.from = getNextDayOfWeek(newFrom, 1)// Last Monday
}

const timesheet = await api.submitTimesheet({
comment: input.comment,
reviewerAccountId: input.reviewerAccountId,
from: input.from,
to: input.to
})
return timesheet
},

async getReviewers(): Promise<UserEntity[]> {
const reviewers = await api.getReviewers()
return reviewers.results
}

}

async function checkToken() {
const isTokenSet = await authenticator.hasTempoToken()
if (!isTokenSet) {
throw Error('Tempo token not set. Setup tempomat by `tempo setup` command.')
}
}

/**
* Returns the date of the next day. If today is friday and we are asking for next friday the friday of the next week is returned.
* @param dayOfWeek 0:Su,1:Mo,2:Tu,3:We,4:Th,5:Fr,6:Sa
*/
function getNextDayOfWeek(date:Date, dayOfWeek:number) {
const resultDate = new Date(date.getTime())
resultDate.setDate(date.getDate() + (7 + dayOfWeek - date.getDay()) % 7)
return resultDate
}
42 changes: 42 additions & 0 deletions test/getReviewers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// @ts-ignore TS6059
// Only for test purpose, isn't compiled to js sources
import { mockCurrentDate } from './mocks/currentDate'

import api from '../src/api/api'
import timesheets from '../src/timesheets/timesheets'
import authenticator from '../src/config/authenticator'

jest.mock('../src/config/configStore', () => jest.requireActual('./mocks/configStore'))

afterEach(() => { jest.clearAllMocks() })

authenticator.saveCredentials({
accountId: 'fakeAccountId',
tempoToken: 'fakeToken'
})

describe('gets reviewers', () => {
const getReviewersMock = jest.fn()
.mockReturnValue([
{
accountId: '123456',
displayName: 'First Reviewer'
},
{
accountId: '456789',
displayName: 'Second Reviewer'
}
])
api.getReviewers = getReviewersMock
api.getUserSchedule = jest.fn()

mockCurrentDate(new Date('2020-02-28T12:00:00.000+01:00'))

describe('gets reviewers', () => {
test('getReviewers', async () => {
await timesheets.getReviewers()

expect(getReviewersMock).toHaveBeenCalled()
})
})
})
Loading