feat(time-tracking): add the v2 time-entry service
This commit is contained in:
parent
565bf97294
commit
80c21e6f40
|
|
@ -0,0 +1,16 @@
|
|||
import type {IAbstract} from './IAbstract'
|
||||
|
||||
export interface ITimeEntry extends IAbstract {
|
||||
id: number
|
||||
userId: number
|
||||
// Exactly one of taskId / projectId is set (0 means unset).
|
||||
taskId: number
|
||||
projectId: number
|
||||
startTime: Date
|
||||
// null while the live timer is running.
|
||||
endTime: Date | null
|
||||
comment: string
|
||||
|
||||
created: Date
|
||||
updated: Date
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
|
||||
import {parseTimeEntry} from './timeEntry'
|
||||
|
||||
describe('parseTimeEntry', () => {
|
||||
it('maps snake_case keys and coerces dates', () => {
|
||||
const e = parseTimeEntry({
|
||||
id: 1,
|
||||
user_id: 2,
|
||||
task_id: 3,
|
||||
project_id: 0,
|
||||
start_time: '2020-01-01T09:00:00Z',
|
||||
end_time: '2020-01-01T10:00:00Z',
|
||||
comment: 'work',
|
||||
})
|
||||
expect(e.userId).toBe(2)
|
||||
expect(e.taskId).toBe(3)
|
||||
expect(e.comment).toBe('work')
|
||||
expect(e.startTime).toBeInstanceOf(Date)
|
||||
expect(e.endTime).toBeInstanceOf(Date)
|
||||
})
|
||||
|
||||
it('treats a null end time as a running timer', () => {
|
||||
const e = parseTimeEntry({
|
||||
id: 1,
|
||||
user_id: 1,
|
||||
task_id: 1,
|
||||
start_time: '2020-01-01T09:00:00Z',
|
||||
end_time: null,
|
||||
})
|
||||
expect(e.endTime).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import {AuthenticatedHTTPFactory, getApiBaseUrl} from '@/helpers/fetcher'
|
||||
import {objectToCamelCase, objectToSnakeCase} from '@/helpers/case'
|
||||
|
||||
import type {ITimeEntry} from '@/modelTypes/ITimeEntry'
|
||||
|
||||
// Time tracking is the first frontend feature on /api/v2, while the shared
|
||||
// AuthenticatedHTTPFactory pins baseURL to /api/v1. We hand axios absolute v2
|
||||
// URLs to bypass that. Bespoke and intentionally a bit dirty — to be folded
|
||||
// into the proper service layer once the frontend moves fully onto v2.
|
||||
function v2Url(path: string): string {
|
||||
const v2Base = getApiBaseUrl().replace(/\/api\/v1\/$/, '/api/v2/')
|
||||
return new URL(v2Base + path, window.location.origin).toString()
|
||||
}
|
||||
|
||||
export function parseTimeEntry(raw: Record<string, unknown>): ITimeEntry {
|
||||
const e = objectToCamelCase(raw)
|
||||
const end = e.endTime as string | null | undefined
|
||||
return {
|
||||
id: e.id,
|
||||
userId: e.userId,
|
||||
taskId: e.taskId ?? 0,
|
||||
projectId: e.projectId ?? 0,
|
||||
startTime: new Date(e.startTime),
|
||||
// null end_time = a running timer.
|
||||
endTime: end ? new Date(end) : null,
|
||||
comment: e.comment ?? '',
|
||||
created: new Date(e.created),
|
||||
updated: new Date(e.updated),
|
||||
maxPermission: e.maxPermission ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export interface TimeEntryListParams {
|
||||
filter?: string
|
||||
filterTimezone?: string
|
||||
q?: string
|
||||
page?: number
|
||||
perPage?: number
|
||||
}
|
||||
|
||||
export interface TimeEntryListResult {
|
||||
items: ITimeEntry[]
|
||||
total: number
|
||||
page: number
|
||||
perPage: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export function useTimeEntryService() {
|
||||
const http = AuthenticatedHTTPFactory()
|
||||
|
||||
async function getAll(params: TimeEntryListParams = {}): Promise<TimeEntryListResult> {
|
||||
const {data} = await http.get(v2Url('time-entries'), {
|
||||
params: {
|
||||
filter: params.filter,
|
||||
filter_timezone: params.filterTimezone,
|
||||
q: params.q,
|
||||
page: params.page,
|
||||
per_page: params.perPage,
|
||||
},
|
||||
})
|
||||
return {
|
||||
items: (data.items ?? []).map(parseTimeEntry),
|
||||
total: data.total,
|
||||
page: data.page,
|
||||
perPage: data.per_page,
|
||||
totalPages: data.total_pages,
|
||||
}
|
||||
}
|
||||
|
||||
async function create(entry: Partial<ITimeEntry>): Promise<ITimeEntry> {
|
||||
const {data} = await http.post(v2Url('time-entries'), objectToSnakeCase(entry))
|
||||
return parseTimeEntry(data)
|
||||
}
|
||||
|
||||
async function update(entry: Partial<ITimeEntry> & {id: number}): Promise<ITimeEntry> {
|
||||
const {data} = await http.put(v2Url(`time-entries/${entry.id}`), objectToSnakeCase(entry))
|
||||
return parseTimeEntry(data)
|
||||
}
|
||||
|
||||
async function remove(id: number): Promise<void> {
|
||||
await http.delete(v2Url(`time-entries/${id}`))
|
||||
}
|
||||
|
||||
async function stopTimer(): Promise<ITimeEntry> {
|
||||
const {data} = await http.post(v2Url('time-entries/timer/stop'))
|
||||
return parseTimeEntry(data)
|
||||
}
|
||||
|
||||
return {getAll, create, update, remove, stopTimer}
|
||||
}
|
||||
Loading…
Reference in New Issue