interface JiraConfig {
user: string;
password: string;
url: string;
}
interface WorklogEntry {
issueId: string;
issueKey: string;
timeSpent: number;
timeSpentSeconds: number;
started: string;
comment?: string;
}
interface WeeklyTimeTrackingResult {
totalTimeSpentSeconds: number;
totalTimeSpent: string;
worklogs: WorklogEntry[];
weekStart: string;
weekEnd: string;
}
interface JiraUser {
accountId?: string;
key?: string;
}
interface TempoWorklog {
tempoWorklogId?: number;
jiraWorklogId?: number;
issue: {
key: string;
id: number;
};
timeSpentSeconds: number;
billableSeconds?: number;
startDate: string;
startTime?: string;
description?: string;
comment?: string;
createdAt?: string;
updatedAt?: string;
author: {
accountId?: string;
key?: string;
displayName?: string;
};
attributes?: Array<{
key: string;
value: string;
}>;
}
interface TempoWorklogResponse {
results?: TempoWorklog[];
metadata?: {
count: number;
offset: number;
limit: number;
next?: string;
};
}
function getWeekBounds(): { start: Date; end: Date } {
const now = new Date();
const day = now.getDay();
const diff = now.getDate() - day + (day === 0 ? -6 : 1);
const start = new Date(now);
start.setDate(diff);
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(start.getDate() + 6);
end.setHours(23, 59, 59, 999);
return { start, end };
}
function formatTimeSpent(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}h ${minutes}m`;
}
async function fetchWorklogs(
config: JiraConfig,
startDate: Date,
endDate: Date
): Promise<WorklogEntry[]> {
const auth = Buffer.from(`${config.user}:${config.password}`).toString('base64');
const baseUrl = config.url.replace(/\/$/, '');
const userResponse = await fetch(`${baseUrl}/rest/api/2/myself`, {
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
});
if (!userResponse.ok) {
throw new Error(`Failed to get current user: ${userResponse.statusText}`);
}
const user = await userResponse.json() as JiraUser;
const accountId = user.accountId || user.key;
const username = user.key || config.user;
const startDateStr = startDate.toISOString().split('T')[0];
const endDateStr = endDate.toISOString().split('T')[0];
const tempoEndpoints = [
`/rest/tempo-timesheets/4/worklogs?from=${startDateStr}&to=${endDateStr}&accountId=${accountId}`,
`/rest/tempo-timesheets/4/worklogs?from=${startDateStr}&to=${endDateStr}&username=${encodeURIComponent(username)}`,
`/rest/tempo-core/1/worklogs?from=${startDateStr}&to=${endDateStr}&accountId=${accountId}`,
`/rest/tempo-core/1/worklogs?from=${startDateStr}&to=${endDateStr}&username=${encodeURIComponent(username)}`,
];
let worklogs: WorklogEntry[] = [];
let lastError: Error | null = null;
for (const endpoint of tempoEndpoints) {
try {
const response = await fetch(`${baseUrl}${endpoint}`, {
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
});
if (response.ok) {
const data = await response.json();
const tempoWorklogs: TempoWorklog[] = Array.isArray(data)
? data
: (data as TempoWorklogResponse).results || [];
worklogs = tempoWorklogs.map((w: TempoWorklog) => {
const startedDateTime = w.startTime
? `${w.startDate}T${w.startTime}`
: `${w.startDate}T00:00:00.000+0000`;
return {
issueId: String(w.issue.id),
issueKey: w.issue.key,
timeSpent: w.timeSpentSeconds,
timeSpentSeconds: w.timeSpentSeconds,
started: startedDateTime,
comment: w.description || w.comment || undefined,
};
});
if (worklogs.length > 0) {
break;
}
} else if (response.status !== 404) {
const errorText = await response.text().catch(() => response.statusText);
lastError = new Error(`Tempo API error: ${response.status} ${errorText}`);
}
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
}
}
if (worklogs.length === 0) {
throw new Error(
`Failed to fetch worklogs from Tempo API. ` +
`Make sure Tempo plugin is installed and accessible. ` +
`Last error: ${lastError?.message || 'No worklogs found'}`
);
}
return worklogs;
}
export async function getWeeklyTimeTracking(
config: JiraConfig
): Promise<WeeklyTimeTrackingResult> {
const { start, end } = getWeekBounds();
const worklogs = await fetchWorklogs(config, start, end);
const totalTimeSpentSeconds = worklogs.reduce(
(sum, w) => sum + w.timeSpentSeconds,
0
);
return {
totalTimeSpentSeconds,
totalTimeSpent: formatTimeSpent(totalTimeSpentSeconds),
worklogs,
weekStart: start.toISOString().split('T')[0],
weekEnd: end.toISOString().split('T')[0],
};
}