feat: Phase 4-5 — demo mode, polish, deploy, and bug fixes
Add demo mode with mock data provider, Docker deployment, Playwright tests, PostHog analytics, error boundaries, and SEO metadata. Fix residences API response unwrapping, kanban drag-and-drop with optimistic updates, trailing slash proxy redirects, and column name mismatches with Go API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import posthog from "posthog-js";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export function initAnalytics() {
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
process.env.NEXT_PUBLIC_POSTHOG_KEY &&
|
||||
!initialized
|
||||
) {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
api_host:
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST ||
|
||||
"https://analytics.88oakapps.com",
|
||||
capture_pageview: true,
|
||||
capture_pageleave: true,
|
||||
});
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function trackEvent(
|
||||
event: string,
|
||||
properties?: Record<string, unknown>
|
||||
) {
|
||||
if (initialized) {
|
||||
posthog.capture(event, properties);
|
||||
}
|
||||
}
|
||||
|
||||
export function trackScreen(screenName: string) {
|
||||
if (initialized) {
|
||||
posthog.capture("$pageview", { $current_url: screenName });
|
||||
}
|
||||
}
|
||||
|
||||
export function identifyUser(userId: string, traits?: Record<string, unknown>) {
|
||||
if (initialized) {
|
||||
posthog.identify(userId, traits);
|
||||
}
|
||||
}
|
||||
|
||||
export function resetAnalytics() {
|
||||
if (initialized) {
|
||||
posthog.reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { initAnalytics, trackScreen } from "@/lib/analytics";
|
||||
|
||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
initAnalytics();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname) {
|
||||
trackScreen(pathname);
|
||||
}
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
+9
-9
@@ -134,7 +134,7 @@ export async function login(credentials: LoginRequest): Promise<LoginResponse> {
|
||||
export async function register(
|
||||
data: RegisterRequest,
|
||||
): Promise<RegisterResponse> {
|
||||
const res = await fetch('/api/proxy/auth/register/', {
|
||||
const res = await fetch('/api/proxy/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -172,7 +172,7 @@ export async function getCurrentUser(): Promise<UserResponse> {
|
||||
export async function updateProfile(
|
||||
data: UpdateProfileRequest,
|
||||
): Promise<UserResponse> {
|
||||
const res = await fetch('/api/proxy/auth/profile/', {
|
||||
const res = await fetch('/api/proxy/auth/profile', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -189,7 +189,7 @@ export async function updateProfile(
|
||||
export async function verifyEmail(
|
||||
data: VerifyEmailRequest,
|
||||
): Promise<VerifyEmailResponse> {
|
||||
const res = await fetch('/api/proxy/auth/verify-email/', {
|
||||
const res = await fetch('/api/proxy/auth/verify-email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -204,7 +204,7 @@ export async function verifyEmail(
|
||||
* Resend the email verification code.
|
||||
*/
|
||||
export async function resendVerification(): Promise<MessageResponse> {
|
||||
const res = await fetch('/api/proxy/auth/resend-verification/', {
|
||||
const res = await fetch('/api/proxy/auth/resend-verification', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -220,7 +220,7 @@ export async function resendVerification(): Promise<MessageResponse> {
|
||||
export async function forgotPassword(
|
||||
data: ForgotPasswordRequest,
|
||||
): Promise<MessageResponse> {
|
||||
const res = await fetch('/api/proxy/auth/forgot-password/', {
|
||||
const res = await fetch('/api/proxy/auth/forgot-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -237,7 +237,7 @@ export async function forgotPassword(
|
||||
export async function verifyResetCode(
|
||||
data: VerifyResetCodeRequest,
|
||||
): Promise<VerifyResetCodeResponse> {
|
||||
const res = await fetch('/api/proxy/auth/verify-reset-code/', {
|
||||
const res = await fetch('/api/proxy/auth/verify-reset-code', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -254,7 +254,7 @@ export async function verifyResetCode(
|
||||
export async function resetPassword(
|
||||
data: ResetPasswordRequest,
|
||||
): Promise<MessageResponse> {
|
||||
const res = await fetch('/api/proxy/auth/reset-password/', {
|
||||
const res = await fetch('/api/proxy/auth/reset-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -271,7 +271,7 @@ export async function resetPassword(
|
||||
export async function changePassword(
|
||||
data: { current_password: string; new_password: string },
|
||||
): Promise<MessageResponse> {
|
||||
const res = await fetch('/api/proxy/auth/change-password/', {
|
||||
const res = await fetch('/api/proxy/auth/change-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -286,7 +286,7 @@ export async function changePassword(
|
||||
* Delete the authenticated user's account permanently.
|
||||
*/
|
||||
export async function deleteAccount(): Promise<MessageResponse> {
|
||||
const res = await fetch('/api/proxy/auth/delete-account/', {
|
||||
const res = await fetch('/api/proxy/auth/delete-account', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_URL || 'https://mycrib.treytartt.com/api';
|
||||
process.env.NEXT_PUBLIC_API_URL || 'https://casera.treytartt.com/api';
|
||||
|
||||
/**
|
||||
* Server-only base URL. Falls back to the public one so that server
|
||||
@@ -45,8 +45,10 @@ export async function apiFetch<T>(
|
||||
path: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<T> {
|
||||
// Ensure trailing slash (Go API requires it)
|
||||
const normalized = path.endsWith('/') ? path : `${path}/`;
|
||||
// Strip trailing slash for the Next.js proxy URL (Next.js 308-redirects
|
||||
// trailing slashes away by default). The proxy route handler re-adds the
|
||||
// trailing slash when forwarding to the Go API.
|
||||
const normalized = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
const url = `/api/proxy${normalized}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface ResidenceResponse {
|
||||
is_owner: boolean;
|
||||
owner_id: number;
|
||||
user_count: number;
|
||||
overdue_count?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -156,8 +157,22 @@ export function listResidences(): Promise<ResidenceResponse[]> {
|
||||
}
|
||||
|
||||
/** Get the user's residences with task summaries. */
|
||||
export function getMyResidences(): Promise<MyResidenceResponse[]> {
|
||||
return apiFetch<MyResidenceResponse[]>('/residences/my-residences/');
|
||||
export async function getMyResidences(): Promise<MyResidenceResponse[]> {
|
||||
// Go API returns { "residences": [ResidenceResponse, ...] }
|
||||
// Each ResidenceResponse has overdue_count but no task_summary.
|
||||
// We transform into MyResidenceResponse shape for compatibility.
|
||||
const data = await apiFetch<{ residences: ResidenceResponse[] }>('/residences/my-residences/');
|
||||
const residences = data.residences ?? [];
|
||||
return residences.map((r) => ({
|
||||
residence: r,
|
||||
task_summary: {
|
||||
total: 0,
|
||||
overdue: r.overdue_count ?? 0,
|
||||
due_soon: 0,
|
||||
in_progress: 0,
|
||||
completed: 0,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/** Get aggregated task summary across all residences. */
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { DataProvider } from './data-provider';
|
||||
|
||||
const DataProviderContext = createContext<DataProvider | null>(null);
|
||||
|
||||
export function useDataProvider(): DataProvider {
|
||||
const ctx = useContext(DataProviderContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useDataProvider must be used within a DataProviderProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function DataProviderProvider({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: DataProvider;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DataProviderContext.Provider value={value}>
|
||||
{children}
|
||||
</DataProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// DataProvider interface — abstraction over real API vs. in-memory demo store
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types are imported from the API modules (the actual source of truth for hooks).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import type {
|
||||
CreateResidenceRequest,
|
||||
UpdateResidenceRequest,
|
||||
ResidenceResponse,
|
||||
MyResidenceResponse,
|
||||
ResidenceSummaryResponse,
|
||||
ShareCodeResponse,
|
||||
SharePackageResponse,
|
||||
GenerateShareCodeRequest,
|
||||
ResidenceUserResponse,
|
||||
TasksReportResponse,
|
||||
MessageResponse as ResidenceMessageResponse,
|
||||
} from '@/lib/api/residences';
|
||||
|
||||
import type {
|
||||
CreateTaskRequest,
|
||||
UpdateTaskRequest,
|
||||
TaskResponse,
|
||||
KanbanResponse,
|
||||
CompletionResponse,
|
||||
CreateCompletionRequest,
|
||||
MessageResponse as TaskMessageResponse,
|
||||
} from '@/lib/api/tasks';
|
||||
|
||||
import type {
|
||||
CreateContractorRequest,
|
||||
UpdateContractorRequest,
|
||||
ContractorResponse,
|
||||
ToggleFavoriteResponse,
|
||||
ContractorTaskResponse,
|
||||
} from '@/lib/api/contractors';
|
||||
|
||||
import type {
|
||||
DocumentListParams,
|
||||
CreateDocumentRequest,
|
||||
UpdateDocumentRequest,
|
||||
DocumentResponse,
|
||||
MessageResponse as DocMessageResponse,
|
||||
} from '@/lib/api/documents';
|
||||
|
||||
import type { StaticDataResponse } from '@/lib/api/lookups';
|
||||
|
||||
import type {
|
||||
NotificationListResponse,
|
||||
UnreadCountResponse,
|
||||
NotificationPreferencesResponse,
|
||||
UpdatePreferencesRequest,
|
||||
} from '@/lib/api/notifications';
|
||||
|
||||
import type {
|
||||
SubscriptionStatusResponse,
|
||||
FeatureBenefitResponse,
|
||||
UpgradeTriggerResponse,
|
||||
} from '@/lib/api/subscription';
|
||||
|
||||
import type { UserResponse } from '@/lib/api/auth';
|
||||
|
||||
// Unified MessageResponse (all API modules define the same shape)
|
||||
type MessageResponse = ResidenceMessageResponse | TaskMessageResponse | DocMessageResponse;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Domain-split interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface DataProvider {
|
||||
basePath: string;
|
||||
|
||||
residences: {
|
||||
list(): Promise<ResidenceResponse[]>;
|
||||
get(id: number): Promise<ResidenceResponse>;
|
||||
create(data: CreateResidenceRequest): Promise<ResidenceResponse>;
|
||||
update(id: number, data: UpdateResidenceRequest): Promise<ResidenceResponse>;
|
||||
delete(id: number): Promise<MessageResponse>;
|
||||
getMyResidences(): Promise<MyResidenceResponse[]>;
|
||||
getSummary(): Promise<ResidenceSummaryResponse>;
|
||||
};
|
||||
|
||||
tasks: {
|
||||
list(days?: number): Promise<KanbanResponse>;
|
||||
get(id: number): Promise<TaskResponse>;
|
||||
create(data: CreateTaskRequest): Promise<TaskResponse>;
|
||||
update(id: number, data: UpdateTaskRequest): Promise<TaskResponse>;
|
||||
delete(id: number): Promise<MessageResponse>;
|
||||
getByResidence(residenceId: number, days?: number): Promise<KanbanResponse>;
|
||||
getCompletions(taskId: number): Promise<CompletionResponse[]>;
|
||||
createCompletion(data: CreateCompletionRequest): Promise<CompletionResponse>;
|
||||
createCompletionWithImages(
|
||||
data: { task_id: number; notes?: string; actual_cost?: number; completed_at?: string },
|
||||
images: File[],
|
||||
): Promise<CompletionResponse>;
|
||||
markInProgress(id: number): Promise<TaskResponse>;
|
||||
cancel(id: number): Promise<TaskResponse>;
|
||||
uncancel(id: number): Promise<TaskResponse>;
|
||||
archive(id: number): Promise<TaskResponse>;
|
||||
unarchive(id: number): Promise<TaskResponse>;
|
||||
quickComplete(id: number): Promise<void>;
|
||||
};
|
||||
|
||||
contractors: {
|
||||
list(): Promise<ContractorResponse[]>;
|
||||
get(id: number): Promise<ContractorResponse>;
|
||||
create(data: CreateContractorRequest): Promise<ContractorResponse>;
|
||||
update(id: number, data: UpdateContractorRequest): Promise<ContractorResponse>;
|
||||
delete(id: number): Promise<MessageResponse>;
|
||||
toggleFavorite(id: number): Promise<ToggleFavoriteResponse>;
|
||||
getTasks(id: number): Promise<ContractorTaskResponse[]>;
|
||||
};
|
||||
|
||||
documents: {
|
||||
list(params?: DocumentListParams): Promise<DocumentResponse[]>;
|
||||
listWarranties(): Promise<DocumentResponse[]>;
|
||||
get(id: number): Promise<DocumentResponse>;
|
||||
create(data: CreateDocumentRequest): Promise<DocumentResponse>;
|
||||
createWithFile(data: CreateDocumentRequest, file: File): Promise<DocumentResponse>;
|
||||
update(id: number, data: UpdateDocumentRequest): Promise<DocumentResponse>;
|
||||
delete(id: number): Promise<MessageResponse>;
|
||||
};
|
||||
|
||||
lookups: {
|
||||
getStaticData(): Promise<StaticDataResponse>;
|
||||
};
|
||||
|
||||
sharing: {
|
||||
getShareCode(residenceId: number): Promise<{ share_code: ShareCodeResponse | null }>;
|
||||
generateShareCode(residenceId: number, data?: GenerateShareCodeRequest): Promise<ShareCodeResponse>;
|
||||
generateSharePackage(residenceId: number, data?: GenerateShareCodeRequest): Promise<SharePackageResponse>;
|
||||
getResidenceUsers(residenceId: number): Promise<ResidenceUserResponse[]>;
|
||||
removeUser(residenceId: number, userId: number): Promise<MessageResponse>;
|
||||
joinWithCode(data: { code: string }): Promise<ResidenceResponse>;
|
||||
generateTasksReport(residenceId: number, email?: string): Promise<TasksReportResponse>;
|
||||
};
|
||||
|
||||
notifications: {
|
||||
list(limit?: number, offset?: number): Promise<NotificationListResponse>;
|
||||
getUnreadCount(): Promise<UnreadCountResponse>;
|
||||
getPreferences(): Promise<NotificationPreferencesResponse>;
|
||||
updatePreferences(data: UpdatePreferencesRequest): Promise<NotificationPreferencesResponse>;
|
||||
markAsRead(id: number): Promise<MessageResponse>;
|
||||
markAllAsRead(): Promise<MessageResponse>;
|
||||
};
|
||||
|
||||
subscription: {
|
||||
getStatus(): Promise<SubscriptionStatusResponse>;
|
||||
getFeatureBenefits(): Promise<FeatureBenefitResponse[]>;
|
||||
getUpgradeTriggers(): Promise<UpgradeTriggerResponse[]>;
|
||||
};
|
||||
|
||||
auth: {
|
||||
getCurrentUser(): Promise<UserResponse>;
|
||||
logout(): Promise<MessageResponse>;
|
||||
};
|
||||
}
|
||||
|
||||
// Re-export types that hooks/consumers will need
|
||||
export type {
|
||||
CreateResidenceRequest,
|
||||
UpdateResidenceRequest,
|
||||
CreateTaskRequest,
|
||||
UpdateTaskRequest,
|
||||
CreateCompletionRequest,
|
||||
CreateContractorRequest,
|
||||
UpdateContractorRequest,
|
||||
CreateDocumentRequest,
|
||||
UpdateDocumentRequest,
|
||||
DocumentListParams,
|
||||
UpdatePreferencesRequest,
|
||||
};
|
||||
@@ -0,0 +1,358 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// DemoProvider — DataProvider implementation backed by in-memory Zustand store
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import type { DataProvider } from './data-provider';
|
||||
import type { DocumentListParams } from '@/lib/api/documents';
|
||||
import { useDemoStore } from './demo-store';
|
||||
import { buildKanbanResponse } from './mock-data/tasks';
|
||||
import { demoStaticData } from './mock-data/lookups';
|
||||
import { demoUser } from './mock-data/user';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: read current store state (snapshot) outside React
|
||||
// ---------------------------------------------------------------------------
|
||||
function snap() {
|
||||
return useDemoStore.getState();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const demoProvider: DataProvider = {
|
||||
basePath: '/demo/app',
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Residences
|
||||
// -----------------------------------------------------------------------
|
||||
residences: {
|
||||
list: async () => snap().residences,
|
||||
get: async (id) => {
|
||||
const r = snap().residences.find(r => r.id === id);
|
||||
if (!r) throw new Error(`Residence ${id} not found`);
|
||||
return r;
|
||||
},
|
||||
create: async (data) => {
|
||||
const staticData = demoStaticData;
|
||||
const propertyType = data.property_type_id
|
||||
? staticData.residence_types.find(t => t.id === data.property_type_id)
|
||||
: undefined;
|
||||
return snap().addResidence({ ...data, property_type: propertyType });
|
||||
},
|
||||
update: async (id, data) => snap().updateResidence(id, data),
|
||||
delete: async (id) => {
|
||||
snap().deleteResidence(id);
|
||||
return { message: 'Residence deleted' };
|
||||
},
|
||||
getMyResidences: async () => snap().myResidences,
|
||||
getSummary: async () => {
|
||||
const { residences, myResidences } = snap();
|
||||
const totalSummary = myResidences.reduce(
|
||||
(acc, mr) => ({
|
||||
total: acc.total + mr.task_summary.total,
|
||||
overdue: acc.overdue + mr.task_summary.overdue,
|
||||
due_soon: acc.due_soon + mr.task_summary.due_soon,
|
||||
in_progress: acc.in_progress + mr.task_summary.in_progress,
|
||||
completed: acc.completed + mr.task_summary.completed,
|
||||
}),
|
||||
{ total: 0, overdue: 0, due_soon: 0, in_progress: 0, completed: 0 },
|
||||
);
|
||||
return {
|
||||
total_residences: residences.length,
|
||||
total_summary: totalSummary,
|
||||
residences: myResidences,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Tasks
|
||||
// -----------------------------------------------------------------------
|
||||
tasks: {
|
||||
list: async () => buildKanbanResponse(snap().tasks),
|
||||
get: async (id) => {
|
||||
const t = snap().tasks.find(t => t.id === id);
|
||||
if (!t) throw new Error(`Task ${id} not found`);
|
||||
return t;
|
||||
},
|
||||
create: async (data) => {
|
||||
const staticData = demoStaticData;
|
||||
const category = data.category_id
|
||||
? staticData.task_categories.find(c => c.id === data.category_id)
|
||||
: undefined;
|
||||
const priority = data.priority_id
|
||||
? staticData.task_priorities.find(p => p.id === data.priority_id)
|
||||
: undefined;
|
||||
const contractor = data.contractor_id
|
||||
? snap().contractors.find(c => c.id === data.contractor_id)
|
||||
: undefined;
|
||||
return snap().addTask({
|
||||
...data,
|
||||
category: category ? { ...category } : undefined,
|
||||
priority: priority ? { ...priority } : undefined,
|
||||
contractor: contractor
|
||||
? { id: contractor.id, name: contractor.name, company: contractor.company }
|
||||
: undefined,
|
||||
});
|
||||
},
|
||||
update: async (id, data) => {
|
||||
const staticData = demoStaticData;
|
||||
const extra: Record<string, unknown> = {};
|
||||
if (data.category_id != null) {
|
||||
extra.category = staticData.task_categories.find(c => c.id === data.category_id);
|
||||
}
|
||||
if (data.priority_id != null) {
|
||||
extra.priority = staticData.task_priorities.find(p => p.id === data.priority_id);
|
||||
}
|
||||
if (data.contractor_id != null) {
|
||||
const c = snap().contractors.find(c => c.id === data.contractor_id);
|
||||
extra.contractor = c ? { id: c.id, name: c.name, company: c.company } : undefined;
|
||||
}
|
||||
return snap().updateTask(id, { ...data, ...extra });
|
||||
},
|
||||
delete: async (id) => {
|
||||
snap().deleteTask(id);
|
||||
return { message: 'Task deleted' };
|
||||
},
|
||||
getByResidence: async (residenceId) => {
|
||||
const tasks = snap().tasks.filter(t => t.residence_id === residenceId);
|
||||
return buildKanbanResponse(tasks);
|
||||
},
|
||||
getCompletions: async (taskId) =>
|
||||
snap().completions.filter(c => c.task_id === taskId),
|
||||
createCompletion: async (data) =>
|
||||
snap().completeTask(data.task_id, data),
|
||||
createCompletionWithImages: async (data) =>
|
||||
snap().completeTask(data.task_id, data),
|
||||
markInProgress: async (id) => snap().markTaskInProgress(id),
|
||||
cancel: async (id) => snap().cancelTask(id),
|
||||
uncancel: async (id) => snap().uncancelTask(id),
|
||||
archive: async (id) => snap().archiveTask(id),
|
||||
unarchive: async (id) => snap().unarchiveTask(id),
|
||||
quickComplete: async (id) => {
|
||||
snap().completeTask(id, {});
|
||||
},
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Contractors
|
||||
// -----------------------------------------------------------------------
|
||||
contractors: {
|
||||
list: async () => snap().contractors,
|
||||
get: async (id) => {
|
||||
const c = snap().contractors.find(c => c.id === id);
|
||||
if (!c) throw new Error(`Contractor ${id} not found`);
|
||||
return c;
|
||||
},
|
||||
create: async (data) => {
|
||||
const specialties = (data.specialty_ids ?? [])
|
||||
.map(sid => demoStaticData.contractor_specialties.find(s => s.id === sid))
|
||||
.filter(Boolean) as { id: number; name: string; icon: string }[];
|
||||
return snap().addContractor({ ...data, specialties });
|
||||
},
|
||||
update: async (id, data) => {
|
||||
const extra: Record<string, unknown> = {};
|
||||
if (data.specialty_ids) {
|
||||
extra.specialties = data.specialty_ids
|
||||
.map(sid => demoStaticData.contractor_specialties.find(s => s.id === sid))
|
||||
.filter(Boolean);
|
||||
}
|
||||
return snap().updateContractor(id, { ...data, ...extra });
|
||||
},
|
||||
delete: async (id) => {
|
||||
snap().deleteContractor(id);
|
||||
return { message: 'Contractor deleted' };
|
||||
},
|
||||
toggleFavorite: async (id) => {
|
||||
const updated = snap().toggleContractorFavorite(id);
|
||||
return { id: updated.id, is_favorite: updated.is_favorite };
|
||||
},
|
||||
getTasks: async (id) => {
|
||||
const tasks = snap().tasks.filter(t => t.contractor_id === id);
|
||||
return tasks.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
residence_name: t.residence_name,
|
||||
status: t.in_progress ? 'in_progress' : t.is_cancelled ? 'cancelled' : 'pending',
|
||||
due_date: t.due_date,
|
||||
priority: t.priority?.name,
|
||||
}));
|
||||
},
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Documents
|
||||
// -----------------------------------------------------------------------
|
||||
documents: {
|
||||
list: async (params?: DocumentListParams) => {
|
||||
let docs = snap().documents;
|
||||
if (params?.residence) docs = docs.filter(d => d.residence_id === params.residence);
|
||||
if (params?.document_type) docs = docs.filter(d => d.document_type === params.document_type);
|
||||
if (params?.is_active != null) docs = docs.filter(d => d.is_active === params.is_active);
|
||||
if (params?.search) {
|
||||
const q = params.search.toLowerCase();
|
||||
docs = docs.filter(d =>
|
||||
d.title.toLowerCase().includes(q) || d.description.toLowerCase().includes(q),
|
||||
);
|
||||
}
|
||||
return docs;
|
||||
},
|
||||
listWarranties: async () =>
|
||||
snap().documents.filter(d => d.document_type === 'warranty'),
|
||||
get: async (id) => {
|
||||
const d = snap().documents.find(d => d.id === id);
|
||||
if (!d) throw new Error(`Document ${id} not found`);
|
||||
return d;
|
||||
},
|
||||
create: async (data) => {
|
||||
const residence = snap().residences.find(r => r.id === data.residence_id);
|
||||
return snap().addDocument({
|
||||
...data,
|
||||
residence_name: residence?.name,
|
||||
});
|
||||
},
|
||||
createWithFile: async (data) => {
|
||||
const residence = snap().residences.find(r => r.id === data.residence_id);
|
||||
return snap().addDocument({
|
||||
...data,
|
||||
residence_name: residence?.name,
|
||||
});
|
||||
},
|
||||
update: async (id, data) => snap().updateDocument(id, data),
|
||||
delete: async (id) => {
|
||||
snap().deleteDocument(id);
|
||||
return { message: 'Document deleted' };
|
||||
},
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Lookups
|
||||
// -----------------------------------------------------------------------
|
||||
lookups: {
|
||||
getStaticData: async () => demoStaticData,
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Sharing (demo stubs — mostly no-ops or simple responses)
|
||||
// -----------------------------------------------------------------------
|
||||
sharing: {
|
||||
getShareCode: async () => ({ share_code: null }),
|
||||
generateShareCode: async (residenceId) => ({
|
||||
code: 'DEMO42',
|
||||
residence_id: residenceId,
|
||||
expires_at: new Date(Date.now() + 24 * 3600000).toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
}),
|
||||
generateSharePackage: async (residenceId) => {
|
||||
const r = snap().residences.find(r => r.id === residenceId);
|
||||
return {
|
||||
code: 'DEMO42',
|
||||
residence_id: residenceId,
|
||||
residence_name: r?.name ?? 'Demo Residence',
|
||||
owner_name: 'Demo User',
|
||||
expires_at: new Date(Date.now() + 24 * 3600000).toISOString(),
|
||||
};
|
||||
},
|
||||
getResidenceUsers: async () => [
|
||||
{
|
||||
id: 1,
|
||||
username: 'demo_user',
|
||||
email: 'demo@casera.app',
|
||||
first_name: 'Demo',
|
||||
last_name: 'User',
|
||||
is_owner: true,
|
||||
},
|
||||
],
|
||||
removeUser: async () => ({ message: 'User removed' }),
|
||||
joinWithCode: async () => {
|
||||
throw new Error('Sharing is not available in demo mode');
|
||||
},
|
||||
generateTasksReport: async (residenceId) => {
|
||||
const r = snap().residences.find(r => r.id === residenceId);
|
||||
return {
|
||||
message: 'Report generated (demo)',
|
||||
residence_name: r?.name ?? 'Demo Residence',
|
||||
recipient_email: 'demo@casera.app',
|
||||
pdf_generated: true,
|
||||
email_sent: false,
|
||||
report: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Notifications
|
||||
// -----------------------------------------------------------------------
|
||||
notifications: {
|
||||
list: async (limit, offset) => {
|
||||
let items = snap().notifications;
|
||||
const start = offset ?? 0;
|
||||
const end = limit ? start + limit : undefined;
|
||||
items = items.slice(start, end);
|
||||
return { count: snap().notifications.length, results: items };
|
||||
},
|
||||
getUnreadCount: async () => ({
|
||||
unread_count: snap().notifications.filter(n => !n.is_read).length,
|
||||
}),
|
||||
getPreferences: async () => ({
|
||||
task_reminders: true,
|
||||
task_completions: true,
|
||||
residence_updates: true,
|
||||
share_notifications: true,
|
||||
marketing: false,
|
||||
}),
|
||||
updatePreferences: async (data) => ({
|
||||
task_reminders: data.task_reminders ?? true,
|
||||
task_completions: data.task_completions ?? true,
|
||||
residence_updates: data.residence_updates ?? true,
|
||||
share_notifications: data.share_notifications ?? true,
|
||||
marketing: data.marketing ?? false,
|
||||
}),
|
||||
markAsRead: async (id) => {
|
||||
snap().markNotificationRead(id);
|
||||
return { message: 'Marked as read' };
|
||||
},
|
||||
markAllAsRead: async () => {
|
||||
snap().markAllNotificationsRead();
|
||||
return { message: 'All marked as read' };
|
||||
},
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Subscription (mock free tier)
|
||||
// -----------------------------------------------------------------------
|
||||
subscription: {
|
||||
getStatus: async () => ({
|
||||
tier: 'free',
|
||||
status: 'active',
|
||||
is_active: true,
|
||||
limits: {
|
||||
max_residences: 2,
|
||||
max_tasks_per_residence: 25,
|
||||
max_contractors: 10,
|
||||
max_documents: 20,
|
||||
can_share: false,
|
||||
can_export: false,
|
||||
},
|
||||
}),
|
||||
getFeatureBenefits: async () => [
|
||||
{ id: 1, title: 'Unlimited Residences', description: 'Track all your properties', icon: '🏠', tier: 'pro', sort_order: 1 },
|
||||
{ id: 2, title: 'Unlimited Tasks', description: 'Never hit a task limit', icon: '✅', tier: 'pro', sort_order: 2 },
|
||||
{ id: 3, title: 'Sharing', description: 'Share residences with family', icon: '👥', tier: 'pro', sort_order: 3 },
|
||||
{ id: 4, title: 'PDF Reports', description: 'Export task reports', icon: '📄', tier: 'pro', sort_order: 4 },
|
||||
],
|
||||
getUpgradeTriggers: async () => [
|
||||
{ id: 1, key: 'residence_limit', title: 'Need more properties?', description: 'Upgrade to track unlimited residences.', action_text: 'Upgrade', icon: '🏠' },
|
||||
{ id: 2, key: 'sharing', title: 'Want to share?', description: 'Upgrade to share residences with family members.', action_text: 'Upgrade', icon: '👥' },
|
||||
],
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Auth (demo user)
|
||||
// -----------------------------------------------------------------------
|
||||
auth: {
|
||||
getCurrentUser: async () => demoUser,
|
||||
logout: async () => ({ message: 'Logged out' }),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,489 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zustand demo store — in-memory CRUD for sandboxed demo experience
|
||||
// No persist middleware: data resets on page reload.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { create } from 'zustand';
|
||||
|
||||
import type { ResidenceResponse, MyResidenceResponse, TaskSummary } from '@/lib/api/residences';
|
||||
import type { TaskResponse, CompletionResponse } from '@/lib/api/tasks';
|
||||
import type { ContractorResponse } from '@/lib/api/contractors';
|
||||
import type { DocumentResponse } from '@/lib/api/documents';
|
||||
import type { NotificationResponse } from '@/lib/api/notifications';
|
||||
|
||||
import {
|
||||
demoResidences,
|
||||
demoMyResidences,
|
||||
demoTasks,
|
||||
demoCompletions,
|
||||
demoContractors,
|
||||
demoDocuments,
|
||||
demoNotifications,
|
||||
} from './mock-data';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State shape
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface DemoState {
|
||||
residences: ResidenceResponse[];
|
||||
myResidences: MyResidenceResponse[];
|
||||
tasks: TaskResponse[];
|
||||
completions: CompletionResponse[];
|
||||
contractors: ContractorResponse[];
|
||||
documents: DocumentResponse[];
|
||||
notifications: NotificationResponse[];
|
||||
|
||||
nextIds: {
|
||||
residence: number;
|
||||
task: number;
|
||||
completion: number;
|
||||
contractor: number;
|
||||
document: number;
|
||||
notification: number;
|
||||
};
|
||||
|
||||
// Residences
|
||||
addResidence: (data: Partial<ResidenceResponse>) => ResidenceResponse;
|
||||
updateResidence: (id: number, data: Partial<ResidenceResponse>) => ResidenceResponse;
|
||||
deleteResidence: (id: number) => void;
|
||||
|
||||
// Tasks
|
||||
addTask: (data: Partial<TaskResponse>) => TaskResponse;
|
||||
updateTask: (id: number, data: Partial<TaskResponse>) => TaskResponse;
|
||||
deleteTask: (id: number) => void;
|
||||
markTaskInProgress: (id: number) => TaskResponse;
|
||||
cancelTask: (id: number) => TaskResponse;
|
||||
uncancelTask: (id: number) => TaskResponse;
|
||||
archiveTask: (id: number) => TaskResponse;
|
||||
unarchiveTask: (id: number) => TaskResponse;
|
||||
completeTask: (taskId: number, data: Partial<CompletionResponse>) => CompletionResponse;
|
||||
|
||||
// Contractors
|
||||
addContractor: (data: Partial<ContractorResponse>) => ContractorResponse;
|
||||
updateContractor: (id: number, data: Partial<ContractorResponse>) => ContractorResponse;
|
||||
deleteContractor: (id: number) => void;
|
||||
toggleContractorFavorite: (id: number) => ContractorResponse;
|
||||
|
||||
// Documents
|
||||
addDocument: (data: Partial<DocumentResponse>) => DocumentResponse;
|
||||
updateDocument: (id: number, data: Partial<DocumentResponse>) => DocumentResponse;
|
||||
deleteDocument: (id: number) => void;
|
||||
|
||||
// Notifications
|
||||
markNotificationRead: (id: number) => void;
|
||||
markAllNotificationsRead: () => void;
|
||||
|
||||
// Reset
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const now = () => new Date().toISOString();
|
||||
|
||||
function buildTaskSummary(tasks: TaskResponse[], residenceId: number): TaskSummary {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const dayMs = 86400000;
|
||||
|
||||
const resTasks = tasks.filter(t => t.residence_id === residenceId && !t.is_cancelled && !t.is_archived);
|
||||
let overdue = 0, dueSoon = 0, inProgress = 0, completed = 0;
|
||||
|
||||
for (const task of resTasks) {
|
||||
if (task.in_progress) { inProgress++; continue; }
|
||||
|
||||
const isCompleted = task.completion_count > 0 && !task.next_due_date;
|
||||
const recentlyCompleted = task.last_completed_at && task.next_due_date &&
|
||||
new Date(task.next_due_date) > new Date(today.getTime() + 14 * dayMs);
|
||||
|
||||
if (isCompleted || recentlyCompleted) { completed++; continue; }
|
||||
|
||||
const dd = task.next_due_date || task.due_date;
|
||||
if (dd) {
|
||||
const dueDate = new Date(dd);
|
||||
dueDate.setHours(0, 0, 0, 0);
|
||||
const diff = Math.floor((dueDate.getTime() - today.getTime()) / dayMs);
|
||||
if (diff < 0) overdue++;
|
||||
else if (diff <= 7) dueSoon++;
|
||||
}
|
||||
}
|
||||
|
||||
return { total: resTasks.length, overdue, due_soon: dueSoon, in_progress: inProgress, completed };
|
||||
}
|
||||
|
||||
function rebuildMyResidences(residences: ResidenceResponse[], tasks: TaskResponse[]): MyResidenceResponse[] {
|
||||
return residences.map(residence => ({
|
||||
residence,
|
||||
task_summary: buildTaskSummary(tasks, residence.id),
|
||||
}));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Initial state factory (for reset)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function initialState() {
|
||||
return {
|
||||
residences: structuredClone(demoResidences),
|
||||
myResidences: structuredClone(demoMyResidences),
|
||||
tasks: structuredClone(demoTasks),
|
||||
completions: structuredClone(demoCompletions),
|
||||
contractors: structuredClone(demoContractors),
|
||||
documents: structuredClone(demoDocuments),
|
||||
notifications: structuredClone(demoNotifications),
|
||||
nextIds: {
|
||||
residence: 100,
|
||||
task: 100,
|
||||
completion: 100,
|
||||
contractor: 100,
|
||||
document: 100,
|
||||
notification: 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Store
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const useDemoStore = create<DemoState>()((set, get) => ({
|
||||
...initialState(),
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Residences
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addResidence: (data) => {
|
||||
const state = get();
|
||||
const id = state.nextIds.residence;
|
||||
const residence: ResidenceResponse = {
|
||||
id,
|
||||
name: data.name ?? 'New Residence',
|
||||
property_type_id: data.property_type_id,
|
||||
property_type: data.property_type,
|
||||
street_address: data.street_address ?? '',
|
||||
apartment_unit: data.apartment_unit ?? '',
|
||||
city: data.city ?? '',
|
||||
state_province: data.state_province ?? '',
|
||||
postal_code: data.postal_code ?? '',
|
||||
country: data.country ?? 'US',
|
||||
bedrooms: data.bedrooms,
|
||||
bathrooms: data.bathrooms,
|
||||
square_footage: data.square_footage,
|
||||
lot_size: data.lot_size,
|
||||
year_built: data.year_built,
|
||||
description: data.description ?? '',
|
||||
purchase_date: data.purchase_date,
|
||||
purchase_price: data.purchase_price,
|
||||
is_primary: data.is_primary ?? false,
|
||||
is_owner: true,
|
||||
owner_id: 1,
|
||||
user_count: 1,
|
||||
created_at: now(),
|
||||
updated_at: now(),
|
||||
};
|
||||
const newResidences = [...state.residences, residence];
|
||||
set({
|
||||
residences: newResidences,
|
||||
myResidences: rebuildMyResidences(newResidences, state.tasks),
|
||||
nextIds: { ...state.nextIds, residence: id + 1 },
|
||||
});
|
||||
return residence;
|
||||
},
|
||||
|
||||
updateResidence: (id, data) => {
|
||||
const state = get();
|
||||
const newResidences = state.residences.map(r =>
|
||||
r.id === id ? { ...r, ...data, id, updated_at: now() } : r,
|
||||
);
|
||||
const updated = newResidences.find(r => r.id === id)!;
|
||||
set({
|
||||
residences: newResidences,
|
||||
myResidences: rebuildMyResidences(newResidences, state.tasks),
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
|
||||
deleteResidence: (id) => {
|
||||
const state = get();
|
||||
const newResidences = state.residences.filter(r => r.id !== id);
|
||||
const newTasks = state.tasks.filter(t => t.residence_id !== id);
|
||||
const newDocuments = state.documents.filter(d => d.residence_id !== id);
|
||||
set({
|
||||
residences: newResidences,
|
||||
tasks: newTasks,
|
||||
documents: newDocuments,
|
||||
myResidences: rebuildMyResidences(newResidences, newTasks),
|
||||
});
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tasks
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addTask: (data) => {
|
||||
const state = get();
|
||||
const id = state.nextIds.task;
|
||||
const residence = state.residences.find(r => r.id === data.residence_id);
|
||||
const task: TaskResponse = {
|
||||
id,
|
||||
residence_id: data.residence_id ?? 1,
|
||||
residence_name: residence?.name ?? 'Unknown',
|
||||
title: data.title ?? 'New Task',
|
||||
description: data.description ?? '',
|
||||
category_id: data.category_id,
|
||||
category: data.category,
|
||||
priority_id: data.priority_id,
|
||||
priority: data.priority,
|
||||
frequency_id: data.frequency_id,
|
||||
in_progress: data.in_progress ?? false,
|
||||
is_cancelled: false,
|
||||
is_archived: false,
|
||||
due_date: data.due_date,
|
||||
next_due_date: data.due_date,
|
||||
estimated_cost: data.estimated_cost,
|
||||
contractor_id: data.contractor_id,
|
||||
contractor: data.contractor,
|
||||
completion_count: 0,
|
||||
created_by_id: 1,
|
||||
created_at: now(),
|
||||
updated_at: now(),
|
||||
};
|
||||
const newTasks = [...state.tasks, task];
|
||||
set({
|
||||
tasks: newTasks,
|
||||
myResidences: rebuildMyResidences(state.residences, newTasks),
|
||||
nextIds: { ...state.nextIds, task: id + 1 },
|
||||
});
|
||||
return task;
|
||||
},
|
||||
|
||||
updateTask: (id, data) => {
|
||||
const state = get();
|
||||
const newTasks = state.tasks.map(t =>
|
||||
t.id === id ? { ...t, ...data, id, updated_at: now() } : t,
|
||||
);
|
||||
const updated = newTasks.find(t => t.id === id)!;
|
||||
set({
|
||||
tasks: newTasks,
|
||||
myResidences: rebuildMyResidences(state.residences, newTasks),
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
|
||||
deleteTask: (id) => {
|
||||
const state = get();
|
||||
const newTasks = state.tasks.filter(t => t.id !== id);
|
||||
set({
|
||||
tasks: newTasks,
|
||||
completions: state.completions.filter(c => c.task_id !== id),
|
||||
myResidences: rebuildMyResidences(state.residences, newTasks),
|
||||
});
|
||||
},
|
||||
|
||||
markTaskInProgress: (id) => {
|
||||
return get().updateTask(id, { in_progress: true });
|
||||
},
|
||||
|
||||
cancelTask: (id) => {
|
||||
return get().updateTask(id, { is_cancelled: true, in_progress: false });
|
||||
},
|
||||
|
||||
uncancelTask: (id) => {
|
||||
return get().updateTask(id, { is_cancelled: false });
|
||||
},
|
||||
|
||||
archiveTask: (id) => {
|
||||
return get().updateTask(id, { is_archived: true });
|
||||
},
|
||||
|
||||
unarchiveTask: (id) => {
|
||||
return get().updateTask(id, { is_archived: false });
|
||||
},
|
||||
|
||||
completeTask: (taskId, data) => {
|
||||
const state = get();
|
||||
const completionId = state.nextIds.completion;
|
||||
const task = state.tasks.find(t => t.id === taskId);
|
||||
|
||||
const completion: CompletionResponse = {
|
||||
id: completionId,
|
||||
task_id: taskId,
|
||||
task_title: task?.title ?? '',
|
||||
completed_at: data.completed_at ?? now(),
|
||||
completed_by_id: 1,
|
||||
completed_by: { id: 1, username: 'demo_user', first_name: 'Demo', last_name: 'User' },
|
||||
notes: data.notes ?? '',
|
||||
actual_cost: data.actual_cost,
|
||||
rating: data.rating,
|
||||
images: data.images ?? [],
|
||||
created_at: now(),
|
||||
};
|
||||
|
||||
// Update task: increment completion_count, set last_completed_at, advance next_due_date for recurring
|
||||
const taskUpdates: Partial<TaskResponse> = {
|
||||
completion_count: (task?.completion_count ?? 0) + 1,
|
||||
last_completed_at: completion.completed_at,
|
||||
in_progress: false,
|
||||
actual_cost: data.actual_cost,
|
||||
};
|
||||
|
||||
// Advance next_due_date for recurring tasks
|
||||
if (task?.frequency_id) {
|
||||
const frequencyDays: Record<number, number> = { 1: 7, 2: 14, 3: 30, 4: 90, 5: 180, 6: 365 };
|
||||
const days = frequencyDays[task.frequency_id] ?? 30;
|
||||
const nextDate = new Date(Date.now() + days * 86400000);
|
||||
taskUpdates.next_due_date = nextDate.toISOString().split('T')[0];
|
||||
} else {
|
||||
// One-time task: clear next_due_date to mark as completed
|
||||
taskUpdates.next_due_date = undefined;
|
||||
}
|
||||
|
||||
const newTasks = state.tasks.map(t =>
|
||||
t.id === taskId ? { ...t, ...taskUpdates, updated_at: now() } : t,
|
||||
);
|
||||
|
||||
set({
|
||||
tasks: newTasks,
|
||||
completions: [...state.completions, completion],
|
||||
myResidences: rebuildMyResidences(state.residences, newTasks),
|
||||
nextIds: { ...state.nextIds, completion: completionId + 1 },
|
||||
});
|
||||
|
||||
return completion;
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Contractors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addContractor: (data) => {
|
||||
const state = get();
|
||||
const id = state.nextIds.contractor;
|
||||
const contractor: ContractorResponse = {
|
||||
id,
|
||||
name: data.name ?? 'New Contractor',
|
||||
company: data.company ?? '',
|
||||
phone: data.phone ?? '',
|
||||
email: data.email ?? '',
|
||||
website: data.website ?? '',
|
||||
notes: data.notes ?? '',
|
||||
street_address: data.street_address ?? '',
|
||||
city: data.city ?? '',
|
||||
state_province: data.state_province ?? '',
|
||||
postal_code: data.postal_code ?? '',
|
||||
specialties: data.specialties ?? [],
|
||||
rating: data.rating,
|
||||
is_favorite: data.is_favorite ?? false,
|
||||
task_count: 0,
|
||||
created_by_id: 1,
|
||||
created_at: now(),
|
||||
updated_at: now(),
|
||||
};
|
||||
set({
|
||||
contractors: [...state.contractors, contractor],
|
||||
nextIds: { ...state.nextIds, contractor: id + 1 },
|
||||
});
|
||||
return contractor;
|
||||
},
|
||||
|
||||
updateContractor: (id, data) => {
|
||||
const state = get();
|
||||
const newContractors = state.contractors.map(c =>
|
||||
c.id === id ? { ...c, ...data, id, updated_at: now() } : c,
|
||||
);
|
||||
set({ contractors: newContractors });
|
||||
return newContractors.find(c => c.id === id)!;
|
||||
},
|
||||
|
||||
deleteContractor: (id) => {
|
||||
set(state => ({ contractors: state.contractors.filter(c => c.id !== id) }));
|
||||
},
|
||||
|
||||
toggleContractorFavorite: (id) => {
|
||||
const state = get();
|
||||
const contractor = state.contractors.find(c => c.id === id);
|
||||
if (!contractor) throw new Error(`Contractor ${id} not found`);
|
||||
return get().updateContractor(id, { is_favorite: !contractor.is_favorite });
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Documents
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addDocument: (data) => {
|
||||
const state = get();
|
||||
const id = state.nextIds.document;
|
||||
const residence = state.residences.find(r => r.id === data.residence_id);
|
||||
const document: DocumentResponse = {
|
||||
id,
|
||||
residence_id: data.residence_id ?? 1,
|
||||
residence_name: residence?.name ?? 'Unknown',
|
||||
title: data.title ?? 'New Document',
|
||||
description: data.description ?? '',
|
||||
document_type: data.document_type ?? 'general',
|
||||
file_url: data.file_url ?? '',
|
||||
file_name: data.file_name ?? '',
|
||||
file_size: data.file_size,
|
||||
mime_type: data.mime_type ?? 'application/pdf',
|
||||
purchase_date: data.purchase_date,
|
||||
expiry_date: data.expiry_date,
|
||||
purchase_price: data.purchase_price,
|
||||
vendor: data.vendor ?? '',
|
||||
serial_number: data.serial_number ?? '',
|
||||
model_number: data.model_number ?? '',
|
||||
is_active: data.is_active ?? true,
|
||||
images: data.images ?? [],
|
||||
created_by: data.created_by ?? { id: 1, username: 'demo_user', first_name: 'Demo', last_name: 'User' },
|
||||
created_at: now(),
|
||||
updated_at: now(),
|
||||
};
|
||||
set({
|
||||
documents: [...state.documents, document],
|
||||
nextIds: { ...state.nextIds, document: id + 1 },
|
||||
});
|
||||
return document;
|
||||
},
|
||||
|
||||
updateDocument: (id, data) => {
|
||||
const state = get();
|
||||
const newDocuments = state.documents.map(d =>
|
||||
d.id === id ? { ...d, ...data, id, updated_at: now() } : d,
|
||||
);
|
||||
set({ documents: newDocuments });
|
||||
return newDocuments.find(d => d.id === id)!;
|
||||
},
|
||||
|
||||
deleteDocument: (id) => {
|
||||
set(state => ({ documents: state.documents.filter(d => d.id !== id) }));
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Notifications
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
markNotificationRead: (id) => {
|
||||
set(state => ({
|
||||
notifications: state.notifications.map(n =>
|
||||
n.id === id ? { ...n, is_read: true } : n,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
markAllNotificationsRead: () => {
|
||||
set(state => ({
|
||||
notifications: state.notifications.map(n => ({ ...n, is_read: true })),
|
||||
}));
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Reset
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
reset: () => {
|
||||
set(initialState());
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { ContractorResponse } from '@/lib/api/contractors';
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
export const demoContractors: ContractorResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Mike Johnson',
|
||||
company: 'Johnson Plumbing',
|
||||
phone: '(555) 123-4567',
|
||||
email: 'mike@johnsonplumbing.com',
|
||||
website: 'https://johnsonplumbing.example.com',
|
||||
notes: 'Very reliable, usually available within 48 hours.',
|
||||
street_address: '456 Oak Ave',
|
||||
city: 'Springfield',
|
||||
state_province: 'IL',
|
||||
postal_code: '62704',
|
||||
specialties: [{ id: 1, name: 'Plumber', icon: '🔧' }],
|
||||
rating: 5,
|
||||
is_favorite: true,
|
||||
task_count: 3,
|
||||
created_by_id: 1,
|
||||
created_at: '2024-02-01T10:00:00Z',
|
||||
updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Sarah Chen',
|
||||
company: 'Bright Spark Electric',
|
||||
phone: '(555) 234-5678',
|
||||
email: 'sarah@brightspark.com',
|
||||
website: '',
|
||||
notes: 'Licensed master electrician. Great with older homes.',
|
||||
street_address: '',
|
||||
city: 'Springfield',
|
||||
state_province: 'IL',
|
||||
postal_code: '62702',
|
||||
specialties: [{ id: 2, name: 'Electrician', icon: '⚡' }],
|
||||
rating: 4,
|
||||
is_favorite: true,
|
||||
task_count: 1,
|
||||
created_by_id: 1,
|
||||
created_at: '2024-03-15T10:00:00Z',
|
||||
updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Carlos Rivera',
|
||||
company: 'Rivera HVAC Solutions',
|
||||
phone: '(555) 345-6789',
|
||||
email: 'carlos@riverahvac.com',
|
||||
website: 'https://riverahvac.example.com',
|
||||
notes: 'Specializes in high-efficiency systems.',
|
||||
street_address: '789 Pine St',
|
||||
city: 'Springfield',
|
||||
state_province: 'IL',
|
||||
postal_code: '62703',
|
||||
specialties: [{ id: 3, name: 'HVAC Technician', icon: '❄️' }],
|
||||
rating: 4,
|
||||
is_favorite: false,
|
||||
task_count: 2,
|
||||
created_by_id: 1,
|
||||
created_at: '2024-04-01T10:00:00Z',
|
||||
updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Lisa Park',
|
||||
company: 'Green Thumb Landscaping',
|
||||
phone: '(555) 456-7890',
|
||||
email: 'lisa@greenthumb.com',
|
||||
website: '',
|
||||
notes: 'Organic lawn care specialist.',
|
||||
street_address: '',
|
||||
city: 'Springfield',
|
||||
state_province: 'IL',
|
||||
postal_code: '62704',
|
||||
specialties: [{ id: 4, name: 'Landscaper', icon: '🌿' }],
|
||||
rating: 5,
|
||||
is_favorite: false,
|
||||
task_count: 0,
|
||||
created_by_id: 1,
|
||||
created_at: '2024-05-01T10:00:00Z',
|
||||
updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Tom Williams',
|
||||
company: 'Williams Roofing',
|
||||
phone: '(555) 567-8901',
|
||||
email: 'tom@williamsroofing.com',
|
||||
website: 'https://williamsroofing.example.com',
|
||||
notes: '20 years experience. Does inspections and repairs.',
|
||||
street_address: '321 Elm St',
|
||||
city: 'Springfield',
|
||||
state_province: 'IL',
|
||||
postal_code: '62701',
|
||||
specialties: [
|
||||
{ id: 7, name: 'Roofer', icon: '🏠' },
|
||||
{ id: 6, name: 'General Contractor', icon: '🔨' },
|
||||
],
|
||||
rating: 4,
|
||||
is_favorite: false,
|
||||
task_count: 1,
|
||||
created_by_id: 1,
|
||||
created_at: '2024-06-01T10:00:00Z',
|
||||
updated_at: now,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { DocumentResponse } from '@/lib/api/documents';
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
function daysFromNow(days: number): string {
|
||||
return new Date(Date.now() + days * 86400000).toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
const creator = { id: 1, username: 'demo_user', first_name: 'Demo', last_name: 'User' };
|
||||
|
||||
export const demoDocuments: DocumentResponse[] = [
|
||||
// Warranties (4)
|
||||
{
|
||||
id: 1, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'HVAC System Warranty', description: 'Carrier furnace and AC unit 10-year warranty',
|
||||
document_type: 'warranty',
|
||||
file_url: '', file_name: 'hvac-warranty.pdf', mime_type: 'application/pdf',
|
||||
purchase_date: '2022-08-15', expiry_date: daysFromNow(2000),
|
||||
purchase_price: 8500, vendor: 'Rivera HVAC Solutions',
|
||||
serial_number: 'CR-2022-88441', model_number: 'Carrier 24ACC636',
|
||||
is_active: true, images: [],
|
||||
created_by: creator, created_at: '2024-01-20T10:00:00Z', updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 2, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Roof Warranty', description: 'Architectural shingle 25-year warranty',
|
||||
document_type: 'warranty',
|
||||
file_url: '', file_name: 'roof-warranty.pdf', mime_type: 'application/pdf',
|
||||
purchase_date: '2020-05-10', expiry_date: daysFromNow(6500),
|
||||
purchase_price: 12000, vendor: 'Williams Roofing',
|
||||
serial_number: '', model_number: 'GAF Timberline HDZ',
|
||||
is_active: true, images: [],
|
||||
created_by: creator, created_at: '2024-01-20T10:00:00Z', updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 3, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Dishwasher Warranty', description: 'Bosch dishwasher 2-year warranty',
|
||||
document_type: 'warranty',
|
||||
file_url: '', file_name: 'dishwasher-warranty.pdf', mime_type: 'application/pdf',
|
||||
purchase_date: '2023-11-01', expiry_date: daysFromNow(-30),
|
||||
purchase_price: 850, vendor: 'Home Depot',
|
||||
serial_number: 'BSH-44210', model_number: 'Bosch 500 Series',
|
||||
is_active: true, images: [],
|
||||
created_by: creator, created_at: '2024-01-20T10:00:00Z', updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 4, residence_id: 2, residence_name: 'Downtown Apartment',
|
||||
title: 'Window AC Warranty', description: 'Portable AC unit warranty',
|
||||
document_type: 'warranty',
|
||||
file_url: '', file_name: 'ac-warranty.pdf', mime_type: 'application/pdf',
|
||||
purchase_date: '2024-06-01', expiry_date: daysFromNow(90),
|
||||
purchase_price: 350, vendor: 'Amazon',
|
||||
serial_number: 'LG-P1234', model_number: 'LG LP0821GSSM',
|
||||
is_active: true, images: [],
|
||||
created_by: creator, created_at: '2024-06-02T10:00:00Z', updated_at: now,
|
||||
},
|
||||
|
||||
// General documents (4)
|
||||
{
|
||||
id: 5, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Home Insurance Policy', description: 'Annual homeowners insurance policy document',
|
||||
document_type: 'insurance',
|
||||
file_url: '', file_name: 'home-insurance-2024.pdf', mime_type: 'application/pdf',
|
||||
expiry_date: daysFromNow(200),
|
||||
vendor: 'State Farm',
|
||||
serial_number: '', model_number: '',
|
||||
is_active: true, images: [],
|
||||
created_by: creator, created_at: '2024-01-05T10:00:00Z', updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 6, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Plumbing Repair Receipt', description: 'Receipt for kitchen faucet replacement',
|
||||
document_type: 'receipt',
|
||||
file_url: '', file_name: 'plumbing-receipt.pdf', mime_type: 'application/pdf',
|
||||
purchase_date: '2025-02-15', purchase_price: 175,
|
||||
vendor: 'Johnson Plumbing',
|
||||
serial_number: '', model_number: '',
|
||||
is_active: true, images: [],
|
||||
created_by: creator, created_at: '2025-02-15T10:00:00Z', updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 7, residence_id: 2, residence_name: 'Downtown Apartment',
|
||||
title: 'Lease Agreement', description: 'Current rental lease agreement',
|
||||
document_type: 'contract',
|
||||
file_url: '', file_name: 'lease-2024.pdf', mime_type: 'application/pdf',
|
||||
expiry_date: daysFromNow(180),
|
||||
vendor: '', serial_number: '', model_number: '',
|
||||
is_active: true, images: [],
|
||||
created_by: creator, created_at: '2024-03-01T10:00:00Z', updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 8, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Furnace Manual', description: 'Carrier furnace owner manual and specs',
|
||||
document_type: 'manual',
|
||||
file_url: '', file_name: 'furnace-manual.pdf', mime_type: 'application/pdf',
|
||||
vendor: 'Carrier', serial_number: '', model_number: 'Carrier 59SC5',
|
||||
is_active: true, images: [],
|
||||
created_by: creator, created_at: '2024-01-20T10:00:00Z', updated_at: now,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
export { demoUser } from './user';
|
||||
export { demoStaticData } from './lookups';
|
||||
export { demoResidences, demoMyResidences } from './residences';
|
||||
export { demoTasks, buildKanbanResponse, demoCompletions } from './tasks';
|
||||
export { demoContractors } from './contractors';
|
||||
export { demoDocuments } from './documents';
|
||||
export { demoNotifications } from './notifications';
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { StaticDataResponse } from '@/lib/api/lookups';
|
||||
|
||||
export const demoStaticData: StaticDataResponse = {
|
||||
residence_types: [
|
||||
{ id: 1, name: 'House', icon: '🏠' },
|
||||
{ id: 2, name: 'Apartment', icon: '🏢' },
|
||||
{ id: 3, name: 'Condo', icon: '🏙️' },
|
||||
{ id: 4, name: 'Townhouse', icon: '🏘️' },
|
||||
{ id: 5, name: 'Cabin', icon: '🏕️' },
|
||||
],
|
||||
task_categories: [
|
||||
{ id: 1, name: 'Plumbing', icon: '🔧', color: '#3B82F6' },
|
||||
{ id: 2, name: 'Electrical', icon: '⚡', color: '#F59E0B' },
|
||||
{ id: 3, name: 'HVAC', icon: '❄️', color: '#6366F1' },
|
||||
{ id: 4, name: 'Landscaping', icon: '🌿', color: '#22C55E' },
|
||||
{ id: 5, name: 'Cleaning', icon: '🧹', color: '#EC4899' },
|
||||
{ id: 6, name: 'Painting', icon: '🎨', color: '#8B5CF6' },
|
||||
{ id: 7, name: 'Appliance', icon: '🔌', color: '#F97316' },
|
||||
{ id: 8, name: 'Exterior', icon: '🏡', color: '#14B8A6' },
|
||||
{ id: 9, name: 'Safety', icon: '🛡️', color: '#EF4444' },
|
||||
{ id: 10, name: 'General', icon: '🔨', color: '#6B7280' },
|
||||
],
|
||||
task_priorities: [
|
||||
{ id: 1, name: 'Low', icon: '🟢', color: '#22C55E', sort_order: 1 },
|
||||
{ id: 2, name: 'Medium', icon: '🟡', color: '#F59E0B', sort_order: 2 },
|
||||
{ id: 3, name: 'High', icon: '🟠', color: '#F97316', sort_order: 3 },
|
||||
{ id: 4, name: 'Urgent', icon: '🔴', color: '#EF4444', sort_order: 4 },
|
||||
],
|
||||
task_frequencies: [
|
||||
{ id: 1, name: 'Weekly', days: 7, description: 'Every week' },
|
||||
{ id: 2, name: 'Biweekly', days: 14, description: 'Every 2 weeks' },
|
||||
{ id: 3, name: 'Monthly', days: 30, description: 'Every month' },
|
||||
{ id: 4, name: 'Quarterly', days: 90, description: 'Every 3 months' },
|
||||
{ id: 5, name: 'Semi-Annually', days: 180, description: 'Every 6 months' },
|
||||
{ id: 6, name: 'Annually', days: 365, description: 'Every year' },
|
||||
],
|
||||
contractor_specialties: [
|
||||
{ id: 1, name: 'Plumber', icon: '🔧' },
|
||||
{ id: 2, name: 'Electrician', icon: '⚡' },
|
||||
{ id: 3, name: 'HVAC Technician', icon: '❄️' },
|
||||
{ id: 4, name: 'Landscaper', icon: '🌿' },
|
||||
{ id: 5, name: 'Painter', icon: '🎨' },
|
||||
{ id: 6, name: 'General Contractor', icon: '🔨' },
|
||||
{ id: 7, name: 'Roofer', icon: '🏠' },
|
||||
{ id: 8, name: 'Cleaner', icon: '🧹' },
|
||||
],
|
||||
task_templates: {
|
||||
groups: [
|
||||
{
|
||||
category: { id: 1, name: 'Plumbing', icon: '🔧', color: '#3B82F6' },
|
||||
templates: [
|
||||
{ id: 1, title: 'Check for leaks', description: 'Inspect pipes and faucets for leaks', category_id: 1, priority_id: 2, frequency_id: 4 },
|
||||
{ id: 2, title: 'Drain water heater', description: 'Flush sediment from water heater', category_id: 1, priority_id: 2, frequency_id: 6 },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: { id: 3, name: 'HVAC', icon: '❄️', color: '#6366F1' },
|
||||
templates: [
|
||||
{ id: 3, title: 'Replace HVAC filter', description: 'Replace air filters in HVAC system', category_id: 3, priority_id: 2, frequency_id: 3 },
|
||||
{ id: 4, title: 'Schedule HVAC tune-up', description: 'Annual HVAC system maintenance', category_id: 3, priority_id: 2, frequency_id: 6 },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: { id: 4, name: 'Landscaping', icon: '🌿', color: '#22C55E' },
|
||||
templates: [
|
||||
{ id: 5, title: 'Mow the lawn', description: 'Cut grass to proper height', category_id: 4, priority_id: 1, frequency_id: 1 },
|
||||
],
|
||||
},
|
||||
],
|
||||
total_count: 5,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { NotificationResponse } from '@/lib/api/notifications';
|
||||
|
||||
const now = new Date();
|
||||
const dayMs = 86400000;
|
||||
|
||||
function hoursAgo(hours: number): string {
|
||||
return new Date(now.getTime() - hours * 3600000).toISOString();
|
||||
}
|
||||
|
||||
function daysAgo(days: number): string {
|
||||
return new Date(now.getTime() - days * dayMs).toISOString();
|
||||
}
|
||||
|
||||
export const demoNotifications: NotificationResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task Overdue',
|
||||
body: 'Replace HVAC filter is 5 days overdue.',
|
||||
notification_type: 'task_reminder',
|
||||
is_read: false,
|
||||
data: { task_id: 1 },
|
||||
created_at: hoursAgo(2),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Task Overdue',
|
||||
body: 'Fix leaking faucet is 2 days overdue.',
|
||||
notification_type: 'task_reminder',
|
||||
is_read: false,
|
||||
data: { task_id: 2 },
|
||||
created_at: hoursAgo(5),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Task Due Today',
|
||||
body: 'Test smoke detectors is due today.',
|
||||
notification_type: 'task_reminder',
|
||||
is_read: false,
|
||||
data: { task_id: 3 },
|
||||
created_at: hoursAgo(8),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Task Completed',
|
||||
body: 'Replace kitchen faucet was marked as complete.',
|
||||
notification_type: 'task_completion',
|
||||
is_read: false,
|
||||
data: { task_id: 13, completion_id: 1 },
|
||||
created_at: daysAgo(3),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Warranty Expiring',
|
||||
body: 'Dishwasher Warranty expired 30 days ago.',
|
||||
notification_type: 'warranty_expiry',
|
||||
is_read: true,
|
||||
data: { document_id: 3 },
|
||||
created_at: daysAgo(5),
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'Task Completed',
|
||||
body: 'Deep clean bathroom was marked as complete.',
|
||||
notification_type: 'task_completion',
|
||||
is_read: true,
|
||||
data: { task_id: 14, completion_id: 2 },
|
||||
created_at: daysAgo(1),
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: 'Task Due Soon',
|
||||
body: 'Mow the lawn is due in 3 days.',
|
||||
notification_type: 'task_reminder',
|
||||
is_read: true,
|
||||
data: { task_id: 5 },
|
||||
created_at: daysAgo(1),
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: 'Warranty Expiring Soon',
|
||||
body: 'Window AC Warranty expires in 90 days.',
|
||||
notification_type: 'warranty_expiry',
|
||||
is_read: true,
|
||||
data: { document_id: 4 },
|
||||
created_at: daysAgo(7),
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: 'Welcome to Casera!',
|
||||
body: 'Start by adding your first residence to track home maintenance.',
|
||||
notification_type: 'system',
|
||||
is_read: true,
|
||||
created_at: daysAgo(30),
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: 'Task Completed',
|
||||
body: 'Trim hedges was marked as complete.',
|
||||
notification_type: 'task_completion',
|
||||
is_read: true,
|
||||
data: { task_id: 15, completion_id: 3 },
|
||||
created_at: daysAgo(2),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { ResidenceResponse, MyResidenceResponse } from '@/lib/api/residences';
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
export const demoResidences: ResidenceResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Maple Street House',
|
||||
property_type_id: 1,
|
||||
property_type: { id: 1, name: 'House', icon: '🏠' },
|
||||
street_address: '742 Maple Street',
|
||||
apartment_unit: '',
|
||||
city: 'Springfield',
|
||||
state_province: 'IL',
|
||||
postal_code: '62704',
|
||||
country: 'US',
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
square_footage: 1800,
|
||||
lot_size: 6500,
|
||||
year_built: 1995,
|
||||
description: 'A charming 3-bedroom home with a spacious backyard and updated kitchen.',
|
||||
purchase_date: '2022-06-15',
|
||||
purchase_price: 285000,
|
||||
is_primary: true,
|
||||
is_owner: true,
|
||||
owner_id: 1,
|
||||
user_count: 1,
|
||||
created_at: '2024-01-15T10:00:00Z',
|
||||
updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Downtown Apartment',
|
||||
property_type_id: 2,
|
||||
property_type: { id: 2, name: 'Apartment', icon: '🏢' },
|
||||
street_address: '100 Main Street',
|
||||
apartment_unit: 'Apt 4B',
|
||||
city: 'Springfield',
|
||||
state_province: 'IL',
|
||||
postal_code: '62701',
|
||||
country: 'US',
|
||||
bedrooms: 1,
|
||||
bathrooms: 1,
|
||||
square_footage: 750,
|
||||
description: 'A cozy downtown apartment close to shops and restaurants.',
|
||||
is_primary: false,
|
||||
is_owner: true,
|
||||
owner_id: 1,
|
||||
user_count: 2,
|
||||
created_at: '2024-03-01T10:00:00Z',
|
||||
updated_at: now,
|
||||
},
|
||||
];
|
||||
|
||||
export const demoMyResidences: MyResidenceResponse[] = [
|
||||
{
|
||||
residence: demoResidences[0],
|
||||
task_summary: { total: 10, overdue: 2, due_soon: 3, in_progress: 1, completed: 2 },
|
||||
},
|
||||
{
|
||||
residence: demoResidences[1],
|
||||
task_summary: { total: 5, overdue: 0, due_soon: 0, in_progress: 0, completed: 1 },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,280 @@
|
||||
import type { TaskResponse, KanbanResponse, CompletionResponse } from '@/lib/api/tasks';
|
||||
|
||||
const now = new Date();
|
||||
const dayMs = 86400000;
|
||||
|
||||
function daysFromNow(days: number): string {
|
||||
return new Date(now.getTime() + days * dayMs).toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
function daysAgo(days: number): string {
|
||||
return new Date(now.getTime() - days * dayMs).toISOString();
|
||||
}
|
||||
|
||||
export const demoTasks: TaskResponse[] = [
|
||||
// Overdue (2)
|
||||
{
|
||||
id: 1, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Replace HVAC filter', description: 'Replace the air filter in the furnace',
|
||||
category_id: 3, category: { id: 3, name: 'HVAC', icon: '❄️', color: '#6366F1' },
|
||||
priority_id: 3, priority: { id: 3, name: 'High', icon: '🟠', color: '#F97316' },
|
||||
frequency_id: 3, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(-5), next_due_date: daysFromNow(-5),
|
||||
completion_count: 2, last_completed_at: daysAgo(35),
|
||||
created_by_id: 1, created_at: daysAgo(60), updated_at: daysAgo(5),
|
||||
},
|
||||
{
|
||||
id: 2, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Fix leaking faucet', description: 'Kitchen faucet drips when turned off',
|
||||
category_id: 1, category: { id: 1, name: 'Plumbing', icon: '🔧', color: '#3B82F6' },
|
||||
priority_id: 4, priority: { id: 4, name: 'Urgent', icon: '🔴', color: '#EF4444' },
|
||||
in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(-2), next_due_date: daysFromNow(-2),
|
||||
estimated_cost: 150, contractor_id: 1,
|
||||
contractor: { id: 1, name: 'Mike Johnson', company: 'Johnson Plumbing' },
|
||||
completion_count: 0,
|
||||
created_by_id: 1, created_at: daysAgo(10), updated_at: daysAgo(2),
|
||||
},
|
||||
|
||||
// Due Today (2)
|
||||
{
|
||||
id: 3, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Test smoke detectors', description: 'Monthly smoke detector test',
|
||||
category_id: 9, category: { id: 9, name: 'Safety', icon: '🛡️', color: '#EF4444' },
|
||||
priority_id: 3, priority: { id: 3, name: 'High', icon: '🟠', color: '#F97316' },
|
||||
frequency_id: 3, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(0), next_due_date: daysFromNow(0),
|
||||
completion_count: 5,
|
||||
created_by_id: 1, created_at: daysAgo(180), updated_at: daysAgo(0),
|
||||
},
|
||||
{
|
||||
id: 4, residence_id: 2, residence_name: 'Downtown Apartment',
|
||||
title: 'Clean kitchen appliances', description: 'Deep clean oven, microwave, and fridge',
|
||||
category_id: 5, category: { id: 5, name: 'Cleaning', icon: '🧹', color: '#EC4899' },
|
||||
priority_id: 2, priority: { id: 2, name: 'Medium', icon: '🟡', color: '#F59E0B' },
|
||||
frequency_id: 3, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(0), next_due_date: daysFromNow(0),
|
||||
completion_count: 3,
|
||||
created_by_id: 1, created_at: daysAgo(90), updated_at: daysAgo(0),
|
||||
},
|
||||
|
||||
// Due Soon (3)
|
||||
{
|
||||
id: 5, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Mow the lawn', description: 'Front and back yard',
|
||||
category_id: 4, category: { id: 4, name: 'Landscaping', icon: '🌿', color: '#22C55E' },
|
||||
priority_id: 2, priority: { id: 2, name: 'Medium', icon: '🟡', color: '#F59E0B' },
|
||||
frequency_id: 1, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(3), next_due_date: daysFromNow(3),
|
||||
completion_count: 10,
|
||||
created_by_id: 1, created_at: daysAgo(120), updated_at: daysAgo(4),
|
||||
},
|
||||
{
|
||||
id: 6, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Clean gutters', description: 'Remove leaves and debris from gutters',
|
||||
category_id: 8, category: { id: 8, name: 'Exterior', icon: '🏡', color: '#14B8A6' },
|
||||
priority_id: 2, priority: { id: 2, name: 'Medium', icon: '🟡', color: '#F59E0B' },
|
||||
frequency_id: 5, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(5), next_due_date: daysFromNow(5),
|
||||
estimated_cost: 200,
|
||||
completion_count: 1,
|
||||
created_by_id: 1, created_at: daysAgo(200), updated_at: daysAgo(5),
|
||||
},
|
||||
{
|
||||
id: 7, residence_id: 2, residence_name: 'Downtown Apartment',
|
||||
title: 'Check fire extinguisher', description: 'Verify pressure gauge and expiration',
|
||||
category_id: 9, category: { id: 9, name: 'Safety', icon: '🛡️', color: '#EF4444' },
|
||||
priority_id: 3, priority: { id: 3, name: 'High', icon: '🟠', color: '#F97316' },
|
||||
frequency_id: 6, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(7), next_due_date: daysFromNow(7),
|
||||
completion_count: 0,
|
||||
created_by_id: 1, created_at: daysAgo(30), updated_at: daysAgo(7),
|
||||
},
|
||||
|
||||
// Upcoming (4)
|
||||
{
|
||||
id: 8, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Service water heater', description: 'Annual flush and inspection',
|
||||
category_id: 1, category: { id: 1, name: 'Plumbing', icon: '🔧', color: '#3B82F6' },
|
||||
priority_id: 2, priority: { id: 2, name: 'Medium', icon: '🟡', color: '#F59E0B' },
|
||||
frequency_id: 6, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(45), next_due_date: daysFromNow(45),
|
||||
estimated_cost: 200, contractor_id: 1,
|
||||
contractor: { id: 1, name: 'Mike Johnson', company: 'Johnson Plumbing' },
|
||||
completion_count: 1,
|
||||
created_by_id: 1, created_at: daysAgo(365), updated_at: daysAgo(45),
|
||||
},
|
||||
{
|
||||
id: 9, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Paint exterior trim', description: 'Touch up paint on window and door trim',
|
||||
category_id: 6, category: { id: 6, name: 'Painting', icon: '🎨', color: '#8B5CF6' },
|
||||
priority_id: 1, priority: { id: 1, name: 'Low', icon: '🟢', color: '#22C55E' },
|
||||
in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(60), next_due_date: daysFromNow(60),
|
||||
estimated_cost: 500,
|
||||
completion_count: 0,
|
||||
created_by_id: 1, created_at: daysAgo(20), updated_at: daysAgo(20),
|
||||
},
|
||||
{
|
||||
id: 10, residence_id: 2, residence_name: 'Downtown Apartment',
|
||||
title: 'Replace light bulbs', description: 'Switch to LED bulbs in all fixtures',
|
||||
category_id: 2, category: { id: 2, name: 'Electrical', icon: '⚡', color: '#F59E0B' },
|
||||
priority_id: 1, priority: { id: 1, name: 'Low', icon: '🟢', color: '#22C55E' },
|
||||
in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(30), next_due_date: daysFromNow(30),
|
||||
estimated_cost: 40,
|
||||
completion_count: 0,
|
||||
created_by_id: 1, created_at: daysAgo(5), updated_at: daysAgo(5),
|
||||
},
|
||||
{
|
||||
id: 11, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Inspect roof', description: 'Annual roof inspection for damage',
|
||||
category_id: 8, category: { id: 8, name: 'Exterior', icon: '🏡', color: '#14B8A6' },
|
||||
priority_id: 2, priority: { id: 2, name: 'Medium', icon: '🟡', color: '#F59E0B' },
|
||||
frequency_id: 6, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(90), next_due_date: daysFromNow(90),
|
||||
estimated_cost: 300, contractor_id: 5,
|
||||
contractor: { id: 5, name: 'Tom Williams', company: 'Williams Roofing' },
|
||||
completion_count: 1,
|
||||
created_by_id: 1, created_at: daysAgo(300), updated_at: daysAgo(90),
|
||||
},
|
||||
|
||||
// In Progress (1)
|
||||
{
|
||||
id: 12, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Repair deck boards', description: 'Replace warped deck boards on back patio',
|
||||
category_id: 8, category: { id: 8, name: 'Exterior', icon: '🏡', color: '#14B8A6' },
|
||||
priority_id: 3, priority: { id: 3, name: 'High', icon: '🟠', color: '#F97316' },
|
||||
in_progress: true, is_cancelled: false, is_archived: false,
|
||||
due_date: daysFromNow(10), next_due_date: daysFromNow(10),
|
||||
estimated_cost: 400,
|
||||
completion_count: 0,
|
||||
created_by_id: 1, created_at: daysAgo(14), updated_at: daysAgo(3),
|
||||
},
|
||||
|
||||
// Completed (3)
|
||||
{
|
||||
id: 13, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Replace kitchen faucet', description: 'Install new pull-down sprayer faucet',
|
||||
category_id: 1, category: { id: 1, name: 'Plumbing', icon: '🔧', color: '#3B82F6' },
|
||||
priority_id: 2, priority: { id: 2, name: 'Medium', icon: '🟡', color: '#F59E0B' },
|
||||
in_progress: false, is_cancelled: false, is_archived: false,
|
||||
completion_count: 1, last_completed_at: daysAgo(3),
|
||||
actual_cost: 175,
|
||||
created_by_id: 1, created_at: daysAgo(30), updated_at: daysAgo(3),
|
||||
},
|
||||
{
|
||||
id: 14, residence_id: 2, residence_name: 'Downtown Apartment',
|
||||
title: 'Deep clean bathroom', description: 'Scrub tiles, clean grout, organize cabinets',
|
||||
category_id: 5, category: { id: 5, name: 'Cleaning', icon: '🧹', color: '#EC4899' },
|
||||
priority_id: 1, priority: { id: 1, name: 'Low', icon: '🟢', color: '#22C55E' },
|
||||
frequency_id: 3, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
completion_count: 4, last_completed_at: daysAgo(1),
|
||||
next_due_date: daysFromNow(29),
|
||||
created_by_id: 1, created_at: daysAgo(120), updated_at: daysAgo(1),
|
||||
},
|
||||
{
|
||||
id: 15, residence_id: 1, residence_name: 'Maple Street House',
|
||||
title: 'Trim hedges', description: 'Shape front yard hedges',
|
||||
category_id: 4, category: { id: 4, name: 'Landscaping', icon: '🌿', color: '#22C55E' },
|
||||
priority_id: 1, priority: { id: 1, name: 'Low', icon: '🟢', color: '#22C55E' },
|
||||
frequency_id: 3, in_progress: false, is_cancelled: false, is_archived: false,
|
||||
completion_count: 6, last_completed_at: daysAgo(2),
|
||||
next_due_date: daysFromNow(28),
|
||||
created_by_id: 1, created_at: daysAgo(180), updated_at: daysAgo(2),
|
||||
},
|
||||
];
|
||||
|
||||
// Build kanban columns from tasks
|
||||
export function buildKanbanResponse(tasks: TaskResponse[]): KanbanResponse {
|
||||
const overdue: TaskResponse[] = [];
|
||||
const dueSoon: TaskResponse[] = [];
|
||||
const upcoming: TaskResponse[] = [];
|
||||
const inProgress: TaskResponse[] = [];
|
||||
const completed: TaskResponse[] = [];
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.is_cancelled || task.is_archived) continue;
|
||||
|
||||
// Completed: has completions and no next_due_date (one-time), or last_completed recently
|
||||
const isCompleted = task.completion_count > 0 && !task.next_due_date;
|
||||
// Also treat as completed if last_completed_at is recent and task.next_due_date is far out
|
||||
const recentlyCompleted = task.last_completed_at && task.next_due_date &&
|
||||
new Date(task.next_due_date) > new Date(today.getTime() + 14 * dayMs);
|
||||
|
||||
if (task.in_progress) {
|
||||
inProgress.push(task);
|
||||
} else if (isCompleted || recentlyCompleted) {
|
||||
completed.push(task);
|
||||
} else if (task.next_due_date || task.due_date) {
|
||||
const dueDate = new Date(task.next_due_date || task.due_date!);
|
||||
dueDate.setHours(0, 0, 0, 0);
|
||||
const diffDays = Math.floor((dueDate.getTime() - today.getTime()) / dayMs);
|
||||
|
||||
if (diffDays < 0) {
|
||||
overdue.push(task);
|
||||
} else if (diffDays <= 7) {
|
||||
dueSoon.push(task);
|
||||
} else {
|
||||
upcoming.push(task);
|
||||
}
|
||||
} else {
|
||||
upcoming.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ name: 'overdue', display_name: 'Overdue', count: overdue.length, tasks: overdue },
|
||||
{ name: 'due_soon', display_name: 'Due Soon', count: dueSoon.length, tasks: dueSoon },
|
||||
{ name: 'upcoming', display_name: 'Upcoming', count: upcoming.length, tasks: upcoming },
|
||||
{ name: 'in_progress', display_name: 'In Progress', count: inProgress.length, tasks: inProgress },
|
||||
{ name: 'completed', display_name: 'Completed', count: completed.length, tasks: completed },
|
||||
];
|
||||
|
||||
return {
|
||||
columns,
|
||||
total_summary: {
|
||||
total: tasks.filter(t => !t.is_cancelled && !t.is_archived).length,
|
||||
overdue: overdue.length,
|
||||
due_soon: dueSoon.length,
|
||||
upcoming: upcoming.length,
|
||||
in_progress: inProgress.length,
|
||||
completed: completed.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const demoCompletions: CompletionResponse[] = [
|
||||
{
|
||||
id: 1, task_id: 13, task_title: 'Replace kitchen faucet',
|
||||
completed_at: daysAgo(3),
|
||||
completed_by_id: 1,
|
||||
completed_by: { id: 1, username: 'demo_user', first_name: 'Demo', last_name: 'User' },
|
||||
notes: 'Installed new Moen pull-down sprayer. Works great!',
|
||||
actual_cost: 175,
|
||||
rating: 5,
|
||||
images: [],
|
||||
created_at: daysAgo(3),
|
||||
},
|
||||
{
|
||||
id: 2, task_id: 14, task_title: 'Deep clean bathroom',
|
||||
completed_at: daysAgo(1),
|
||||
completed_by_id: 1,
|
||||
completed_by: { id: 1, username: 'demo_user', first_name: 'Demo', last_name: 'User' },
|
||||
notes: 'Used new grout cleaner — big improvement.',
|
||||
images: [],
|
||||
created_at: daysAgo(1),
|
||||
},
|
||||
{
|
||||
id: 3, task_id: 15, task_title: 'Trim hedges',
|
||||
completed_at: daysAgo(2),
|
||||
completed_by_id: 1,
|
||||
completed_by: { id: 1, username: 'demo_user', first_name: 'Demo', last_name: 'User' },
|
||||
notes: 'Shaped front hedges nicely.',
|
||||
images: [],
|
||||
created_at: daysAgo(2),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { UserResponse } from '@/lib/api/auth';
|
||||
|
||||
export const demoUser: UserResponse = {
|
||||
id: 1,
|
||||
username: 'demo_user',
|
||||
email: 'demo@casera.app',
|
||||
first_name: 'Demo',
|
||||
last_name: 'User',
|
||||
is_email_verified: true,
|
||||
date_joined: '2024-01-15T00:00:00Z',
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// RealProvider — DataProvider implementation that delegates to existing API modules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import type { DataProvider } from './data-provider';
|
||||
import * as residencesApi from '@/lib/api/residences';
|
||||
import * as tasksApi from '@/lib/api/tasks';
|
||||
import * as contractorsApi from '@/lib/api/contractors';
|
||||
import * as documentsApi from '@/lib/api/documents';
|
||||
import * as lookupsApi from '@/lib/api/lookups';
|
||||
import * as notificationsApi from '@/lib/api/notifications';
|
||||
import * as subscriptionApi from '@/lib/api/subscription';
|
||||
import * as authApi from '@/lib/api/auth';
|
||||
|
||||
export const realProvider: DataProvider = {
|
||||
basePath: '/app',
|
||||
|
||||
residences: {
|
||||
list: () => residencesApi.listResidences(),
|
||||
get: (id) => residencesApi.getResidence(id),
|
||||
create: (data) => residencesApi.createResidence(data),
|
||||
update: (id, data) => residencesApi.updateResidence(id, data),
|
||||
delete: (id) => residencesApi.deleteResidence(id),
|
||||
getMyResidences: () => residencesApi.getMyResidences(),
|
||||
getSummary: () => residencesApi.getSummary(),
|
||||
},
|
||||
|
||||
tasks: {
|
||||
list: (days) => tasksApi.listTasks(days),
|
||||
get: (id) => tasksApi.getTask(id),
|
||||
create: (data) => tasksApi.createTask(data),
|
||||
update: (id, data) => tasksApi.updateTask(id, data),
|
||||
delete: (id) => tasksApi.deleteTask(id),
|
||||
getByResidence: (residenceId, days) => tasksApi.getTasksByResidence(residenceId, days),
|
||||
getCompletions: (taskId) => tasksApi.getTaskCompletions(taskId),
|
||||
createCompletion: (data) => tasksApi.createCompletion(data),
|
||||
createCompletionWithImages: (data, images) => tasksApi.createCompletionWithImages(data, images),
|
||||
markInProgress: (id) => tasksApi.markInProgress(id),
|
||||
cancel: (id) => tasksApi.cancelTask(id),
|
||||
uncancel: (id) => tasksApi.uncancelTask(id),
|
||||
archive: (id) => tasksApi.archiveTask(id),
|
||||
unarchive: (id) => tasksApi.unarchiveTask(id),
|
||||
quickComplete: (id) => tasksApi.quickComplete(id),
|
||||
},
|
||||
|
||||
contractors: {
|
||||
list: () => contractorsApi.listContractors(),
|
||||
get: (id) => contractorsApi.getContractor(id),
|
||||
create: (data) => contractorsApi.createContractor(data),
|
||||
update: (id, data) => contractorsApi.updateContractor(id, data),
|
||||
delete: (id) => contractorsApi.deleteContractor(id),
|
||||
toggleFavorite: (id) => contractorsApi.toggleFavorite(id),
|
||||
getTasks: (id) => contractorsApi.getContractorTasks(id),
|
||||
},
|
||||
|
||||
documents: {
|
||||
list: (params) => documentsApi.listDocuments(params),
|
||||
listWarranties: () => documentsApi.listWarranties(),
|
||||
get: (id) => documentsApi.getDocument(id),
|
||||
create: (data) => documentsApi.createDocument(data),
|
||||
createWithFile: (data, file) => documentsApi.createDocumentWithFile(data, file),
|
||||
update: (id, data) => documentsApi.updateDocument(id, data),
|
||||
delete: (id) => documentsApi.deleteDocument(id),
|
||||
},
|
||||
|
||||
lookups: {
|
||||
getStaticData: () => lookupsApi.getStaticData(),
|
||||
},
|
||||
|
||||
sharing: {
|
||||
getShareCode: (residenceId) => residencesApi.getShareCode(residenceId),
|
||||
generateShareCode: (residenceId, data) => residencesApi.generateShareCode(residenceId, data),
|
||||
generateSharePackage: (residenceId, data) => residencesApi.generateSharePackage(residenceId, data),
|
||||
getResidenceUsers: (residenceId) => residencesApi.getResidenceUsers(residenceId),
|
||||
removeUser: (residenceId, userId) => residencesApi.removeResidenceUser(residenceId, userId),
|
||||
joinWithCode: (data) => residencesApi.joinWithCode(data),
|
||||
generateTasksReport: (residenceId, email) => residencesApi.generateTasksReport(residenceId, email),
|
||||
},
|
||||
|
||||
notifications: {
|
||||
list: (limit, offset) => notificationsApi.listNotifications(limit, offset),
|
||||
getUnreadCount: () => notificationsApi.getUnreadCount(),
|
||||
getPreferences: () => notificationsApi.getPreferences(),
|
||||
updatePreferences: (data) => notificationsApi.updatePreferences(data),
|
||||
markAsRead: (id) => notificationsApi.markAsRead(id),
|
||||
markAllAsRead: () => notificationsApi.markAllAsRead(),
|
||||
},
|
||||
|
||||
subscription: {
|
||||
getStatus: () => subscriptionApi.getSubscriptionStatus(),
|
||||
getFeatureBenefits: () => subscriptionApi.getFeatureBenefits(),
|
||||
getUpgradeTriggers: () => subscriptionApi.getAllUpgradeTriggers(),
|
||||
},
|
||||
|
||||
auth: {
|
||||
getCurrentUser: () => authApi.getCurrentUser(),
|
||||
logout: () => authApi.logout(),
|
||||
},
|
||||
};
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as authApi from '@/lib/api/auth';
|
||||
import { useDataProvider } from '@/lib/demo/data-provider-context';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export function useCurrentUser() {
|
||||
const { auth } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['auth', 'user'],
|
||||
queryFn: () => authApi.getCurrentUser(),
|
||||
queryFn: () => auth.getCurrentUser(),
|
||||
retry: false,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
@@ -16,12 +17,18 @@ export function useCurrentUser() {
|
||||
export function useLogout() {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const { auth, basePath } = useDataProvider();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => authApi.logout(),
|
||||
mutationFn: () => auth.logout(),
|
||||
onSuccess: () => {
|
||||
queryClient.clear();
|
||||
router.push('/login');
|
||||
// In demo mode, redirect to /demo; in real mode, redirect to /login
|
||||
if (basePath.startsWith('/demo')) {
|
||||
router.push('/demo');
|
||||
} else {
|
||||
router.push('/login');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as contractorsApi from '@/lib/api/contractors';
|
||||
import { useDataProvider } from '@/lib/demo/data-provider-context';
|
||||
import type { CreateContractorRequest, UpdateContractorRequest } from '@/lib/api/contractors';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -9,25 +9,30 @@ import type { CreateContractorRequest, UpdateContractorRequest } from '@/lib/api
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useContractors() {
|
||||
const { contractors } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['contractors'],
|
||||
queryFn: () => contractorsApi.listContractors(),
|
||||
queryFn: () => contractors.list(),
|
||||
select: (data) => (Array.isArray(data) ? data : []),
|
||||
});
|
||||
}
|
||||
|
||||
export function useContractor(id: number) {
|
||||
const { contractors } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['contractors', id],
|
||||
queryFn: () => contractorsApi.getContractor(id),
|
||||
queryFn: () => contractors.get(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useContractorTasks(id: number) {
|
||||
const { contractors } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['contractors', id, 'tasks'],
|
||||
queryFn: () => contractorsApi.getContractorTasks(id),
|
||||
queryFn: () => contractors.getTasks(id),
|
||||
enabled: !!id,
|
||||
select: (data) => (Array.isArray(data) ? data : []),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,9 +42,10 @@ export function useContractorTasks(id: number) {
|
||||
|
||||
export function useCreateContractor() {
|
||||
const queryClient = useQueryClient();
|
||||
const { contractors } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateContractorRequest) =>
|
||||
contractorsApi.createContractor(data),
|
||||
contractors.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contractors'] });
|
||||
},
|
||||
@@ -48,9 +54,10 @@ export function useCreateContractor() {
|
||||
|
||||
export function useUpdateContractor(id: number) {
|
||||
const queryClient = useQueryClient();
|
||||
const { contractors } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateContractorRequest) =>
|
||||
contractorsApi.updateContractor(id, data),
|
||||
contractors.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contractors'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contractors', id] });
|
||||
@@ -60,8 +67,9 @@ export function useUpdateContractor(id: number) {
|
||||
|
||||
export function useDeleteContractor() {
|
||||
const queryClient = useQueryClient();
|
||||
const { contractors } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => contractorsApi.deleteContractor(id),
|
||||
mutationFn: (id: number) => contractors.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contractors'] });
|
||||
},
|
||||
@@ -70,8 +78,9 @@ export function useDeleteContractor() {
|
||||
|
||||
export function useToggleFavorite() {
|
||||
const queryClient = useQueryClient();
|
||||
const { contractors } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => contractorsApi.toggleFavorite(id),
|
||||
mutationFn: (id: number) => contractors.toggleFavorite(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contractors'] });
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as documentsApi from '@/lib/api/documents';
|
||||
import { useDataProvider } from '@/lib/demo/data-provider-context';
|
||||
import type { DocumentListParams, CreateDocumentRequest, UpdateDocumentRequest } from '@/lib/api/documents';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -9,15 +9,26 @@ import type { DocumentListParams, CreateDocumentRequest, UpdateDocumentRequest }
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useDocuments(params?: DocumentListParams) {
|
||||
return useQuery({ queryKey: ['documents', params], queryFn: () => documentsApi.listDocuments(params) });
|
||||
const { documents } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['documents', params],
|
||||
queryFn: () => documents.list(params),
|
||||
select: (data) => (Array.isArray(data) ? data : []),
|
||||
});
|
||||
}
|
||||
|
||||
export function useWarranties() {
|
||||
return useQuery({ queryKey: ['documents', 'warranties'], queryFn: () => documentsApi.listWarranties() });
|
||||
const { documents } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['documents', 'warranties'],
|
||||
queryFn: () => documents.listWarranties(),
|
||||
select: (data) => (Array.isArray(data) ? data : []),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDocument(id: number) {
|
||||
return useQuery({ queryKey: ['documents', id], queryFn: () => documentsApi.getDocument(id), enabled: !!id });
|
||||
const { documents } = useDataProvider();
|
||||
return useQuery({ queryKey: ['documents', id], queryFn: () => documents.get(id), enabled: !!id });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -26,12 +37,13 @@ export function useDocument(id: number) {
|
||||
|
||||
export function useCreateDocument() {
|
||||
const queryClient = useQueryClient();
|
||||
const { documents } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: ({ data, file }: { data: CreateDocumentRequest; file?: File }) => {
|
||||
if (file) {
|
||||
return documentsApi.createDocumentWithFile(data, file);
|
||||
return documents.createWithFile(data, file);
|
||||
}
|
||||
return documentsApi.createDocument(data);
|
||||
return documents.create(data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] });
|
||||
@@ -41,9 +53,10 @@ export function useCreateDocument() {
|
||||
|
||||
export function useUpdateDocument(id: number) {
|
||||
const queryClient = useQueryClient();
|
||||
const { documents } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateDocumentRequest) =>
|
||||
documentsApi.updateDocument(id, data),
|
||||
documents.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['documents', id] });
|
||||
@@ -53,8 +66,9 @@ export function useUpdateDocument(id: number) {
|
||||
|
||||
export function useDeleteDocument() {
|
||||
const queryClient = useQueryClient();
|
||||
const { documents } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => documentsApi.deleteDocument(id),
|
||||
mutationFn: (id: number) => documents.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] });
|
||||
},
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as lookupsApi from '@/lib/api/lookups';
|
||||
import { useDataProvider } from '@/lib/demo/data-provider-context';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main hook — fetches all static data in a single request
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useLookups() {
|
||||
const { lookups } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['lookups', 'static-data'],
|
||||
queryFn: () => lookupsApi.getStaticData(),
|
||||
queryFn: () => lookups.getStaticData(),
|
||||
staleTime: Infinity, // ETag-based; never auto-refetch
|
||||
});
|
||||
}
|
||||
@@ -21,27 +22,32 @@ export function useLookups() {
|
||||
|
||||
export function useTaskCategories() {
|
||||
const { data, ...rest } = useLookups();
|
||||
return { data: data?.task_categories ?? [], ...rest };
|
||||
const arr = data?.task_categories;
|
||||
return { data: Array.isArray(arr) ? arr : [], ...rest };
|
||||
}
|
||||
|
||||
export function useTaskPriorities() {
|
||||
const { data, ...rest } = useLookups();
|
||||
return { data: data?.task_priorities ?? [], ...rest };
|
||||
const arr = data?.task_priorities;
|
||||
return { data: Array.isArray(arr) ? arr : [], ...rest };
|
||||
}
|
||||
|
||||
export function useTaskFrequencies() {
|
||||
const { data, ...rest } = useLookups();
|
||||
return { data: data?.task_frequencies ?? [], ...rest };
|
||||
const arr = data?.task_frequencies;
|
||||
return { data: Array.isArray(arr) ? arr : [], ...rest };
|
||||
}
|
||||
|
||||
export function useContractorSpecialties() {
|
||||
const { data, ...rest } = useLookups();
|
||||
return { data: data?.contractor_specialties ?? [], ...rest };
|
||||
const arr = data?.contractor_specialties;
|
||||
return { data: Array.isArray(arr) ? arr : [], ...rest };
|
||||
}
|
||||
|
||||
export function useResidenceTypes() {
|
||||
const { data, ...rest } = useLookups();
|
||||
return { data: data?.residence_types ?? [], ...rest };
|
||||
const arr = data?.residence_types;
|
||||
return { data: Array.isArray(arr) ? arr : [], ...rest };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as notificationsApi from '@/lib/api/notifications';
|
||||
import { useDataProvider } from '@/lib/demo/data-provider-context';
|
||||
import type { UpdatePreferencesRequest } from '@/lib/api/notifications';
|
||||
|
||||
export function useNotifications(limit?: number) {
|
||||
const { notifications } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['notifications', limit],
|
||||
queryFn: () => notificationsApi.listNotifications(limit),
|
||||
queryFn: () => notifications.list(limit),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUnreadCount() {
|
||||
const { notifications } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['notifications', 'unread-count'],
|
||||
queryFn: () => notificationsApi.getUnreadCount(),
|
||||
queryFn: () => notifications.getUnreadCount(),
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useNotificationPreferences() {
|
||||
const { notifications } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['notifications', 'preferences'],
|
||||
queryFn: () => notificationsApi.getPreferences(),
|
||||
queryFn: () => notifications.getPreferences(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdatePreferences() {
|
||||
const queryClient = useQueryClient();
|
||||
const { notifications } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdatePreferencesRequest) => notificationsApi.updatePreferences(data),
|
||||
mutationFn: (data: UpdatePreferencesRequest) => notifications.updatePreferences(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['notifications', 'preferences'] });
|
||||
},
|
||||
@@ -38,8 +42,9 @@ export function useUpdatePreferences() {
|
||||
|
||||
export function useMarkAsRead() {
|
||||
const queryClient = useQueryClient();
|
||||
const { notifications } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => notificationsApi.markAsRead(id),
|
||||
mutationFn: (id: number) => notifications.markAsRead(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['notifications'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['notifications', 'unread-count'] });
|
||||
@@ -49,8 +54,9 @@ export function useMarkAsRead() {
|
||||
|
||||
export function useMarkAllAsRead() {
|
||||
const queryClient = useQueryClient();
|
||||
const { notifications } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: () => notificationsApi.markAllAsRead(),
|
||||
mutationFn: () => notifications.markAllAsRead(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['notifications'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['notifications', 'unread-count'] });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as residencesApi from '@/lib/api/residences';
|
||||
import { useDataProvider } from '@/lib/demo/data-provider-context';
|
||||
import type { CreateResidenceRequest, UpdateResidenceRequest } from '@/lib/api/residences';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -9,16 +9,19 @@ import type { CreateResidenceRequest, UpdateResidenceRequest } from '@/lib/api/r
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useResidences() {
|
||||
const { residences } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['residences'],
|
||||
queryFn: () => residencesApi.getMyResidences(),
|
||||
queryFn: () => residences.getMyResidences(),
|
||||
select: (data) => (Array.isArray(data) ? data : []),
|
||||
});
|
||||
}
|
||||
|
||||
export function useResidence(id: number) {
|
||||
const { residences } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['residences', id],
|
||||
queryFn: () => residencesApi.getResidence(id),
|
||||
queryFn: () => residences.get(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
@@ -29,9 +32,10 @@ export function useResidence(id: number) {
|
||||
|
||||
export function useCreateResidence() {
|
||||
const queryClient = useQueryClient();
|
||||
const { residences } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateResidenceRequest) =>
|
||||
residencesApi.createResidence(data),
|
||||
residences.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['residences'] });
|
||||
},
|
||||
@@ -40,9 +44,10 @@ export function useCreateResidence() {
|
||||
|
||||
export function useUpdateResidence(id: number) {
|
||||
const queryClient = useQueryClient();
|
||||
const { residences } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateResidenceRequest) =>
|
||||
residencesApi.updateResidence(id, data),
|
||||
residences.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['residences'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['residences', id] });
|
||||
@@ -52,8 +57,9 @@ export function useUpdateResidence(id: number) {
|
||||
|
||||
export function useDeleteResidence() {
|
||||
const queryClient = useQueryClient();
|
||||
const { residences } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => residencesApi.deleteResidence(id),
|
||||
mutationFn: (id: number) => residences.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['residences'] });
|
||||
},
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as residencesApi from '@/lib/api/residences';
|
||||
import { useDataProvider } from '@/lib/demo/data-provider-context';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query hooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useShareCode(residenceId: number) {
|
||||
const { sharing } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['residences', residenceId, 'share-code'],
|
||||
queryFn: () => residencesApi.getShareCode(residenceId),
|
||||
queryFn: () => sharing.getShareCode(residenceId),
|
||||
enabled: !!residenceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useResidenceUsers(residenceId: number) {
|
||||
const { sharing } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['residences', residenceId, 'users'],
|
||||
queryFn: () => residencesApi.getResidenceUsers(residenceId),
|
||||
queryFn: () => sharing.getResidenceUsers(residenceId),
|
||||
enabled: !!residenceId,
|
||||
});
|
||||
}
|
||||
@@ -29,8 +31,9 @@ export function useResidenceUsers(residenceId: number) {
|
||||
|
||||
export function useGenerateShareCode(residenceId: number) {
|
||||
const queryClient = useQueryClient();
|
||||
const { sharing } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: () => residencesApi.generateShareCode(residenceId),
|
||||
mutationFn: () => sharing.generateShareCode(residenceId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['residences', residenceId, 'share-code'] });
|
||||
},
|
||||
@@ -39,8 +42,9 @@ export function useGenerateShareCode(residenceId: number) {
|
||||
|
||||
export function useRemoveResidenceUser(residenceId: number) {
|
||||
const queryClient = useQueryClient();
|
||||
const { sharing } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (userId: number) => residencesApi.removeResidenceUser(residenceId, userId),
|
||||
mutationFn: (userId: number) => sharing.removeUser(residenceId, userId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['residences', residenceId, 'users'] });
|
||||
},
|
||||
@@ -49,8 +53,9 @@ export function useRemoveResidenceUser(residenceId: number) {
|
||||
|
||||
export function useJoinResidence() {
|
||||
const queryClient = useQueryClient();
|
||||
const { sharing } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (code: string) => residencesApi.joinWithCode({ code }),
|
||||
mutationFn: (code: string) => sharing.joinWithCode({ code }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['residences'] });
|
||||
},
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as subscriptionApi from '@/lib/api/subscription';
|
||||
import { useDataProvider } from '@/lib/demo/data-provider-context';
|
||||
|
||||
export function useSubscriptionStatus() {
|
||||
const { subscription } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['subscription', 'status'],
|
||||
queryFn: () => subscriptionApi.getSubscriptionStatus(),
|
||||
queryFn: () => subscription.getStatus(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFeatureBenefits() {
|
||||
const { subscription } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['subscription', 'features'],
|
||||
queryFn: () => subscriptionApi.getFeatureBenefits(),
|
||||
queryFn: () => subscription.getFeatureBenefits(),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpgradeTriggers() {
|
||||
const { subscription } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['subscription', 'upgrade-triggers'],
|
||||
queryFn: () => subscriptionApi.getAllUpgradeTriggers(),
|
||||
queryFn: () => subscription.getUpgradeTriggers(),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
+26
-14
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as tasksApi from '@/lib/api/tasks';
|
||||
import { useDataProvider } from '@/lib/demo/data-provider-context';
|
||||
import type { CreateTaskRequest, UpdateTaskRequest, CreateCompletionRequest } from '@/lib/api/tasks';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -9,40 +9,45 @@ import type { CreateTaskRequest, UpdateTaskRequest, CreateCompletionRequest } fr
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useTasks() {
|
||||
const { tasks } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['tasks'],
|
||||
queryFn: () => tasksApi.listTasks(),
|
||||
queryFn: () => tasks.list(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTasksByResidence(residenceId: number) {
|
||||
const { tasks } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['tasks', 'by-residence', residenceId],
|
||||
queryFn: () => tasksApi.getTasksByResidence(residenceId),
|
||||
queryFn: () => tasks.getByResidence(residenceId),
|
||||
enabled: !!residenceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTask(id: number) {
|
||||
const { tasks } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['tasks', id],
|
||||
queryFn: () => tasksApi.getTask(id),
|
||||
queryFn: () => tasks.get(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useKanbanBoard(residenceId: number) {
|
||||
const { tasks } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['tasks', 'kanban', residenceId],
|
||||
queryFn: () => tasksApi.getTasksByResidence(residenceId),
|
||||
queryFn: () => tasks.getByResidence(residenceId),
|
||||
enabled: !!residenceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTaskCompletions(taskId: number) {
|
||||
const { tasks } = useDataProvider();
|
||||
return useQuery({
|
||||
queryKey: ['tasks', taskId, 'completions'],
|
||||
queryFn: () => tasksApi.getTaskCompletions(taskId),
|
||||
queryFn: () => tasks.getCompletions(taskId),
|
||||
enabled: !!taskId,
|
||||
});
|
||||
}
|
||||
@@ -53,8 +58,9 @@ export function useTaskCompletions(taskId: number) {
|
||||
|
||||
export function useCreateTask() {
|
||||
const queryClient = useQueryClient();
|
||||
const { tasks } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateTaskRequest) => tasksApi.createTask(data),
|
||||
mutationFn: (data: CreateTaskRequest) => tasks.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['residences'] });
|
||||
@@ -64,8 +70,9 @@ export function useCreateTask() {
|
||||
|
||||
export function useUpdateTask(id: number) {
|
||||
const queryClient = useQueryClient();
|
||||
const { tasks } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateTaskRequest) => tasksApi.updateTask(id, data),
|
||||
mutationFn: (data: UpdateTaskRequest) => tasks.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', id] });
|
||||
@@ -76,8 +83,9 @@ export function useUpdateTask(id: number) {
|
||||
|
||||
export function useDeleteTask() {
|
||||
const queryClient = useQueryClient();
|
||||
const { tasks } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => tasksApi.deleteTask(id),
|
||||
mutationFn: (id: number) => tasks.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['residences'] });
|
||||
@@ -87,8 +95,9 @@ export function useDeleteTask() {
|
||||
|
||||
export function useMarkInProgress() {
|
||||
const queryClient = useQueryClient();
|
||||
const { tasks } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => tasksApi.markInProgress(id),
|
||||
mutationFn: (id: number) => tasks.markInProgress(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
},
|
||||
@@ -97,8 +106,9 @@ export function useMarkInProgress() {
|
||||
|
||||
export function useCancelTask() {
|
||||
const queryClient = useQueryClient();
|
||||
const { tasks } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => tasksApi.cancelTask(id),
|
||||
mutationFn: (id: number) => tasks.cancel(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
},
|
||||
@@ -107,8 +117,9 @@ export function useCancelTask() {
|
||||
|
||||
export function useArchiveTask() {
|
||||
const queryClient = useQueryClient();
|
||||
const { tasks } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => tasksApi.archiveTask(id),
|
||||
mutationFn: (id: number) => tasks.archive(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
},
|
||||
@@ -117,6 +128,7 @@ export function useArchiveTask() {
|
||||
|
||||
export function useCreateCompletion() {
|
||||
const queryClient = useQueryClient();
|
||||
const { tasks } = useDataProvider();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
data,
|
||||
@@ -126,7 +138,7 @@ export function useCreateCompletion() {
|
||||
images: File[];
|
||||
}) => {
|
||||
if (images.length > 0) {
|
||||
return tasksApi.createCompletionWithImages(
|
||||
return tasks.createCompletionWithImages(
|
||||
{
|
||||
task_id: data.task_id,
|
||||
notes: data.notes,
|
||||
@@ -136,7 +148,7 @@ export function useCreateCompletion() {
|
||||
images,
|
||||
);
|
||||
}
|
||||
return tasksApi.createCompletion(data);
|
||||
return tasks.createCompletion(data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
|
||||
Reference in New Issue
Block a user