- Add useReorderCandidates mutation hook with apiPatch to /candidates/reorder endpoint - Add candidateViewMode (list|grid) state and setCandidateViewMode to uiStore - Create CandidateListItem component with drag handle, rank badge, horizontal layout - Export RankBadge helper (gold/silver/bronze medal icons for top 3) - Add style prop support to LucideIcon component - Add pros/cons fields to CandidateWithCategory in useThreads.ts
120 lines
2.9 KiB
TypeScript
120 lines
2.9 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { apiDelete, apiGet, apiPost, apiPut } from "../lib/api";
|
|
|
|
interface ThreadListItem {
|
|
id: number;
|
|
name: string;
|
|
status: "active" | "resolved";
|
|
resolvedCandidateId: number | null;
|
|
categoryId: number;
|
|
categoryName: string;
|
|
categoryIcon: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
candidateCount: number;
|
|
minPriceCents: number | null;
|
|
maxPriceCents: number | null;
|
|
}
|
|
|
|
interface CandidateWithCategory {
|
|
id: number;
|
|
threadId: number;
|
|
name: string;
|
|
weightGrams: number | null;
|
|
priceCents: number | null;
|
|
categoryId: number;
|
|
notes: string | null;
|
|
productUrl: string | null;
|
|
imageFilename: string | null;
|
|
status: "researching" | "ordered" | "arrived";
|
|
pros: string | null;
|
|
cons: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
categoryName: string;
|
|
categoryIcon: string;
|
|
}
|
|
|
|
interface ThreadWithCandidates {
|
|
id: number;
|
|
name: string;
|
|
status: "active" | "resolved";
|
|
resolvedCandidateId: number | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
candidates: CandidateWithCategory[];
|
|
}
|
|
|
|
export function useThreads(includeResolved = false) {
|
|
return useQuery({
|
|
queryKey: ["threads", { includeResolved }],
|
|
queryFn: () =>
|
|
apiGet<ThreadListItem[]>(
|
|
`/api/threads${includeResolved ? "?includeResolved=true" : ""}`,
|
|
),
|
|
});
|
|
}
|
|
|
|
export function useThread(threadId: number | null) {
|
|
return useQuery({
|
|
queryKey: ["threads", threadId],
|
|
queryFn: () => apiGet<ThreadWithCandidates>(`/api/threads/${threadId}`),
|
|
enabled: threadId != null,
|
|
});
|
|
}
|
|
|
|
export function useCreateThread() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: { name: string; categoryId: number }) =>
|
|
apiPost<ThreadListItem>("/api/threads", data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["threads"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateThread() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, ...data }: { id: number; name?: string }) =>
|
|
apiPut<ThreadListItem>(`/api/threads/${id}`, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["threads"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteThread() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (id: number) =>
|
|
apiDelete<{ success: boolean }>(`/api/threads/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["threads"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useResolveThread() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({
|
|
threadId,
|
|
candidateId,
|
|
}: {
|
|
threadId: number;
|
|
candidateId: number;
|
|
}) =>
|
|
apiPost<{ success: boolean; item: unknown }>(
|
|
`/api/threads/${threadId}/resolve`,
|
|
{ candidateId },
|
|
),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["threads"] });
|
|
queryClient.invalidateQueries({ queryKey: ["items"] });
|
|
queryClient.invalidateQueries({ queryKey: ["totals"] });
|
|
},
|
|
});
|
|
}
|