5a50d77515
Adds sharing (residence share codes, join, user management, .casera file export/import), subscription status with feature comparison, notification preferences with bell icon, profile settings (edit info, change password, theme picker, delete account), onboarding wizard with create/join paths, enhanced dashboard with stats cards, Recharts completion chart, recent activity feed, and task report PDF download. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
329 lines
8.2 KiB
TypeScript
329 lines
8.2 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// Tasks API client (client-side)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { apiFetch } from './client';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Request / response shapes
|
|
// TODO: import from @/lib/types once the shared types package is finalised
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface CreateTaskRequest {
|
|
residence_id: number;
|
|
title: string;
|
|
description?: string;
|
|
category_id?: number;
|
|
priority_id?: number;
|
|
frequency_id?: number;
|
|
custom_interval_days?: number;
|
|
in_progress?: boolean;
|
|
assigned_to_id?: number;
|
|
due_date?: string;
|
|
estimated_cost?: number;
|
|
contractor_id?: number;
|
|
}
|
|
|
|
export interface UpdateTaskRequest {
|
|
title?: string;
|
|
description?: string;
|
|
category_id?: number;
|
|
priority_id?: number;
|
|
frequency_id?: number;
|
|
custom_interval_days?: number;
|
|
in_progress?: boolean;
|
|
assigned_to_id?: number;
|
|
due_date?: string;
|
|
estimated_cost?: number;
|
|
actual_cost?: number;
|
|
contractor_id?: number;
|
|
}
|
|
|
|
export interface TaskResponse {
|
|
id: number;
|
|
residence_id: number;
|
|
residence_name: string;
|
|
title: string;
|
|
description: string;
|
|
category_id?: number;
|
|
category?: LookupResponse;
|
|
priority_id?: number;
|
|
priority?: LookupResponse;
|
|
frequency_id?: number;
|
|
frequency?: LookupResponse;
|
|
custom_interval_days?: number;
|
|
in_progress: boolean;
|
|
is_cancelled: boolean;
|
|
is_archived: boolean;
|
|
assigned_to_id?: number;
|
|
assigned_to?: TaskUserResponse;
|
|
due_date?: string;
|
|
next_due_date?: string;
|
|
estimated_cost?: number;
|
|
actual_cost?: number;
|
|
contractor_id?: number;
|
|
contractor?: TaskContractorResponse;
|
|
completion_count: number;
|
|
last_completed_at?: string;
|
|
created_by_id: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface LookupResponse {
|
|
id: number;
|
|
name: string;
|
|
icon?: string;
|
|
color?: string;
|
|
}
|
|
|
|
export interface TaskUserResponse {
|
|
id: number;
|
|
username: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
}
|
|
|
|
export interface TaskContractorResponse {
|
|
id: number;
|
|
name: string;
|
|
company: string;
|
|
}
|
|
|
|
/** Kanban board response returned by ListTasks and GetTasksByResidence. */
|
|
export interface KanbanResponse {
|
|
columns: KanbanColumn[];
|
|
total_summary: TaskTotalSummary;
|
|
}
|
|
|
|
export interface KanbanColumn {
|
|
name: string;
|
|
display_name: string;
|
|
count: number;
|
|
tasks: TaskResponse[];
|
|
}
|
|
|
|
export interface TaskTotalSummary {
|
|
total: number;
|
|
overdue: number;
|
|
due_soon: number;
|
|
in_progress: number;
|
|
completed: number;
|
|
upcoming: number;
|
|
}
|
|
|
|
// --- Task Completions ---
|
|
|
|
export interface CreateCompletionRequest {
|
|
task_id: number;
|
|
completed_at?: string;
|
|
notes?: string;
|
|
actual_cost?: number;
|
|
rating?: number;
|
|
image_urls?: string[];
|
|
}
|
|
|
|
export interface UpdateCompletionRequest {
|
|
notes?: string;
|
|
actual_cost?: number;
|
|
rating?: number;
|
|
image_urls?: string[];
|
|
}
|
|
|
|
export interface CompletionResponse {
|
|
id: number;
|
|
task_id: number;
|
|
task_title: string;
|
|
completed_at: string;
|
|
completed_by_id: number;
|
|
completed_by?: TaskUserResponse;
|
|
notes: string;
|
|
actual_cost?: number;
|
|
rating?: number;
|
|
images: CompletionImageResponse[];
|
|
created_at: string;
|
|
}
|
|
|
|
export interface CompletionImageResponse {
|
|
id: number;
|
|
image_url: string;
|
|
caption: string;
|
|
}
|
|
|
|
export interface MessageResponse {
|
|
message: string;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API functions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* List all tasks for the current user (kanban board).
|
|
* @param days Number of days for "due soon" threshold (default 30)
|
|
*/
|
|
export function listTasks(days?: number): Promise<KanbanResponse> {
|
|
const params = days != null ? `?days=${days}` : '';
|
|
return apiFetch<KanbanResponse>(`/tasks/${params}`);
|
|
}
|
|
|
|
/** Get tasks for a specific residence (kanban board). */
|
|
export function getTasksByResidence(
|
|
residenceId: number,
|
|
days?: number,
|
|
): Promise<KanbanResponse> {
|
|
const params = days != null ? `?days=${days}` : '';
|
|
return apiFetch<KanbanResponse>(
|
|
`/tasks/by-residence/${residenceId}/${params}`,
|
|
);
|
|
}
|
|
|
|
/** Get a single task by ID. */
|
|
export function getTask(id: number): Promise<TaskResponse> {
|
|
return apiFetch<TaskResponse>(`/tasks/${id}/`);
|
|
}
|
|
|
|
/** Create a new task. */
|
|
export function createTask(data: CreateTaskRequest): Promise<TaskResponse> {
|
|
return apiFetch<TaskResponse>('/tasks/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
/** Update an existing task. */
|
|
export function updateTask(
|
|
id: number,
|
|
data: UpdateTaskRequest,
|
|
): Promise<TaskResponse> {
|
|
return apiFetch<TaskResponse>(`/tasks/${id}/`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
/** Delete a task. */
|
|
export function deleteTask(id: number): Promise<MessageResponse> {
|
|
return apiFetch<MessageResponse>(`/tasks/${id}/`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/** Mark a task as in-progress. */
|
|
export function markInProgress(id: number): Promise<TaskResponse> {
|
|
return apiFetch<TaskResponse>(`/tasks/${id}/mark-in-progress/`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/** Cancel a task. */
|
|
export function cancelTask(id: number): Promise<TaskResponse> {
|
|
return apiFetch<TaskResponse>(`/tasks/${id}/cancel/`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/** Un-cancel a previously cancelled task. */
|
|
export function uncancelTask(id: number): Promise<TaskResponse> {
|
|
return apiFetch<TaskResponse>(`/tasks/${id}/uncancel/`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/** Archive a task. */
|
|
export function archiveTask(id: number): Promise<TaskResponse> {
|
|
return apiFetch<TaskResponse>(`/tasks/${id}/archive/`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/** Un-archive a previously archived task. */
|
|
export function unarchiveTask(id: number): Promise<TaskResponse> {
|
|
return apiFetch<TaskResponse>(`/tasks/${id}/unarchive/`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/** Quick-complete a task (lightweight, for widgets). Returns void (204). */
|
|
export function quickComplete(id: number): Promise<void> {
|
|
return apiFetch<void>(`/tasks/${id}/quick-complete/`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
// --- Task Completions ---
|
|
|
|
/** Get completions for a specific task. */
|
|
export function getTaskCompletions(
|
|
taskId: number,
|
|
): Promise<CompletionResponse[]> {
|
|
return apiFetch<CompletionResponse[]>(`/tasks/${taskId}/completions/`);
|
|
}
|
|
|
|
/** List all completions for the current user. */
|
|
export function listCompletions(): Promise<CompletionResponse[]> {
|
|
return apiFetch<CompletionResponse[]>('/task-completions/');
|
|
}
|
|
|
|
/** Get a single completion by ID. */
|
|
export function getCompletion(id: number): Promise<CompletionResponse> {
|
|
return apiFetch<CompletionResponse>(`/task-completions/${id}/`);
|
|
}
|
|
|
|
/** Create a new task completion (JSON). */
|
|
export function createCompletion(
|
|
data: CreateCompletionRequest,
|
|
): Promise<CompletionResponse> {
|
|
return apiFetch<CompletionResponse>('/task-completions/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a task completion with image upload (multipart/form-data).
|
|
* Use this when the user attaches photos to the completion.
|
|
*/
|
|
export function createCompletionWithImages(
|
|
data: {
|
|
task_id: number;
|
|
notes?: string;
|
|
actual_cost?: number;
|
|
completed_at?: string;
|
|
},
|
|
images: File[],
|
|
): Promise<CompletionResponse> {
|
|
const formData = new FormData();
|
|
formData.append('task_id', String(data.task_id));
|
|
if (data.notes) formData.append('notes', data.notes);
|
|
if (data.actual_cost != null)
|
|
formData.append('actual_cost', String(data.actual_cost));
|
|
if (data.completed_at) formData.append('completed_at', data.completed_at);
|
|
for (const image of images) {
|
|
formData.append('images', image);
|
|
}
|
|
|
|
return apiFetch<CompletionResponse>('/task-completions/', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
}
|
|
|
|
/** Update an existing completion. */
|
|
export function updateCompletion(
|
|
id: number,
|
|
data: UpdateCompletionRequest,
|
|
): Promise<CompletionResponse> {
|
|
return apiFetch<CompletionResponse>(`/task-completions/${id}/`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
/** Delete a completion. */
|
|
export function deleteCompletion(id: number): Promise<MessageResponse> {
|
|
return apiFetch<MessageResponse>(`/task-completions/${id}/`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|