feat(timesheet): add tags management and responsive time tracking flows
This commit is contained in:
61
src/api/tags.ts
Normal file
61
src/api/tags.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { authFetch } from "./client";
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
workspace: string;
|
||||
name: string;
|
||||
color: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface PaginatedResponse<T> {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: T[];
|
||||
}
|
||||
|
||||
export const getTags = async (
|
||||
workspaceId: string,
|
||||
params: { limit?: number; offset?: number; search?: string; ordering?: string } = {},
|
||||
): Promise<PaginatedResponse<Tag>> => {
|
||||
const query = new URLSearchParams({ workspace: workspaceId });
|
||||
|
||||
if (params.limit !== undefined) query.append("limit", String(params.limit));
|
||||
if (params.offset !== undefined) query.append("offset", String(params.offset));
|
||||
if (params.search) query.append("search", params.search);
|
||||
if (params.ordering) query.append("ordering", params.ordering);
|
||||
|
||||
const response = await authFetch(`/api/tags/?${query.toString()}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch tags");
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const createTag = async (workspaceId: string, data: { name: string; color: string }) => {
|
||||
const response = await authFetch("/api/tags/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
workspace_id: workspaceId,
|
||||
...data,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to create tag");
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const updateTag = async (id: string, data: Partial<Pick<Tag, "name" | "color">>) => {
|
||||
const response = await authFetch(`/api/tags/${id}/`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to update tag");
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const deleteTag = async (id: string) => {
|
||||
const response = await authFetch(`/api/tags/${id}/`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to delete tag");
|
||||
};
|
||||
122
src/api/timeEntries.ts
Normal file
122
src/api/timeEntries.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { authFetch } from "./client";
|
||||
|
||||
export interface TimeEntry {
|
||||
id: string;
|
||||
workspace: string;
|
||||
user: string;
|
||||
project: string | null;
|
||||
description: string;
|
||||
start_time: string;
|
||||
end_time: string | null;
|
||||
duration: string | null;
|
||||
tags: string[];
|
||||
is_billable: boolean;
|
||||
hourly_rate: string | null;
|
||||
currency: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface TimeEntryGroupDay {
|
||||
key: string;
|
||||
date: string;
|
||||
total_ms: number;
|
||||
entries: TimeEntry[];
|
||||
}
|
||||
|
||||
export interface TimeEntryGroupWeek {
|
||||
key: string;
|
||||
week_start: string;
|
||||
week_end: string;
|
||||
total_ms: number;
|
||||
days: TimeEntryGroupDay[];
|
||||
}
|
||||
|
||||
interface GroupedTimeEntryResponse {
|
||||
items_per_page: number;
|
||||
current_page_items_count: number;
|
||||
total_items: number;
|
||||
offset: number;
|
||||
next_offset: number | null;
|
||||
has_more: boolean;
|
||||
groups: TimeEntryGroupWeek[];
|
||||
}
|
||||
|
||||
export interface TimeEntryPayload {
|
||||
workspace_id?: string;
|
||||
project_id?: string | null;
|
||||
description?: string;
|
||||
start_time?: string;
|
||||
end_time?: string | null;
|
||||
tags?: string[];
|
||||
is_billable?: boolean;
|
||||
}
|
||||
|
||||
export interface TimeEntryListParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
status?: "running" | "ended" | "all";
|
||||
project?: string;
|
||||
client?: string;
|
||||
tags?: string[];
|
||||
started_after?: string;
|
||||
started_before?: string;
|
||||
}
|
||||
|
||||
export const getTimeEntries = async (
|
||||
workspaceId: string,
|
||||
params: TimeEntryListParams = {},
|
||||
): Promise<GroupedTimeEntryResponse> => {
|
||||
const query = new URLSearchParams({ workspace: workspaceId });
|
||||
|
||||
if (params.limit !== undefined) query.append("limit", String(params.limit));
|
||||
if (params.offset !== undefined) query.append("offset", String(params.offset));
|
||||
if (params.search) query.append("search", params.search);
|
||||
if (params.status) query.append("status", params.status);
|
||||
if (params.project) query.append("project", params.project);
|
||||
if (params.client) query.append("client", params.client);
|
||||
if (params.started_after) query.append("started_after", params.started_after);
|
||||
if (params.started_before) query.append("started_before", params.started_before);
|
||||
if (params.tags?.length) {
|
||||
params.tags.forEach((tagId) => query.append("tags", tagId));
|
||||
}
|
||||
|
||||
const response = await authFetch(`/api/time-entries/?${query.toString()}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch time entries");
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const createTimeEntry = async (payload: TimeEntryPayload) => {
|
||||
const response = await authFetch("/api/time-entries/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to create time entry");
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const updateTimeEntry = async (id: string, payload: TimeEntryPayload) => {
|
||||
const response = await authFetch(`/api/time-entries/${id}/`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to update time entry");
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const stopTimeEntry = async (id: string, endTime?: string) => {
|
||||
const response = await authFetch(`/api/time-entries/${id}/stop/`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(endTime ? { end_time: endTime } : {}),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to stop time entry");
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const deleteTimeEntry = async (id: string) => {
|
||||
const response = await authFetch(`/api/time-entries/${id}/`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to delete time entry");
|
||||
};
|
||||
Reference in New Issue
Block a user