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:
Trey t
2026-03-03 09:31:29 -06:00
commit 5a50d77515
183 changed files with 34450 additions and 0 deletions
+328
View File
@@ -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',
});
}