feat: add CRM task search clients

This commit is contained in:
Matthew Meszaros
2026-06-07 07:04:21 +02:00
parent 5f3940602f
commit 4e015e7efd
6 changed files with 160 additions and 0 deletions
@@ -0,0 +1,21 @@
import type SearchTasks from "@/lib/api/models/app/crm/SearchTasks";
import type TasksSearchResult from "@/lib/api/models/app/crm/TasksSearchResult";
import Request from "../../../Request";
export default async function searchTasks(
filters: SearchTasks,
offset = 0,
limit = 50,
): Promise<TasksSearchResult> {
const qs = new URLSearchParams();
if (offset) qs.set("offset", String(offset));
if (limit) qs.set("limit", String(limit));
const suffix = qs.toString() ? `?${qs.toString()}` : "";
return await Request<TasksSearchResult>({
method: "POST",
url: `/crm/tasks/search${suffix}`,
data: filters,
authorization: true,
});
}
@@ -0,0 +1,12 @@
import type SearchTasks from "@/lib/api/models/app/crm/SearchTasks";
import type { TasksSummary } from "@/lib/api/models/app/crm/TasksSearchResult";
import Request from "../../../Request";
export default async function tasksSummary(filters: SearchTasks): Promise<TasksSummary> {
return await Request<TasksSummary>({
method: "POST",
url: "/crm/tasks/summary",
data: filters,
authorization: true,
});
}
@@ -0,0 +1,41 @@
import { useInfiniteQuery, type InfiniteData } from "@tanstack/react-query";
import type SearchTasks from "@/lib/api/models/app/crm/SearchTasks";
import type TasksSearchResult from "@/lib/api/models/app/crm/TasksSearchResult";
import searchTasks from "@/lib/api/client/app/crm/tasks/searchTasks";
interface UseSearchTasksProps {
filters: SearchTasks;
limit?: number;
enabled?: boolean;
}
// Server-driven tasks fetch that scales to thousands of rows. Offset-paginated
// (the backend uses offset rather than a keyset cursor so nullable due-date
// sorts don't drop rows), so the page param is the next offset. Pages flatten
// into a single `tasks` list and `total` comes straight off the server so the
// UI can show "N of M loaded".
export default function useSearchTasks({ filters, limit = 50, enabled = true }: UseSearchTasksProps) {
const queryResult = useInfiniteQuery<
TasksSearchResult,
Error,
InfiniteData<TasksSearchResult, number>,
[string, string, string, SearchTasks, number],
number
>({
queryKey: ["crm", "tasks", "search", filters, limit],
queryFn: async ({ pageParam }) => searchTasks(filters, pageParam, limit),
initialPageParam: 0,
getNextPageParam: (lastPage) =>
lastPage.pagination.has_more ? (lastPage.pagination.next_offset ?? undefined) : undefined,
staleTime: 30_000,
enabled,
});
const tasks = queryResult.data?.pages
.flatMap((p) => p.data ?? [])
.filter((t): t is NonNullable<typeof t> => t != null);
const total = queryResult.data?.pages[0]?.pagination.total ?? 0;
return { ...queryResult, tasks, total };
}
@@ -0,0 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import type SearchTasks from "@/lib/api/models/app/crm/SearchTasks";
import tasksSummary from "@/lib/api/client/app/crm/tasks/tasksSummary";
// Server-aggregated totals for the same filter the table renders. Kept as a
// separate query (not folded into the list) so the header stats stay correct
// over the whole set while the rows page in.
export default function useTasksSummary(filters: SearchTasks, enabled = true) {
return useQuery({
queryKey: ["crm", "tasks", "summary", filters],
queryFn: () => tasksSummary(filters),
staleTime: 30_000,
enabled,
});
}
@@ -0,0 +1,42 @@
// Filter body for POST /crm/tasks/search and /crm/tasks/summary. Every field
// is optional; an empty body matches every task in the org (the "All tasks"
// default). The same body drives the rows and the summary totals, so a header
// number always reflects the exact filter shown below it.
import type { CRMTaskPriority, CRMTaskStatus } from "@/lib/api/models/app/crm/CRMTask";
export type TaskSortBy =
| "created_at"
| "updated_at"
| "due_date"
| "priority"
| "title";
export default interface SearchTasks {
query: string;
statuses: CRMTaskStatus[];
priorities: CRMTaskPriority[];
// Task type NAMEs (crm_tasks.type), e.g. ["Call", "Email"].
types: string[];
// User UUIDs (assigned_to). String-matched on the server.
assigned_to: string[];
contact_id?: string;
deal_id?: string;
due_after?: string;
due_before?: string;
// due_date < now() AND status NOT IN ('completed','cancelled').
overdue?: boolean;
sort_by: TaskSortBy;
// false => DESC (default), true => ASC.
reverse: boolean;
}
export const EMPTY_TASK_SEARCH: SearchTasks = {
query: "",
statuses: [],
priorities: [],
types: [],
assigned_to: [],
sort_by: "created_at",
reverse: false,
};
@@ -0,0 +1,29 @@
import type CRMTask from "@/lib/api/models/app/crm/CRMTask";
export interface TasksSearchPagination {
total: number;
limit: number;
offset: number;
has_more: boolean;
next_offset?: number | null;
}
export default interface TasksSearchResult {
data: CRMTask[];
pagination: TasksSearchPagination;
}
// Server-aggregated totals over a SearchTasks filter. Every number here is a
// COUNT over the whole matching set — never a client reduce over a loaded
// page — so the header stats stay honest at scale.
export interface TasksSummary {
total: number;
pending_count: number;
in_progress_count: number;
completed_count: number;
cancelled_count: number;
// due_date < now() AND status NOT IN ('completed','cancelled').
overdue_count: number;
// priority IN ('high','urgent').
high_priority_count: number;
}