feat(trips): trip tracking with analytics, tag conversion, and transaction assignment
Adds trips table usage across API and UI: trip CRUD, per-trip analytics (category/daily/merchant/tag/participant breakdowns), tag-to-trip conversion, trip assignment via transaction overrides, and trip filter in the transactions view. Recovered from working tree after local git corruption; feature was already live via host-context Docker builds.
This commit is contained in:
+95
-1
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { TransactionRow, StatementRow, TagRow } from "./queries";
|
||||
import type { TransactionRow, StatementRow, TagRow, TripRow, TripAnalytics } from "./queries";
|
||||
export type { TripRow, TripAnalytics };
|
||||
import type { CurrentUser } from "./auth";
|
||||
|
||||
interface TransactionsResponse {
|
||||
@@ -27,6 +28,7 @@ interface TransactionFilters {
|
||||
amount_min?: number;
|
||||
amount_max?: number;
|
||||
has_split?: string;
|
||||
trip_id?: string;
|
||||
}
|
||||
|
||||
function buildParams(filters: TransactionFilters): string {
|
||||
@@ -137,6 +139,7 @@ export function useUpdateTransaction() {
|
||||
description?: string;
|
||||
amount?: number;
|
||||
transaction_date?: string;
|
||||
trip_id?: number | null;
|
||||
}) => {
|
||||
const res = await fetch(`/api/transactions/${id}`, {
|
||||
method: "PATCH",
|
||||
@@ -801,3 +804,94 @@ export function useMerchantTransactions(merchant: string | null) {
|
||||
enabled: !!merchant,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Trips ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export function useTrips() {
|
||||
return useQuery<TripRow[]>({
|
||||
queryKey: ["trips"],
|
||||
queryFn: async () => (await fetch("/api/trips")).json(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTrip(id: number) {
|
||||
return useQuery<TripRow>({
|
||||
queryKey: ["trip", id],
|
||||
queryFn: async () => (await fetch(`/api/trips/${id}`)).json(),
|
||||
enabled: id > 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTripAnalytics(id: number) {
|
||||
return useQuery<TripAnalytics>({
|
||||
queryKey: ["trip-analytics", id],
|
||||
queryFn: async () => (await fetch(`/api/trips/${id}/analytics`)).json(),
|
||||
enabled: id > 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateTrip() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (data: Omit<TripRow, "id" | "owner_id" | "created_at" | "total_spend" | "transaction_count">) => {
|
||||
const res = await fetch("/api/trips", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.json()).error || "Failed");
|
||||
return res.json() as Promise<TripRow>;
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["trips"] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateTrip() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, ...data }: Partial<TripRow> & { id: number }) => {
|
||||
const res = await fetch(`/api/trips/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.json()).error || "Failed");
|
||||
return res.json() as Promise<TripRow>;
|
||||
},
|
||||
onSuccess: (_d, { id }) => {
|
||||
qc.invalidateQueries({ queryKey: ["trips"] });
|
||||
qc.invalidateQueries({ queryKey: ["trip", id] });
|
||||
qc.invalidateQueries({ queryKey: ["trip-analytics", id] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteTrip() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
await fetch(`/api/trips/${id}`, { method: "DELETE" });
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["trips"] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignTransactionsToTrip() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ tripId, transactionIds }: { tripId: number | null; transactionIds: number[] }) => {
|
||||
const res = await fetch("/api/transactions/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "assign_trip", ids: transactionIds, trip_id: tripId }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to assign trip");
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["transactions"] });
|
||||
qc.invalidateQueries({ queryKey: ["trips"] });
|
||||
qc.invalidateQueries({ queryKey: ["trip-analytics"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user