feat: complete Phase 3 — advanced features for Casera web app
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>
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user